Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions src/services/deployment_credential_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
65 changes: 64 additions & 1 deletion src/tasks/deploy_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion tests/unit/test_deployment_credential_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading