diff --git a/src/services/deployment_credential_service.py b/src/services/deployment_credential_service.py index b3a9d02..78b8c31 100644 --- a/src/services/deployment_credential_service.py +++ b/src/services/deployment_credential_service.py @@ -100,24 +100,51 @@ def _extract_access_entries( for application in user_json.get("applications") or []: app_name = (application.get("name") or "").lower() is_pgadmin = "pgadmin" in app_name + # pgAdmin entries become WEB_URL accesses (the user-visible UI is + # the pgadmin web app), everything else (postgres, mysql, ...) is + # a DATABASE entry. The frontend renders them differently. + access_type = AccessType.WEB_URL if is_pgadmin else AccessType.DATABASE for cred in application.get("credentials") or []: + username = cred.get("email") or cred.get("db_user") or cred.get("username") + db_name = cred.get("database_name") or cred.get("db_name") + if is_pgadmin: + conn_url, port = pgadmin_url, 80 + elif username and floating_ip and db_name: + conn_url = f"postgresql://{username}@{floating_ip}/{db_name}" + port = cred.get("port") or 5432 + elif username and floating_ip: + conn_url = f"postgresql://{username}@{floating_ip}" + port = cred.get("port") or 5432 + else: + conn_url, port = None, cred.get("port") or 5432 entries.append({ - "access_type": AccessType.DATABASE, - "username": cred.get("email") or cred.get("db_user") or cred.get("username"), + "access_type": access_type, + "username": username, "password": cred.get("password"), - "connection_url": pgadmin_url if is_pgadmin else None, - "port": 80 if is_pgadmin else None, + # Stamp the CourseGroup FK so student self-service filters + # work for application credentials too — without this, + # postgres/pgadmin entries always landed in the "Dozent" tab. + "group_id": cred.get("group_id"), + "connection_url": conn_url, + "port": port, }) app_admin = application.get("admin_credentials") if app_admin: + a_username = app_admin.get("email") or app_admin.get("db_user") or app_admin.get("username") entries.append({ - "access_type": AccessType.DATABASE, - "username": app_admin.get("email") or app_admin.get("db_user") or app_admin.get("username"), + "access_type": access_type, + "username": a_username, "password": app_admin.get("password"), - "connection_url": pgadmin_url if is_pgadmin else None, - "port": 80 if is_pgadmin else None, + # Admin app-credentials (e.g. pgAdmin superuser, postgres + # superuser) are intentionally not tied to a CourseGroup — + # they show up in the lecturer's "Dozent" tab. + "group_id": None, + "connection_url": pgadmin_url if is_pgadmin else ( + f"postgresql://{a_username}@{floating_ip}" if a_username and floating_ip else None + ), + "port": 80 if is_pgadmin else 5432, }) return [e for e in entries if e.get("password") or e.get("ssh_private_key")] diff --git a/src/tasks/deploy_tasks.py b/src/tasks/deploy_tasks.py index 0e9f481..adad05e 100644 --- a/src/tasks/deploy_tasks.py +++ b/src/tasks/deploy_tasks.py @@ -251,7 +251,69 @@ def deploy_stack(self, deployment_id: str) -> dict: ) try: - # Build user_json from `generated` so DB passwords match what Ansible sets + # Build user_json from `generated` so DB passwords match what Ansible sets. + # + # The shape mirrors what the lecturer-side credentials view + # expects (DeploymentCredentialService._extract_access_entries): + # * instance.credentials = per-group Linux/SSH logins + # * instance.admin_credentials = teacher Linux/SSH + # * applications[] = one entry per non-linux credential + # type (postgres, pgadmin, ...) that the app.yaml asked + # for. Without applications[], DB/pgAdmin credentials + # never reach the credentials API and the UI's + # "Gruppen" tab stays empty for those apps. + _GROUP_META_KEYS = {"username", "email", "group_name", "group_index", "students", "course_group_id"} + + # Collect every non-linux credential type that ANY group or + # the teacher produced. linux is handled by instance.* above. + app_cred_types: set[str] = set() + for g in generated.get("deployment_groups", []): + for k, v in g.items(): + if k in _GROUP_META_KEYS or k == "linux" or not isinstance(v, dict): + continue + app_cred_types.add(k) + for k, v in (generated.get("teacher") or {}).items(): + if k in _GROUP_META_KEYS or k == "linux" or not isinstance(v, dict): + continue + app_cred_types.add(k) + + applications_payload: list[dict] = [] + for cred_type in sorted(app_cred_types): + per_group_creds: list[dict] = [] + for g in generated.get("deployment_groups", []): + spec = g.get(cred_type) + if not isinstance(spec, dict) or not spec.get("password"): + continue + per_group_creds.append({ + "email": spec.get("email"), + "db_user": spec.get("db_user"), + "username": spec.get("username"), + "password": spec.get("password"), + "database_name": spec.get("database_name") or spec.get("db_name"), + # Same group_id stamp as for instance.credentials + # above — drives the Dozent/Gruppen split in the UI. + "group_id": g.get("course_group_id"), + }) + + teacher_spec = (generated.get("teacher") or {}).get(cred_type) + admin_payload = None + if isinstance(teacher_spec, dict) and teacher_spec.get("password"): + admin_payload = { + "email": teacher_spec.get("email"), + "db_user": teacher_spec.get("db_user"), + "username": teacher_spec.get("username"), + "password": teacher_spec.get("password"), + } + + if not per_group_creds and not admin_payload: + continue + + applications_payload.append({ + "name": cred_type, + "credentials": per_group_creds, + "admin_credentials": admin_payload, + }) + credentials_for_db = { "instance": { "credentials": [ @@ -276,6 +338,7 @@ def deploy_stack(self, deployment_id: str) -> dict: "ssh_private_key": (generated["teacher"]["linux"].get("ssh_key") or {}).get("private_key"), } if generated.get("teacher", {}).get("linux", {}).get("password") else None, }, + "applications": applications_payload, } DeploymentCredentialService(db).persist_credentials_for_stack( deployment_id=deployment_id, diff --git a/tests/unit/test_deployment_credential_service.py b/tests/unit/test_deployment_credential_service.py index 97576a7..1ce0a50 100644 --- a/tests/unit/test_deployment_credential_service.py +++ b/tests/unit/test_deployment_credential_service.py @@ -52,7 +52,12 @@ def test_extracts_postgres_credentials(): rows = DeploymentCredentialService._extract_access_entries(user_json) assert len(rows) == 4 - assert all(r["access_type"] == AccessType.DATABASE for r in rows) + # postgres → DATABASE, pgadmin → WEB_URL so the frontend can render them + # differently (DB connection string vs clickable web URL). + assert rows[0]["access_type"] == AccessType.DATABASE + assert rows[1]["access_type"] == AccessType.DATABASE + assert rows[2]["access_type"] == AccessType.WEB_URL + assert rows[3]["access_type"] == AccessType.WEB_URL # Postgres credentials use db_user; pgAdmin uses email when db_user absent. assert rows[0]["username"] == "grp1" assert rows[1]["username"] == "teacher"