import logging import math import urllib.parse from djagger.decorators import schema from django.conf import settings from django.core.paginator import Paginator from django.http import ( HttpResponse, JsonResponse, ) from django.urls import reverse from django.views.decorators.csrf import csrf_exempt from spid_cie_oidc.authority.models import ( FederationDescendant, FederationEntityAssignedProfile ) from spid_cie_oidc.authority.settings import MAX_ENTRIES_PAGE from spid_cie_oidc.entity.jwtse import ( create_jws, unpad_jwt_head, unpad_jwt_payload, ) from spid_cie_oidc.entity.models import get_first_self_trust_anchor from spid_cie_oidc.entity.statements import get_trust_mark_type_id from spid_cie_oidc.entity.utils import iat_now from . schemas.fetch_endpoint_request import FetchRequest, FedAPIErrorResponse, FetchResponse from . schemas.list_endpoint import ListRequest, ListResponse from . schemas.advanced_entity_list_endpoint import AdvancedEntityListRequest, AdvancedEntityListResponse from . schemas.trust_mark_status_endpoint import ( TrustMarkRequest, TrustMarkResponse, ) logger = logging.getLogger(__name__) @schema( methods=['GET'], get_request_schema = { "application/x-www-form-urlencoded": FetchRequest }, get_response_schema = { "400": FedAPIErrorResponse, "404": FedAPIErrorResponse, "200": FetchResponse }, tags = ['Federation API'] ) def fetch(request): """ OpenID Federation 8.1: Fetch endpoint. Request: GET with sub REQUIRED (issuer implied by this endpoint's URL). """ iss = get_first_self_trust_anchor() if not request.GET.get("sub"): conf = get_first_self_trust_anchor() if request.GET.get("format") == "json": return JsonResponse(conf.entity_configuration_as_dict, safe=False) return HttpResponse( conf.entity_configuration_as_jws, content_type="application/entity-statement+jwt" ) sub = FederationDescendant.objects.filter( sub=request.GET["sub"], is_active=True ).first() if not sub: return JsonResponse( { "error": "invalid_subject", "error_description": "entity not found" }, status=404 ) if request.GET.get("format") == "json": return JsonResponse( sub.entity_statement_as_dict(iss.sub), safe=False ) return HttpResponse( sub.entity_statement_as_jws(iss.sub), content_type="application/entity-statement+jwt", ) @schema( methods=['GET'], get_request_schema = { "application/x-www-form-urlencoded": ListRequest }, get_response_schema = { "400": FedAPIErrorResponse, "404": FedAPIErrorResponse, "200": ListResponse }, tags = ['Federation API'] ) def entity_list(request): """ OpenID Federation 8.2: Subordinate Listing. Optional filters: entity_type, trust_marked, trust_mark_type, intermediate. """ _q = {"descendant__is_active": True} if request.GET.get("intermediate", "").lower() == "true": _q["profile__profile_category"] = "federation_entity" elif request.GET.get("entity_type"): _q["profile__profile_category"] = request.GET["entity_type"] if request.GET.get("trust_mark_type"): _q["profile__profile_id"] = request.GET["trust_mark_type"] entries = FederationEntityAssignedProfile.objects.filter(**_q).values_list( "descendant__sub", flat=True ) return JsonResponse(list(set(entries)), safe=False) # TODO - add the schema # @schema( # methods=['GET'], # get_request_schema = { # "application/x-www-form-urlencoded": ListRequest # }, # get_response_schema = { # "400": FedAPIErrorResponse, # "404": FedAPIErrorResponse, # "200": ListResponse # }, # tags = ['Federation API'] # ) def trust_marked_list(request): if request.GET.get("trust_mark_id", "").lower(): _q = {"profile__profile_id": request.GET["trust_mark_id"]} else: _q = {} entries = FederationEntityAssignedProfile.objects.filter(**_q).values_list( "descendant__sub", flat=True ) return JsonResponse(list(set(entries)), safe=False) @schema( methods=['GET'], get_request_schema = { "application/x-www-form-urlencoded": AdvancedEntityListRequest }, get_response_schema = { "400": FedAPIErrorResponse, "404": FedAPIErrorResponse, "200": AdvancedEntityListResponse }, tags = ['Federation API'] ) def advanced_entity_listing(request): desecendants = FederationDescendant.objects.filter( is_active = True, ).order_by("-modified") entities_list = [] for descendant in desecendants: try: _ss = descendant.entity_statement_as_jws() except AttributeError as e: logger.warning( f"Subordinate {descendant} missing authority hint: {e}" ) continue entity = { descendant.sub : { "iat" : int(descendant.modified.timestamp()), "subordinate_statement": _ss } } entities_list.append(entity) total_entries = desecendants.count() _max_entries = getattr(settings, 'MAX_ENTRIES_PAGE', MAX_ENTRIES_PAGE) p = Paginator(entities_list, _max_entries) page = request.GET.get("page", 1) entities = p.get_page(page) next_page_path = "" if entities.has_next(): param = {"page": entities.next_page_number()} url = f'{reverse("oidcfed_advanced_entity_listing")}?{urllib.parse.urlencode(param)}' next_page_path = f"{url}" prev_page_path = "" if entities.has_previous(): param = {"page": entities.previous_page_number()} url = f'{reverse("oidcfed_advanced_entity_listing")}?{urllib.parse.urlencode(param)}' prev_page_path = f"{url}" try: iss = get_first_self_trust_anchor().sub except Exception: return JsonResponse( { "error": "Missing trust anchor", }, status = 404 ) res = { "iss" : iss, "iat" : iat_now(), "entities" : entities_list, "page" : int(page), "total_pages" : math.ceil(total_entries / MAX_ENTRIES_PAGE), "total_entries" : total_entries, "next_page_path": next_page_path, "prev_page_path": prev_page_path, } return JsonResponse(res, safe=False) @schema( methods=['GET', 'POST'], get_request_schema={ "application/x-www-form-urlencoded": TrustMarkRequest }, get_response_schema={ "400": FedAPIErrorResponse, "404": FedAPIErrorResponse, "200": TrustMarkResponse }, tags=['Federation API'] ) @csrf_exempt def trust_mark_status(request): """ OpenID Federation 8.4: Trust Mark Status endpoint. Request: trust_mark (JWT) REQUIRED [draft 48], or legacy: sub + trust_mark_id/trust_mark_type/id. Response: 200 application/trust-mark-status-response+jwt (signed JWT), or 404 not_found. """ sub = request.POST.get("sub") or request.GET.get("sub", None) _id = ( request.POST.get("trust_mark_type") or request.GET.get("trust_mark_type") or request.POST.get("trust_mark_id") or request.GET.get("trust_mark_id") or request.POST.get("id") or request.GET.get("id") ) trust_mark_jwt = request.POST.get("trust_mark") or request.GET.get("trust_mark", None) if request.method not in ("GET", "POST"): return JsonResponse( {"error": "invalid_request", "error_description": "Method not allowed"}, status=400 ) if trust_mark_jwt: try: unpad_jwt_head(trust_mark_jwt) payload = unpad_jwt_payload(trust_mark_jwt) sub = payload.get("sub") _id = get_trust_mark_type_id(payload) except Exception: return JsonResponse( {"error": "invalid_request", "error_description": "Invalid trust_mark JWT"}, status=400 ) elif not (sub and _id): return JsonResponse( {"error": "invalid_request", "error_description": "trust_mark REQUIRED or sub + trust_mark_type/id"}, status=400 ) assigned = FederationEntityAssignedProfile.objects.filter( descendant__sub=sub, profile__profile_id=_id, descendant__is_active=True, ).select_related("issuer").first() if not assigned: return JsonResponse( {"error": "not_found", "error_description": "Trust Mark not found or not active"}, status=404 ) # Draft 48: response is signed JWT with iss, iat, trust_mark, status tm_jwt_for_response = trust_mark_jwt or assigned.trust_mark.get("trust_mark") or assigned.trust_mark_as_jws issuer = assigned.issuer jwk = issuer.jwks_fed[0] status_payload = { "iss": issuer.sub, "iat": iat_now(), "trust_mark": tm_jwt_for_response, "status": "active", } signed = create_jws( status_payload, jwk, alg=issuer.default_signature_alg, protected={"typ": "trust-mark-status-response+jwt", "kid": jwk.get("kid")}, ) return HttpResponse( signed, content_type="application/trust-mark-status-response+jwt", status=200, )