feat: link local library
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
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)
|
||||
#
|
||||
Reference in New Issue
Block a user