354 lines
11 KiB
Python
354 lines
11 KiB
Python
from copy import deepcopy
|
|
from cryptojwt.jwk.jwk import key_from_jwk_dict
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import models
|
|
|
|
# from django.db.models.signals import post_save
|
|
from django.utils.translation import gettext as _
|
|
|
|
from spid_cie_oidc.entity.abstract_models import TimeStampedModel
|
|
from spid_cie_oidc.entity.models import (
|
|
ENTITY_TYPES,
|
|
ENTITY_STATUS,
|
|
FederationEntityConfiguration
|
|
)
|
|
|
|
from spid_cie_oidc.entity.jwtse import create_jws
|
|
from spid_cie_oidc.entity.jwk_x5c import update_jwks_with_x5c
|
|
from spid_cie_oidc.entity.validators import validate_public_jwks
|
|
from spid_cie_oidc.entity.utils import iat_now, exp_from_now
|
|
from spid_cie_oidc.entity.settings import FEDERATION_DEFAULT_EXP
|
|
from spid_cie_oidc.entity.models import get_first_self_trust_anchor
|
|
|
|
from . settings import FEDERATION_DEFAULT_POLICY
|
|
from .validators import validate_entity_configuration
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FederationEntityProfile(TimeStampedModel):
|
|
"""
|
|
Federation OnBoarding profile.
|
|
It optionally defines trust marks templates
|
|
"""
|
|
|
|
name = models.CharField(max_length=33, help_text=_("Profile name. "))
|
|
profile_category = models.CharField(
|
|
max_length=64,
|
|
help_text=_("Profile id. It SHOULD be a URL but feel free to put whatever"),
|
|
choices=[(i, i) for i in ENTITY_TYPES],
|
|
)
|
|
profile_id = models.CharField(
|
|
max_length=1024,
|
|
help_text=_("Profile id. It SHOULD be a URL but feel free to put whatever"),
|
|
unique=True,
|
|
)
|
|
trust_mark_template = models.JSONField(
|
|
help_text=_("trust marks template for this profile"), default=dict
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Federation Entity Profile"
|
|
verbose_name_plural = "Federation Entity Profiles"
|
|
|
|
@property
|
|
def trust_mark_template_as_json(self):
|
|
return json.dumps(self.trust_mark_template)
|
|
|
|
def __str__(self):
|
|
return f"{self.name} {self.profile_id}"
|
|
|
|
|
|
class FederationDescendant(TimeStampedModel):
|
|
"""
|
|
Federation OnBoarding entries.
|
|
"""
|
|
|
|
def def_uid():
|
|
return f"autouid-{uuid.uuid4()}"
|
|
|
|
uid = models.CharField(
|
|
max_length=1024,
|
|
default=def_uid,
|
|
unique=True,
|
|
help_text=_(
|
|
"an unique code that identifies this entry. "
|
|
"For italian public service it may be the IPA code."
|
|
),
|
|
)
|
|
name = models.CharField(
|
|
max_length=33,
|
|
help_text=_(
|
|
"human readable name of this entity. "
|
|
"It may be a unit or organization name"
|
|
),
|
|
)
|
|
sub = models.URLField(
|
|
max_length=255,
|
|
blank=False,
|
|
null=False,
|
|
unique=True,
|
|
help_text=_("URL that identifies this Entity in the Federation."),
|
|
validators=[validate_entity_configuration],
|
|
)
|
|
type = models.CharField(
|
|
max_length=33,
|
|
blank=True,
|
|
default="openid_relying_party",
|
|
choices=[(i, i) for i in ENTITY_TYPES],
|
|
help_text=_("OpenID Connect Federation entity type"),
|
|
)
|
|
registrant = models.ManyToManyField(
|
|
get_user_model(),
|
|
help_text=_(
|
|
"Logged in users can modify only sub, contacts and jwks "
|
|
"attributes, if they're owner of this entry. "
|
|
),
|
|
blank=True,
|
|
)
|
|
jwks = models.JSONField(
|
|
blank=False,
|
|
null=False,
|
|
help_text=_("a list of public keys"),
|
|
default=list,
|
|
validators = [validate_public_jwks]
|
|
)
|
|
metadata_policy = models.JSONField(
|
|
blank=True, help_text=_(
|
|
"if present it overloads the DEFAULT policy. This will be applied "
|
|
"recursively to all the descendants."
|
|
), default=dict
|
|
)
|
|
metadata = models.JSONField(
|
|
blank=True, help_text=_(
|
|
"if present it overloads the descendants metadata, "
|
|
"in the defined parts. This will be applied to the subordinate only."
|
|
), default=dict
|
|
)
|
|
constraints = models.JSONField(
|
|
blank=True, help_text=_("if present overloads the DEFAULT policy"), default=dict
|
|
)
|
|
extended_claims = models.JSONField(
|
|
blank=True,
|
|
help_text=_(
|
|
"a dictionary containing any other claim like: "
|
|
"jti, crti, policy_language_crit and any other extension"
|
|
),
|
|
default=dict,
|
|
)
|
|
status = models.CharField(
|
|
max_length=33,
|
|
default=False,
|
|
help_text=_("Its entity configuration is periodically fetched and validated."),
|
|
choices=[(i, i) for i in ENTITY_STATUS.keys()],
|
|
)
|
|
status_log = models.JSONField(blank=True, help_text=_("status log"), default=dict)
|
|
|
|
is_active = models.BooleanField(
|
|
default=False, help_text=_("If this entity is active. ")
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Federation Entity Descendant"
|
|
verbose_name_plural = "Federation Entity Descendants"
|
|
|
|
@property
|
|
def trust_marks(self):
|
|
#
|
|
profiles = FederationEntityAssignedProfile.objects.filter(descendant=self)
|
|
return [i.trust_mark for i in profiles]
|
|
|
|
@property
|
|
def trust_marks_as_json(self):
|
|
return json.dumps(self.trust_marks)
|
|
|
|
@property
|
|
def entity_profiles(self):
|
|
return [
|
|
i.profile.profile_category
|
|
for i in FederationEntityAssignedProfile.objects.filter(descendant=self)
|
|
]
|
|
|
|
def entity_statement_as_dict(self, iss: str = None, aud: list = None) -> dict:
|
|
|
|
ta = get_first_self_trust_anchor(iss)
|
|
private_key = key_from_jwk_dict(ta.jwks_fed[0])
|
|
|
|
if hasattr(settings, "X509_COMMON_NAME"):
|
|
self.jwks = update_jwks_with_x5c(
|
|
jwks = self.jwks,
|
|
private_key=private_key.private_key(),
|
|
subject = self.sub,
|
|
path_length = None,
|
|
is_ca_or_int = True
|
|
)
|
|
|
|
policies = {
|
|
k: FEDERATION_DEFAULT_POLICY.get(k, {}) for k in self.entity_profiles
|
|
}
|
|
|
|
# apply custom policies if defined
|
|
policies.update(self.metadata_policy)
|
|
|
|
data = {
|
|
"exp": exp_from_now(minutes=FEDERATION_DEFAULT_EXP),
|
|
"iat": iat_now(),
|
|
"iss": ta.sub,
|
|
"sub": self.sub,
|
|
"jwks": {"keys": self.jwks}
|
|
}
|
|
|
|
if policies:
|
|
data["metadata_policy"] = policies
|
|
|
|
# OpenID Federation 8.1.2: source_endpoint enables optimized refresh
|
|
if ta.fetch_endpoint:
|
|
data["source_endpoint"] = ta.fetch_endpoint
|
|
|
|
if aud:
|
|
data["aud"] = [aud] if isinstance(aud, str) else aud
|
|
|
|
# add contacts
|
|
contacts = FederationDescendantContact.objects.filter(entity=self).values_list(
|
|
"contact", flat=True
|
|
)
|
|
|
|
if contacts:
|
|
for k, v in data.get("metadata_policy", {}).items():
|
|
if data["metadata_policy"][k].get("contacts"):
|
|
data["metadata_policy"][k]["contacts"].update(
|
|
{"add": [i for i in contacts]}
|
|
)
|
|
else:
|
|
data["metadata_policy"][k]["contacts"] = {
|
|
"add": [i for i in contacts]
|
|
}
|
|
|
|
# include active trust marks
|
|
tm = self.trust_marks
|
|
if tm:
|
|
data["trust_marks"] = tm
|
|
|
|
return data
|
|
|
|
def entity_statement_as_json(self, iss: str = None, aud: list = None) -> str:
|
|
return json.dumps(self.entity_statement_as_dict(iss, aud))
|
|
|
|
def entity_statement_preview(self):
|
|
return self.entity_statement_as_json()
|
|
|
|
def entity_statement_as_jws(self, iss: str = None, aud: list = None) -> str:
|
|
issuer = get_first_self_trust_anchor(iss)
|
|
return create_jws(
|
|
self.entity_statement_as_dict(iss, aud),
|
|
issuer.jwks_fed[0],
|
|
alg=issuer.default_signature_alg,
|
|
typ="entity-statement+jwt"
|
|
)
|
|
|
|
def __str__(self):
|
|
return "{} [{} and {}]".format(
|
|
self.sub, self.status, "active" if self.is_active else "--"
|
|
)
|
|
|
|
|
|
class FederationEntityAssignedProfile(TimeStampedModel):
|
|
descendant = models.ForeignKey(FederationDescendant, on_delete=models.CASCADE)
|
|
profile = models.ForeignKey(FederationEntityProfile, on_delete=models.CASCADE)
|
|
issuer = models.ForeignKey(FederationEntityConfiguration, on_delete=models.CASCADE)
|
|
|
|
class Meta:
|
|
verbose_name = "Federation Entity Descendant Assigned Profile"
|
|
verbose_name_plural = "Federation Descendant Assigned Profiles"
|
|
|
|
@property
|
|
def trust_mark_as_dict(self) -> dict:
|
|
data = deepcopy(self.profile.trust_mark_template)
|
|
data["sub"] = self.descendant.sub
|
|
data["iss"] = self.issuer.sub
|
|
data["iat"] = iat_now()
|
|
# Draft 48: trust_mark_type; retrocompat: also set trust_mark_id
|
|
data["trust_mark_type"] = self.profile.profile_id
|
|
data.setdefault("trust_mark_id", self.profile.profile_id)
|
|
return data
|
|
|
|
@property
|
|
def trust_mark_as_json(self):
|
|
return json.dumps(self.trust_mark_as_dict)
|
|
|
|
@property
|
|
def trust_mark_as_jws(self):
|
|
return create_jws(
|
|
self.trust_mark_as_dict,
|
|
self.issuer.jwks_fed[0],
|
|
alg=self.issuer.default_signature_alg,
|
|
typ="trust-mark+jwt"
|
|
)
|
|
|
|
@property
|
|
def trust_mark(self):
|
|
# Draft 48: trust_mark_type; retrocompat: also trust_mark_id and id
|
|
return {
|
|
"trust_mark_type": self.profile.profile_id,
|
|
"trust_mark": self.trust_mark_as_jws,
|
|
"trust_mark_id": self.profile.profile_id,
|
|
"id": self.profile.profile_id,
|
|
}
|
|
|
|
def __str__(self):
|
|
return f"{self.profile} [{self.descendant}]"
|
|
|
|
|
|
class FederationDescendantContact(TimeStampedModel):
|
|
"""
|
|
Federation OnBoarding entries.
|
|
"""
|
|
|
|
entity = models.ForeignKey(
|
|
FederationDescendant,
|
|
on_delete=models.CASCADE,
|
|
help_text=_("Entity for which this contac is related"),
|
|
)
|
|
contact = models.CharField(
|
|
max_length=255,
|
|
blank=False,
|
|
null=False,
|
|
help_text=_("any kind of contact type, usually an email."),
|
|
)
|
|
type = models.CharField(
|
|
max_length=255,
|
|
blank=False,
|
|
null=False,
|
|
choices=[(i, i) for i in ("email", "telephone", "other")],
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Federation Entity Contact"
|
|
verbose_name_plural = "Federation Entity Contacts"
|
|
|
|
def __str__(self):
|
|
return f"{self.contact} {self.entity.sub}"
|
|
|
|
|
|
# signal on each save
|
|
# def trust_chain_trigger(**kwargs):
|
|
# subject = kwargs['instance'].sub
|
|
|
|
# onboarding uses the first available configuration of federation_entity
|
|
# fe = FederationEntityConfiguration.objects.filter(
|
|
# is_active=True,
|
|
# ).first()
|
|
|
|
# logger.info(
|
|
# f"Receiving trust chain evaluation signal for {subject}"
|
|
# )
|
|
# return trust_chain_builder(subject)
|
|
# post_save.connect(trust_chain_trigger, sender=FederationDescendant)
|
|
#
|