"""Authentik Core-API: apps, brugere, grupper. Læsninger er cachet (cache.py); skrivninger rydder cachen via cache.clear() i kaldsstedet (main.py).""" import asyncio import httpx import cache from config import API_TOKEN, AUTHENTIK, GROUP_ORDER def _h(): return {"Authorization": f"Bearer {API_TOKEN}"} def _client(timeout=15): return httpx.AsyncClient(base_url=AUTHENTIK, headers=_h(), timeout=timeout) async def fetch_user_apps(username: str): """Apps brugeren har adgang til. Cachet 45s.""" ck = f"apps:{username}" hit = cache.get(ck) if hit is not None: return hit async with _client(10) as c: u = await c.get("/api/v3/core/users/", params={"username": username}) results = u.json().get("results", []) if not results: return [] pk = results[0]["pk"] a = await c.get("/api/v3/core/applications/", params={"for_user": pk, "superuser_full_list": "false", "page_size": 100}) apps = a.json().get("results", []) out = [] for app_ in apps: if app_.get("slug") == "weircon-portal": continue url = app_.get("launch_url") or app_.get("meta_launch_url") if not url: continue host = url.split("//", 1)[-1].split("/", 1)[0] out.append({"slug": app_.get("slug"), "name": app_.get("name"), "url": url, "host": host, "group": (app_.get("group") or "").strip(), "desc": (app_.get("meta_description") or "").strip(), "icon": app_.get("meta_icon") or ""}) out.sort(key=lambda x: (x["name"] or "").lower()) cache.put(ck, out, 45) return out def group_apps(apps): groups = {} for a in apps: groups.setdefault(a.get("group") or "Øvrige", []).append(a) def gkey(name): return (999, "") if name == "Øvrige" else (GROUP_ORDER.get(name, 50), name.lower()) return [{"name": g, "apps": sorted(groups[g], key=lambda x: (x["name"] or "").lower())} for g in sorted(groups, key=gkey)] async def fetch_all_apps_admin(): hit = cache.get("apps_admin") if hit is not None: return hit async with _client(15) as c: r = await c.get("/api/v3/core/applications/", params={"page_size": 100, "superuser_full_list": "true"}) apps = r.json().get("results", []) out = [{"slug": a["slug"], "name": a.get("name"), "group": a.get("group") or "", "desc": (a.get("meta_description") or "").strip()} for a in apps if a.get("slug") != "weircon-portal"] out.sort(key=lambda x: ((x["group"] or "~~").lower(), (x["name"] or "").lower())) cache.put("apps_admin", out, 45) return out async def patch_apps(rows): async with _client(20) as c: async def one(r): try: resp = await c.patch(f"/api/v3/core/applications/{r['slug']}/", json={"meta_description": r["desc"], "group": r["group"]}) return resp.status_code < 300 except Exception: return False results = await asyncio.gather(*[one(r) for r in rows]) return sum(1 for ok in results if ok) async def list_groups(): hit = cache.get("groups") if hit is not None: return hit async with _client(15) as c: gs = (await c.get("/api/v3/core/groups/", params={"page_size": 100, "ordering": "name"})).json().get("results", []) res = [{"pk": g["pk"], "name": g["name"], "members": len(g.get("users") or [])} for g in gs] cache.put("groups", res, 30) return res async def list_users(): hit = cache.get("users") if hit is not None: return hit async with _client(20) as c: us = (await c.get("/api/v3/core/users/", params={"page_size": 300, "ordering": "username"})).json().get("results", []) out = [{"pk": u["pk"], "username": u["username"], "name": u.get("name"), "email": u.get("email"), "active": u.get("is_active"), "groups": [str(x) for x in (u.get("groups") or [])], "group_names": [g.get("name") for g in (u.get("groups_obj") or [])], "type": u.get("type")} for u in us] cache.put("users", out, 30) return out async def get_user(pk): async with _client(15) as c: u = (await c.get(f"/api/v3/core/users/{pk}/")).json() return {"pk": u["pk"], "username": u["username"], "name": u.get("name"), "email": u.get("email"), "active": u.get("is_active"), "groups": [str(x) for x in (u.get("groups") or [])]} async def create_user(username, name, email, password, group_pks): async with _client(20) as c: r = await c.post("/api/v3/core/users/", json={ "username": username, "name": name or username, "email": email or "", "is_active": True, "type": "internal", "path": "users", "groups": group_pks}) if r.status_code >= 300: return False, r.text[:300] pk = r.json()["pk"] if password: await c.post(f"/api/v3/core/users/{pk}/set_password/", json={"password": password}) return True, pk async def update_user(pk, group_pks, is_active, password=""): async with _client(20) as c: r = await c.patch(f"/api/v3/core/users/{pk}/", json={"groups": group_pks, "is_active": is_active}) if password: await c.post(f"/api/v3/core/users/{pk}/set_password/", json={"password": password}) return r.status_code < 300 async def create_group(name): async with _client(15) as c: r = await c.post("/api/v3/core/groups/", json={"name": name}) return r.status_code < 300 async def group_members(gpk): async with _client(15) as c: g = (await c.get(f"/api/v3/core/groups/{gpk}/")).json() return {"pk": g["pk"], "name": g["name"], "users": [str(x) for x in (g.get("users") or [])]} async def set_group_members(gpk, user_pks): desired = set(str(x) for x in user_pks) async with _client(25) as c: g = (await c.get(f"/api/v3/core/groups/{gpk}/")).json() cur = set(str(x) for x in (g.get("users") or [])) ops = [c.post(f"/api/v3/core/groups/{gpk}/add_user/", json={"pk": int(p)}) for p in desired - cur] ops += [c.post(f"/api/v3/core/groups/{gpk}/remove_user/", json={"pk": int(p)}) for p in cur - desired] await asyncio.gather(*ops) return True async def admin_group_pk(name): """UUID for ADMIN_GROUP-navnet (til guardrails). Cachet via list_groups().""" for g in await list_groups(): if g["name"] == name: return g["pk"] return None async def user_by_username(username): """Slå nuværende bruger op (pk + gruppe-uuids) til guardrail-tjek.""" for u in await list_users(): if u["username"] == username: return {"pk": str(u["pk"]), "groups": set(u["groups"])} return None # ---------------- per-app adgang (Authentik policy-bindings) ---------------- # Adgang til en app styres af bindings på app'en: ingen bindings = åben for alle # autentificerede; ≥1 binding = kun de bundne grupper/brugere. Vi administrerer # KUN simple (negate=false) gruppe-/bruger-bindings og lader policy-/negate- # bindings være urørte (de styres bevidst i Authentik selv). async def _all_app_bindings(c): out, page = {}, 1 while True: r = (await c.get("/api/v3/policies/bindings/", params={"page_size": 100, "page": page})).json() for b in r.get("results", []): out.setdefault(b.get("target"), []).append(b) if not (r.get("pagination") or {}).get("next"): break page += 1 return out def _split_bindings(blist): """→ (gruppe-uuids, bruger-pk-strenge, antal andre bindings vi ikke rører).""" groups, users, other = set(), set(), 0 for b in blist or []: if b.get("negate") or b.get("policy"): other += 1 elif b.get("group"): groups.add(str(b["group"])) elif b.get("user") is not None: users.add(str(b["user"])) else: other += 1 return groups, users, other async def list_apps_access(): """Alle apps + adgangs-resumé (åben / antal principaler). Cachet 30s.""" hit = cache.get("access:list") if hit is not None: return hit async with _client(20) as c: r = await c.get("/api/v3/core/applications/", params={"page_size": 100, "superuser_full_list": "true"}) apps = r.json().get("results", []) bindings = await _all_app_bindings(c) out = [] for a in apps: if a.get("slug") == "weircon-portal": continue blist = bindings.get(a["pk"], []) g, u, other = _split_bindings(blist) out.append({"slug": a["slug"], "name": a.get("name"), "pk": a["pk"], "group": a.get("group") or "", "icon": a.get("meta_icon") or "", "open": not blist, "n_groups": len(g), "n_users": len(u), "n_other": other}) out.sort(key=lambda x: ((x["group"] or "~~").lower(), (x["name"] or "").lower())) cache.put("access:list", out, 30) return out async def app_access(slug): """Bundne grupper/brugere for én app (til redigering).""" async with _client(20) as c: r = (await c.get("/api/v3/core/applications/", params={"slug": slug, "superuser_full_list": "true"})).json() results = r.get("results", []) if not results: return None app_ = results[0] b = (await c.get("/api/v3/policies/bindings/", params={"target": app_["pk"], "page_size": 100})).json().get("results", []) g, u, other = _split_bindings(b) return {"slug": app_["slug"], "name": app_.get("name"), "pk": app_["pk"], "icon": app_.get("meta_icon") or "", "groups": g, "users": u, "n_other": other} async def set_app_access(app_pk, want_groups, want_users): """Diff & anvend simple gruppe-/bruger-bindings for én app. Returnerer (antal_tilføjet, antal_fjernet). Rører ikke policy-/negate-bindings.""" want_g = set(str(x) for x in want_groups) want_u = set(str(x) for x in want_users) async with _client(25) as c: cur = (await c.get("/api/v3/policies/bindings/", params={"target": app_pk, "page_size": 100})).json().get("results", []) cur_g = {str(b["group"]): b["pk"] for b in cur if b.get("group") and not b.get("negate")} cur_u = {str(b["user"]): b["pk"] for b in cur if b.get("user") is not None and not b.get("negate")} orders = [b.get("order") or 0 for b in cur] nxt = (max(orders) + 1) if orders else 0 ops = [] for gid in want_g - set(cur_g): ops.append(c.post("/api/v3/policies/bindings/", json={"target": app_pk, "group": gid, "order": nxt, "enabled": True})) nxt += 1 for uid in want_u - set(cur_u): ops.append(c.post("/api/v3/policies/bindings/", json={"target": app_pk, "user": int(uid), "order": nxt, "enabled": True})) nxt += 1 dels = [cur_g[g] for g in set(cur_g) - want_g] + [cur_u[u] for u in set(cur_u) - want_u] for pk in dels: ops.append(c.delete(f"/api/v3/policies/bindings/{pk}/")) await asyncio.gather(*ops) return len(want_g - set(cur_g)) + len(want_u - set(cur_u)), len(dels)