Add app home Spaces shell and session diagnostics profile menu
Post-login lands on Spaces empty state instead of a principal dump. Profile menu exposes Session details with identity, tenant, roles/groups, and authz diagnostics for operator refinement (CSOC-WP-0004 T01/T07).
This commit is contained in:
parent
8169102866
commit
422cd613f6
10 changed files with 404 additions and 88 deletions
|
|
@ -1,10 +1,16 @@
|
|||
from django.conf import settings
|
||||
|
||||
from coulomb_social.apps.core.principal import display_name_for
|
||||
|
||||
|
||||
def site_context(request):
|
||||
return {
|
||||
ctx = {
|
||||
"site_name": "coulomb.social",
|
||||
"default_tenant_id": settings.DEFAULT_TENANT_ID,
|
||||
"oidc_enabled": settings.OIDC_ENABLED,
|
||||
"registration_enabled": bool(settings.NETKINGDOM_REGISTRATION_URL),
|
||||
"nav_display_name": "",
|
||||
}
|
||||
if getattr(request, "user", None) is not None and request.user.is_authenticated:
|
||||
ctx["nav_display_name"] = display_name_for(request)
|
||||
return ctx
|
||||
|
|
|
|||
75
coulomb_social/apps/core/principal.py
Normal file
75
coulomb_social/apps/core/principal.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""Build principal/session diagnostic dicts for templates (no secrets)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
from coulomb_social.apps.identity import flex_auth
|
||||
|
||||
|
||||
def display_name_for(request: HttpRequest) -> str:
|
||||
member = getattr(request.user, "member", None)
|
||||
if member and member.display_name:
|
||||
return member.display_name
|
||||
return request.user.get_full_name() or request.user.get_username()
|
||||
|
||||
|
||||
def build_principal(request: HttpRequest, *, authz_resource_id: str = "app_home") -> dict[str, Any]:
|
||||
"""Principal card fields + optional session claim diagnostics."""
|
||||
member = getattr(request.user, "member", None)
|
||||
subject = member.subject if member else request.user.get_username()
|
||||
tenant = member.tenant_id if member else ""
|
||||
decision = flex_auth.check(
|
||||
"shell:view",
|
||||
resource="shell",
|
||||
resource_id=authz_resource_id,
|
||||
subject=subject,
|
||||
tenant=tenant,
|
||||
attributes={
|
||||
"issuer": member.issuer if member else "",
|
||||
"user_engine_user_id": str(member.user_engine_user_id) if member else "",
|
||||
},
|
||||
)
|
||||
groups = request.session.get("identity_groups") or []
|
||||
roles = request.session.get("identity_roles") or []
|
||||
assurance = request.session.get("identity_assurance") or {}
|
||||
return {
|
||||
"username": request.user.get_username(),
|
||||
"display_name": display_name_for(request),
|
||||
"email": (member.email if member else "") or request.user.email or "",
|
||||
"issuer": member.issuer if member else "",
|
||||
"subject": member.subject if member else "",
|
||||
"user_engine_user_id": str(member.user_engine_user_id) if member else "",
|
||||
"tenant_id": member.tenant_id if member else "",
|
||||
"user_engine_source": request.session.get("user_engine_source", ""),
|
||||
"principal_type": request.session.get("identity_principal_type", "") or "—",
|
||||
"groups": groups,
|
||||
"roles": roles,
|
||||
"groups_display": ", ".join(str(g) for g in groups) if groups else "—",
|
||||
"roles_display": ", ".join(str(r) for r in roles) if roles else "—",
|
||||
"assurance": assurance,
|
||||
"assurance_display": _format_assurance(assurance),
|
||||
"authz_decision_id": decision.decision_id,
|
||||
"authz_reason": decision.reason,
|
||||
"authz_allow": decision.allow,
|
||||
"member": member,
|
||||
"decision": decision,
|
||||
}
|
||||
|
||||
|
||||
def _format_assurance(assurance: dict[str, Any]) -> str:
|
||||
if not assurance:
|
||||
return "—"
|
||||
aal = assurance.get("aal") or assurance.get("acr") or ""
|
||||
methods = assurance.get("methods") or []
|
||||
mfa = assurance.get("mfa")
|
||||
parts: list[str] = []
|
||||
if aal:
|
||||
parts.append(str(aal))
|
||||
if methods:
|
||||
parts.append("methods=" + ",".join(str(m) for m in methods))
|
||||
if mfa is not None:
|
||||
parts.append(f"mfa={mfa}")
|
||||
return "; ".join(parts) if parts else str(assurance)
|
||||
|
|
@ -7,4 +7,5 @@ app_name = "core"
|
|||
urlpatterns = [
|
||||
path("", views.landing, name="landing"),
|
||||
path("app/", views.app_home, name="app_home"),
|
||||
path("account/session/", views.account_session, name="account_session"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,61 +1,51 @@
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from coulomb_social.apps.identity import flex_auth
|
||||
from .principal import build_principal
|
||||
|
||||
|
||||
def landing(request: HttpRequest) -> HttpResponse:
|
||||
if request.user.is_authenticated:
|
||||
from django.shortcuts import redirect
|
||||
|
||||
return redirect("core:app_home")
|
||||
return render(request, "core/landing.html")
|
||||
|
||||
|
||||
@login_required
|
||||
def app_home(request: HttpRequest) -> HttpResponse:
|
||||
member = getattr(request.user, "member", None)
|
||||
subject = member.subject if member else request.user.get_username()
|
||||
tenant = member.tenant_id if member else ""
|
||||
decision = flex_auth.check(
|
||||
"shell:view",
|
||||
resource="shell",
|
||||
resource_id="app_home",
|
||||
subject=subject,
|
||||
tenant=tenant,
|
||||
attributes={
|
||||
"issuer": member.issuer if member else "",
|
||||
"user_engine_user_id": str(member.user_engine_user_id) if member else "",
|
||||
},
|
||||
)
|
||||
if not decision.allow:
|
||||
principal = build_principal(request, authz_resource_id="app_home")
|
||||
if not principal["authz_allow"]:
|
||||
return HttpResponseForbidden(
|
||||
f"Not authorized to view shell ({decision.reason})."
|
||||
f"Not authorized to view shell ({principal['authz_reason']})."
|
||||
)
|
||||
|
||||
# Spaces list is empty until CSOC-WP-0004-T02; keep the product empty-state.
|
||||
spaces: list[dict] = []
|
||||
return render(
|
||||
request,
|
||||
"core/app_home.html",
|
||||
{
|
||||
"member": member,
|
||||
"principal": {
|
||||
"username": request.user.get_username(),
|
||||
"display_name": (
|
||||
member.display_name
|
||||
if member and member.display_name
|
||||
else request.user.get_full_name() or request.user.get_username()
|
||||
),
|
||||
"issuer": member.issuer if member else "",
|
||||
"subject": member.subject if member else "",
|
||||
"user_engine_user_id": (
|
||||
str(member.user_engine_user_id) if member else ""
|
||||
),
|
||||
"tenant_id": member.tenant_id if member else "",
|
||||
"user_engine_source": request.session.get("user_engine_source", ""),
|
||||
"authz_decision_id": decision.decision_id,
|
||||
"authz_reason": decision.reason,
|
||||
},
|
||||
"principal": principal,
|
||||
"display_name": principal["display_name"],
|
||||
"spaces": spaces,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def account_session(request: HttpRequest) -> HttpResponse:
|
||||
"""Principal / session diagnostics (profile menu → T07)."""
|
||||
principal = build_principal(request, authz_resource_id="account_session")
|
||||
if not principal["authz_allow"]:
|
||||
return HttpResponseForbidden(
|
||||
f"Not authorized to view session details ({principal['authz_reason']})."
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"core/account_session.html",
|
||||
{
|
||||
"principal": principal,
|
||||
"display_name": principal["display_name"],
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,9 +59,13 @@ def establish_session(request: HttpRequest, claims: IdentityClaims) -> Member:
|
|||
member.last_login_at = timezone.now()
|
||||
member.save()
|
||||
|
||||
# Stash provenance for debugging shell (no secrets)
|
||||
# Stash non-secret claim provenance for account diagnostics (T07)
|
||||
request.session["user_engine_source"] = link.source
|
||||
request.session["user_engine_user_id"] = link.user_id
|
||||
request.session["identity_groups"] = list(claims.groups)
|
||||
request.session["identity_roles"] = list(claims.roles)
|
||||
request.session["identity_principal_type"] = claims.principal_type or "human"
|
||||
request.session["identity_assurance"] = dict(claims.assurance or {})
|
||||
|
||||
login(request, user, backend="django.contrib.auth.backends.ModelBackend")
|
||||
return member
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue