feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run

This commit is contained in:
CodexPi
2026-11-14 20:23:59 +01:00
commit 995b17a6e2
295 changed files with 36803 additions and 0 deletions

104
django_ui/identity/views.py Normal file
View File

@@ -0,0 +1,104 @@
"""Same-origin identity contract in the existing Django service, deny by default."""
from functools import wraps
from django.contrib.auth import get_user_model
from django.db import DatabaseError
from django.http import JsonResponse
from django.views.decorators.http import require_GET, require_POST, require_http_methods
from django_ui.views import AUDIT, client_ip, json_body, security_headers
from .models import APPLICATIONS, ROLES, ApplicationGrant
PERMISSIONS = {"reader": ("read",), "operator": ("read", "write"), "admin": ("read", "write", "admin")}
def reply(body, status=200):
response = security_headers(JsonResponse(body, status=status))
response["Vary"] = "Cookie"
return response
def identity_request(view):
@wraps(view)
def guarded(request, application, **kwargs):
try:
if not request.user.is_authenticated:
return reply({"detail": "Authentification requise"}, 401)
if application not in APPLICATIONS:
return reply({"detail": "Application inconnue"}, 404)
if request.headers.get("Sec-Fetch-Site") == "cross-site":
return reply({"detail": "Requête intersite refusée"}, 403)
if request.method not in {"GET", "HEAD", "OPTIONS"} and (
request.content_type != "application/json" or len(request.body) > 4096
):
return reply({"detail": "Corps JSON limité requis"}, 400)
return view(request, application, **kwargs)
except DatabaseError:
return reply({"detail": "Identité indisponible ; accès refusé"}, 503)
return guarded
def principal(request, application):
grant = ApplicationGrant.objects.filter(user=request.user, application=application).first()
if not grant or grant.role not in PERMISSIONS:
return None
return {
"issuer": "django-codexpi",
"subject": str(request.user.pk),
"username": request.user.get_username(),
"application": application,
"role": grant.role,
"permissions": list(PERMISSIONS[grant.role]),
}
@require_GET
@identity_request
def application_identity(request, application):
identity = principal(request, application)
return reply(identity) if identity else reply({"detail": "Accès à cette application refusé"}, 403)
@require_POST
@identity_request
def authorize(request, application):
body = json_body(request)
if set(body) != {"permission"} or body["permission"] not in ("read", "write", "admin"):
return reply({"detail": "Permission invalide"}, 400)
identity = principal(request, application)
if not identity or body["permission"] not in identity["permissions"]:
return reply({"detail": "Permission refusée"}, 403)
return reply({**identity, "authorized_permission": body["permission"]})
@require_http_methods(["GET", "PUT", "DELETE"])
@identity_request
def application_grant(request, application, user_id):
if not (request.user.is_staff or request.user.is_superuser):
return reply({"detail": "Administration des identités requise"}, 403)
target = get_user_model().objects.filter(pk=user_id).first()
if not target:
return reply({"detail": "Compte introuvable"}, 404)
grants = ApplicationGrant.objects.filter(user=target, application=application)
if request.method == "GET":
grant = grants.first()
return reply({"subject": str(target.pk), "application": application, "role": grant.role if grant else None})
if request.method == "DELETE":
grants.delete()
role = None
else:
body = json_body(request)
if set(body) != {"role"} or body["role"] not in ROLES:
return reply({"detail": "Rôle invalide"}, 400)
role = body["role"]
ApplicationGrant.objects.update_or_create(user=target, application=application, defaults={"role": role})
AUDIT.log(
"identity.grant_revoked" if role is None else "identity.grant_updated",
client_ip(request),
username=request.user.get_username(),
project_id=application,
outcome="allowed",
reason=f"target_user={target.pk};role={role or 'none'}",
)
return reply({"subject": str(target.pk), "application": application, "role": role})