feat: link local library
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
FederationDescendant,
|
||||
FederationDescendantContact,
|
||||
FederationEntityProfile,
|
||||
FederationEntityAssignedProfile
|
||||
)
|
||||
|
||||
|
||||
class FederationDescendantContactAdminInline(admin.TabularInline):
|
||||
model = FederationDescendantContact
|
||||
extra = 0
|
||||
readonly_fields = ("created", "modified")
|
||||
raw_id_fields = ("entity",)
|
||||
|
||||
|
||||
@admin.register(FederationDescendant)
|
||||
class FederationDescendantAdmin(admin.ModelAdmin):
|
||||
list_display = ("sub", "name", "type", "status", "is_active", "created")
|
||||
list_filter = ("type", "created", "modified", "is_active")
|
||||
search_fields = ("sub",)
|
||||
readonly_fields = (
|
||||
"created",
|
||||
"modified",
|
||||
"entity_statement_preview",
|
||||
)
|
||||
inlines = (
|
||||
FederationDescendantContactAdminInline,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(FederationEntityProfile)
|
||||
class FederationEntityProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "profile_id")
|
||||
list_filter = ("created", "modified")
|
||||
search_fields = ("name", "profile_id",)
|
||||
readonly_fields = ("trust_mark_template_as_json",)
|
||||
|
||||
|
||||
@admin.register(FederationEntityAssignedProfile)
|
||||
class FederationEntityAssignedProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ("descendant", "profile", "issuer", "created")
|
||||
raw_id_fields = ("descendant", "profile", "issuer")
|
||||
list_filter = ("created", "modified")
|
||||
search_fields = ("descendant__sub", "descendant__name", "profile")
|
||||
readonly_fields = ("trust_mark_as_json", "trust_mark", "created", "modified")
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthorityConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "spid_cie_oidc.authority"
|
||||
verbose_name = "SPID/CIE OIDC Federation Authority"
|
||||
label = "spid_cie_oidc_authority"
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-13 09:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import spid_cie_oidc.authority.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FederationDescendant",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("modified", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"uid",
|
||||
models.CharField(
|
||||
default=spid_cie_oidc.authority.models.FederationDescendant.def_uid,
|
||||
help_text="an unique code that identifies this entry. For italian public service it may be the IPA code.",
|
||||
max_length=1024,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="human readable name of this entity. It may be a unit or organization name",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"sub",
|
||||
models.URLField(
|
||||
help_text="URL that identifies this Entity in the Federation.",
|
||||
max_length=255,
|
||||
unique=True,
|
||||
validators=[
|
||||
spid_cie_oidc.authority.validators.validate_entity_configuration
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federation_entity", "federation_entity"),
|
||||
("openid_relying_party", "openid_relying_party"),
|
||||
("openid_provider", "openid_provider"),
|
||||
("oauth_resource", "oauth_resource"),
|
||||
],
|
||||
default="openid_relying_party",
|
||||
help_text="OpenID Connect Federation entity type",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"metadata_policy",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="if present overloads the DEFAULT policy",
|
||||
),
|
||||
),
|
||||
(
|
||||
"constraints",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="if present overloads the DEFAULT policy",
|
||||
),
|
||||
),
|
||||
(
|
||||
"extended_claims",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="a dictionary containing any other claim like: jti, crti, policy_language_crit and any other extension",
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("unreachable", "unreachable"),
|
||||
("valid", "valid"),
|
||||
("signature_failed", "signature_failed"),
|
||||
("not_valid", "not_valid"),
|
||||
("unknown", "unknown"),
|
||||
("expired", "expired"),
|
||||
],
|
||||
default=False,
|
||||
help_text="Its entity configuration is periodically fetched and validated.",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status_log",
|
||||
models.JSONField(blank=True, default=dict, help_text="status log"),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=False, help_text="If this entity is active. "
|
||||
),
|
||||
),
|
||||
(
|
||||
"registrant",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Logged in users can modify only sub, contacts and jwks attributes, if they're owner of this entry. ",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Entity Descendant",
|
||||
"verbose_name_plural": "Federation Entity Descendants",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="FederationEntityProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("modified", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(help_text="Profile name. ", max_length=33)),
|
||||
(
|
||||
"profile_category",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("federation_entity", "federation_entity"),
|
||||
("openid_relying_party", "openid_relying_party"),
|
||||
("openid_provider", "openid_provider"),
|
||||
("oauth_resource", "oauth_resource"),
|
||||
],
|
||||
help_text="Profile id. It SHOULD be a URL but feel free to put whatever",
|
||||
max_length=64,
|
||||
),
|
||||
),
|
||||
(
|
||||
"profile_id",
|
||||
models.CharField(
|
||||
help_text="Profile id. It SHOULD be a URL but feel free to put whatever",
|
||||
max_length=1024,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"trust_mark_template",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="trust marks template for this profile",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Entity Profile",
|
||||
"verbose_name_plural": "Federation Entity Profiles",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="FederationEntityAssignedProfile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("modified", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"descendant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationdescendant",
|
||||
),
|
||||
),
|
||||
(
|
||||
"issuer",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_entity.federationentityconfiguration",
|
||||
),
|
||||
),
|
||||
(
|
||||
"profile",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationentityprofile",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Entity Descendant Assigned Profile",
|
||||
"verbose_name_plural": "Federation Descendant Assigned Profiles",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="FederationDescendantJwk",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("modified", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"descendant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationdescendant",
|
||||
),
|
||||
),
|
||||
(
|
||||
"jwk",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_entity.publicjwk",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Descendant Public JWK",
|
||||
"verbose_name_plural": "Federation Descendant Public JWKs",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="FederationDescendantContact",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("modified", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"contact",
|
||||
models.CharField(
|
||||
help_text="any kind of contact type, usually an email.",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("email", "email"),
|
||||
("telephone", "telephone"),
|
||||
("other", "other"),
|
||||
],
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"entity",
|
||||
models.ForeignKey(
|
||||
help_text="Entity for which this contac is related",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationdescendant",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Entity Contact",
|
||||
"verbose_name_plural": "Federation Entity Contacts",
|
||||
},
|
||||
),
|
||||
]
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-14 22:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_authority", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationentityprofile",
|
||||
name="trust_mark_template",
|
||||
field=models.JSONField(
|
||||
default=dict, help_text="trust marks template for this profile"
|
||||
),
|
||||
),
|
||||
]
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-20 22:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_authority",
|
||||
"0002_alter_federationentityprofile_trust_mark_template",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationdescendantcontact",
|
||||
name="entity",
|
||||
field=models.ForeignKey(
|
||||
help_text="Entity for which this contac is related",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationdescendant",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationdescendantjwk",
|
||||
name="descendant",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationdescendant",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityassignedprofile",
|
||||
name="descendant",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationdescendant",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityassignedprofile",
|
||||
name="profile",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_authority.federationentityprofile",
|
||||
),
|
||||
),
|
||||
]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-06 22:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_authority', '0003_alter_federationdescendantcontact_entity_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='federationdescendant',
|
||||
name='jwks',
|
||||
field=models.JSONField(default=list, help_text='a list of public keys'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='FederationDescendantJwk',
|
||||
),
|
||||
]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-06 23:11
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.entity.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_authority', '0004_federationdescendant_jwks_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='federationdescendant',
|
||||
name='jwks',
|
||||
field=models.JSONField(default=list, help_text='a list of public keys', validators=[spid_cie_oidc.entity.validators.validate_public_jwks]),
|
||||
),
|
||||
]
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.0.3 on 2022-03-20 10:50
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import spid_cie_oidc.authority.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('spid_cie_oidc_authority', '0005_alter_federationdescendant_jwks'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StaffToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('modified', models.DateTimeField(auto_now=True)),
|
||||
('token', models.CharField(default=spid_cie_oidc.authority.utils.random_token, help_text='any kind of contact type, usually an email.', max_length=255)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('user', models.ForeignKey(help_text='The user responsible of thi token', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Staff Token',
|
||||
'verbose_name_plural': 'Staff Tokens',
|
||||
},
|
||||
),
|
||||
]
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.3 on 2022-03-20 10:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_authority', '0006_stafftoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stafftoken',
|
||||
name='expire_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-28 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.authority.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_authority', '0007_stafftoken_expire_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stafftoken',
|
||||
name='token',
|
||||
field=models.CharField(default=spid_cie_oidc.authority.utils.random_token, help_text='it will be generated automatically.', max_length=255),
|
||||
),
|
||||
]
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 4.0.5 on 2022-10-18 09:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_authority', '0008_alter_stafftoken_token'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='StaffToken',
|
||||
),
|
||||
]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-09 15:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_authority", "0009_delete_stafftoken"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="federationdescendant",
|
||||
name="metadata",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="if present it overloads the descendants metadata, in the defined parts. This will be applied to the subordinate only.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationdescendant",
|
||||
name="metadata_policy",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="if present it overloads the DEFAULT policy. This will be applied recursively to all the descendants.",
|
||||
),
|
||||
),
|
||||
]
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-09 15:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_authority", "0010_federationdescendant_metadata_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="federationdescendant",
|
||||
name="issue_x509",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Issues x509 certificates in the entity statement.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationdescendant",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federation_entity", "federation_entity"),
|
||||
("openid_relying_party", "openid_relying_party"),
|
||||
("openid_provider", "openid_provider"),
|
||||
("oauth_resource", "oauth_resource"),
|
||||
("wallet_provider", "wallet_provider"),
|
||||
("wallet_relying_party", "wallet_relying_party"),
|
||||
],
|
||||
default="openid_relying_party",
|
||||
help_text="OpenID Connect Federation entity type",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityprofile",
|
||||
name="profile_category",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("federation_entity", "federation_entity"),
|
||||
("openid_relying_party", "openid_relying_party"),
|
||||
("openid_provider", "openid_provider"),
|
||||
("oauth_resource", "oauth_resource"),
|
||||
("wallet_provider", "wallet_provider"),
|
||||
("wallet_relying_party", "wallet_relying_party"),
|
||||
],
|
||||
help_text="Profile id. It SHOULD be a URL but feel free to put whatever",
|
||||
max_length=64,
|
||||
),
|
||||
),
|
||||
]
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-13 04:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_authority", "0011_federationdescendant_issue_x509_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="federationdescendant",
|
||||
name="issue_x509",
|
||||
),
|
||||
]
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.7 on 2023-11-14 14:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_authority", "0012_remove_federationdescendant_issue_x509"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationdescendant",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federation_entity", "federation_entity"),
|
||||
("openid_relying_party", "openid_relying_party"),
|
||||
("openid_provider", "openid_provider"),
|
||||
("openid_credential_issuer", "openid_credential_issuer"),
|
||||
("oauth_resource", "oauth_resource"),
|
||||
("wallet_provider", "wallet_provider"),
|
||||
("wallet_relying_party", "wallet_relying_party"),
|
||||
],
|
||||
default="openid_relying_party",
|
||||
help_text="OpenID Connect Federation entity type",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityprofile",
|
||||
name="profile_category",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("federation_entity", "federation_entity"),
|
||||
("openid_relying_party", "openid_relying_party"),
|
||||
("openid_provider", "openid_provider"),
|
||||
("openid_credential_issuer", "openid_credential_issuer"),
|
||||
("oauth_resource", "oauth_resource"),
|
||||
("wallet_provider", "wallet_provider"),
|
||||
("wallet_relying_party", "wallet_relying_party"),
|
||||
],
|
||||
help_text="Profile id. It SHOULD be a URL but feel free to put whatever",
|
||||
max_length=64,
|
||||
),
|
||||
),
|
||||
]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.0.5 on 2026-03-23 15:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_authority', '0013_alter_federationdescendant_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='federationdescendant',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], default='openid_relying_party', help_text='OpenID Connect Federation entity type', max_length=33),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='federationentityprofile',
|
||||
name='profile_category',
|
||||
field=models.CharField(choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], help_text='Profile id. It SHOULD be a URL but feel free to put whatever', max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
#
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, HttpUrl, Json
|
||||
|
||||
|
||||
class AdvancedEntityListRequest(BaseModel):
|
||||
page : Optional[int]
|
||||
|
||||
def example(): # pragma: no cover
|
||||
return AdvancedEntityListRequest(
|
||||
page= 1,
|
||||
)
|
||||
|
||||
|
||||
class AdvancedEntityListResponse(BaseModel):
|
||||
iss: HttpUrl
|
||||
iat: int
|
||||
entities: List[Json]
|
||||
page: int
|
||||
total_pages: int
|
||||
total_entries: int
|
||||
next_page_path: str
|
||||
prev_page_path: str
|
||||
|
||||
def example(): # pragma: no cover
|
||||
return AdvancedEntityListResponse(
|
||||
iss= "https://registry.spid.gov.it/",
|
||||
iat= 1620050972,
|
||||
entities= [
|
||||
# "{'https://rp.example.it/spid': {'iat': 1588455866} }",
|
||||
# "{'https://rp.another.it/spid': {'iat': 1588455866} }"
|
||||
],
|
||||
page= 1,
|
||||
total_pages= 2,
|
||||
total_entries= 189,
|
||||
next_page_path= "/federation_adv_list/?page=2",
|
||||
prev_page_path= ""
|
||||
)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, HttpUrl, constr
|
||||
|
||||
|
||||
class FetchRequest(BaseModel):
|
||||
# OpenID Federation 8.1.1: sub REQUIRED; issuer implied by fetch endpoint URL
|
||||
sub: HttpUrl
|
||||
|
||||
def example(): # pragma: no cover
|
||||
return FetchRequest(sub="http://127.0.0.1:8000/oidc/rp")
|
||||
|
||||
|
||||
class FetchResponse(BaseModel):
|
||||
jose : constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722
|
||||
|
||||
|
||||
class FedAPIErrorResponse(BaseModel):
|
||||
operation: Optional[str]
|
||||
error: Literal[
|
||||
"invalid_request",
|
||||
"invalid_client",
|
||||
"invalid_issuer",
|
||||
"invalid_subject",
|
||||
"invalid_trust_anchor",
|
||||
"invalid_trust_chain",
|
||||
"invalid_metadata",
|
||||
"not_found",
|
||||
"server_error",
|
||||
"temporarily_unavailable",
|
||||
"unsupported_parameter",
|
||||
]
|
||||
error_description: str
|
||||
@@ -0,0 +1,20 @@
|
||||
from typing import List, Literal, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
|
||||
class ListRequest(BaseModel):
|
||||
# OpenID Federation 8.2.1: optional filters
|
||||
entity_type: Optional[
|
||||
Literal["openid_relying_party", "openid_provider", "oauth_resource", "federation_entity"]
|
||||
] = None
|
||||
trust_marked: Optional[bool] = None
|
||||
trust_mark_type: Optional[Union[HttpUrl, str]] = None
|
||||
intermediate: Optional[bool] = None
|
||||
|
||||
def example(): # pragma: no cover
|
||||
return ListRequest(entity_type="openid_provider")
|
||||
|
||||
|
||||
class ListResponse(BaseModel):
|
||||
response: List[HttpUrl]
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, HttpUrl, constr
|
||||
|
||||
# JWT pattern (three base64url segments) to avoid flake8 F722 on constr(regex=...)
|
||||
_JWT_PATTERN = r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+"
|
||||
|
||||
|
||||
class TrustMarkRequest(BaseModel):
|
||||
# Draft 48: trust_mark (JWT) REQUIRED for new flow
|
||||
trust_mark: Optional[constr(regex=_JWT_PATTERN)]
|
||||
# Retrocompat: sub + trust_mark_id / trust_mark_type / id
|
||||
sub: Optional[HttpUrl]
|
||||
trust_mark_id: Optional[HttpUrl]
|
||||
trust_mark_type: Optional[HttpUrl]
|
||||
id: Optional[HttpUrl]
|
||||
|
||||
def example(): # pragma: no cover
|
||||
return TrustMarkRequest(
|
||||
trust_mark_type="https://www.spid.gov.it/openid-federation/agreement/op-public/",
|
||||
sub="http://127.0.0.1:8000/oidc/op",
|
||||
)
|
||||
|
||||
|
||||
class TrustMarkResponse(BaseModel):
|
||||
active: bool
|
||||
|
||||
|
||||
# OpenID Federation 8.4.2: Trust Mark Status Response JWT claims
|
||||
class TrustMarkStatusResponsePayload(BaseModel):
|
||||
iss: str # Entity Identifier of the issuer (Trust Mark Issuer)
|
||||
iat: int
|
||||
trust_mark: str # The Trust Mark JWT this status is about
|
||||
status: Literal["active", "expired", "revoked", "invalid"]
|
||||
@@ -0,0 +1,59 @@
|
||||
from django.conf import settings
|
||||
from spid_cie_oidc.entity import settings as entity_settings
|
||||
|
||||
MAX_ENTRIES_PAGE = getattr(settings, "MAX_ENTRIES_PAGE", 100)
|
||||
DEFAULT_JWS_ALG = getattr(settings, "DEFAULT_JWS_ALG", entity_settings.DEFAULT_JWS_ALG)
|
||||
DEFAULT_JWE_ALG = getattr(settings, "DEFAULT_JWE_ALG", entity_settings.DEFAULT_JWE_ALG)
|
||||
DEFAULT_JWE_ENC = getattr(settings, "DEFAULT_JWE_ENC", entity_settings.DEFAULT_JWE_ENC)
|
||||
SIGNING_ALG_VALUES_SUPPORTED = getattr(
|
||||
settings,
|
||||
"SIGNING_ALG_VALUES_SUPPORTED",
|
||||
entity_settings.SIGNING_ALG_VALUES_SUPPORTED,
|
||||
)
|
||||
ENCRYPTION_ALG_VALUES_SUPPORTED = getattr(
|
||||
settings,
|
||||
"ENCRYPTION_ALG_VALUES_SUPPORTED",
|
||||
entity_settings.ENCRYPTION_ALG_VALUES_SUPPORTED,
|
||||
)
|
||||
ENCRYPTION_ENC_SUPPORTED = getattr(
|
||||
settings, "ENCRYPTION_ENC_SUPPORTED", entity_settings.ENCRYPTION_ENC_SUPPORTED
|
||||
)
|
||||
|
||||
FEDERATION_DEFAULT_POLICY = getattr(
|
||||
settings, "FEDERATION_DEFAULT_POLICY", {
|
||||
"openid_provider": {
|
||||
"subject_types_supported": {"value": ["pairwise"]},
|
||||
"id_token_signing_alg_values_supported": {
|
||||
"subset_of": SIGNING_ALG_VALUES_SUPPORTED
|
||||
},
|
||||
"userinfo_signing_alg_values_supported": {
|
||||
"subset_of": SIGNING_ALG_VALUES_SUPPORTED
|
||||
},
|
||||
"token_endpoint_auth_methods_supported": {"value": ["private_key_jwt"]},
|
||||
"userinfo_encryption_alg_values_supported": {
|
||||
"subset_of": ENCRYPTION_ALG_VALUES_SUPPORTED
|
||||
},
|
||||
"userinfo_encryption_enc_values_supported": {
|
||||
"subset_of": ENCRYPTION_ENC_SUPPORTED
|
||||
},
|
||||
"request_object_encryption_alg_values_supported": {
|
||||
"subset_of": ENCRYPTION_ALG_VALUES_SUPPORTED
|
||||
},
|
||||
"request_object_encryption_enc_values_supported": {
|
||||
"subset_of": ENCRYPTION_ENC_SUPPORTED
|
||||
},
|
||||
"request_object_signing_alg_values_supported": {
|
||||
"subset_of": SIGNING_ALG_VALUES_SUPPORTED
|
||||
},
|
||||
},
|
||||
"openid_relying_party": {
|
||||
"grant_types": {"subset_of": ["authorization_code", "refresh_token"]},
|
||||
"scope": {
|
||||
"superset_of": ["openid"],
|
||||
"subset_of": ["openid", "offline_access"],
|
||||
},
|
||||
},
|
||||
"federation_entity": {},
|
||||
"oauth_resource": {},
|
||||
}
|
||||
)
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
|
||||
from spid_cie_oidc.authority.models import FederationDescendant
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
from spid_cie_oidc.entity.jwtse import unpad_jwt_payload, create_jws
|
||||
from spid_cie_oidc.entity.exceptions import HttpError
|
||||
from spid_cie_oidc.entity.tests.settings import ta_conf_data
|
||||
|
||||
from .settings import rp_onboarding_data, intermediary_conf, rp_conf
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DummyContent:
|
||||
def __init__(self, content):
|
||||
self.content = content.encode()
|
||||
self.status_code = 200
|
||||
|
||||
|
||||
class EntityResponse:
|
||||
def __init__(self):
|
||||
self.status_code = 200
|
||||
self.req_counter = 0
|
||||
self.client = Client()
|
||||
self.result = None
|
||||
|
||||
def result_as_it_is(self):
|
||||
logger.info(f"Response #{self.req_counter}: {self.result.content.decode()}")
|
||||
self.req_counter += 1
|
||||
return self.result.content
|
||||
|
||||
def result_as_jws(self):
|
||||
logger.info(
|
||||
f"Response #{self.req_counter}: "
|
||||
f"{unpad_jwt_payload(self.result.content.decode())}"
|
||||
)
|
||||
self.req_counter += 1
|
||||
return self.result.content
|
||||
|
||||
def trust_anchor_ec(self):
|
||||
url = reverse("entity_configuration")
|
||||
res = self.client.get(url, data={"sub": ta_conf_data["sub"]})
|
||||
return res
|
||||
|
||||
def rp_ec(self):
|
||||
rp = FederationEntityConfiguration.objects.get(sub=rp_onboarding_data["sub"])
|
||||
res = DummyContent(rp.entity_configuration_as_jws)
|
||||
return res
|
||||
|
||||
def fetch_rp_from_ta(self):
|
||||
url = reverse("oidcfed_fetch")
|
||||
res = self.client.get(url, data={"sub": rp_onboarding_data["sub"]})
|
||||
return res
|
||||
|
||||
|
||||
class EntityResponseNoIntermediate(EntityResponse):
|
||||
@property
|
||||
def content(self):
|
||||
if self.req_counter == 0:
|
||||
self.result = self.trust_anchor_ec()
|
||||
elif self.req_counter == 1:
|
||||
self.result = self.rp_ec()
|
||||
elif self.req_counter == 2:
|
||||
self.result = self.fetch_rp_from_ta()
|
||||
elif self.req_counter > 2:
|
||||
raise NotImplementedError(
|
||||
"The mocked resposes seems to be not aligned with the correct flow"
|
||||
)
|
||||
|
||||
return self.result_as_jws()
|
||||
|
||||
class EntityResponseNoIntermediateSignedJwksUri(EntityResponse):
|
||||
@property
|
||||
def content(self):
|
||||
if self.req_counter == 0:
|
||||
self.result = self.trust_anchor_ec()
|
||||
elif self.req_counter == 1:
|
||||
self.result = self.rp_ec()
|
||||
elif self.req_counter == 2:
|
||||
self.result = self.fetch_rp_from_ta()
|
||||
elif self.req_counter == 3:
|
||||
metadata = copy.deepcopy(rp_conf['metadata']['openid_relying_party'])
|
||||
_jwks = metadata.pop('jwks')
|
||||
fed_jwks = rp_conf['jwks_fed'][0]
|
||||
self.result = create_jws(_jwks, fed_jwks)
|
||||
return self.result.encode()
|
||||
elif self.req_counter > 3:
|
||||
raise NotImplementedError(
|
||||
"The mocked resposes seems to be not aligned with the correct flow"
|
||||
)
|
||||
|
||||
return self.result_as_jws()
|
||||
|
||||
|
||||
class EntityResponseWithIntermediate(EntityResponse):
|
||||
@property
|
||||
def content(self):
|
||||
if self.req_counter == 0:
|
||||
self.result = self.trust_anchor_ec()
|
||||
elif self.req_counter == 1:
|
||||
self.result = self.rp_ec()
|
||||
elif self.req_counter == 2:
|
||||
sa = FederationEntityConfiguration.objects.get(sub=intermediary_conf["sub"])
|
||||
self.result = DummyContent(sa.entity_configuration_as_jws)
|
||||
elif self.req_counter == 3:
|
||||
# Subordinate statement: intermediate -> RP (not TA -> RP)
|
||||
rp_desc = FederationDescendant.objects.get(sub=rp_onboarding_data["sub"])
|
||||
stmt_jws = rp_desc.entity_statement_as_jws(intermediary_conf["sub"])
|
||||
self.result = DummyContent(stmt_jws)
|
||||
elif self.req_counter == 4:
|
||||
url = reverse("oidcfed_fetch")
|
||||
self.result = self.client.get(url, data={"sub": intermediary_conf["sub"]})
|
||||
elif self.req_counter == 5:
|
||||
url = reverse("entity_configuration")
|
||||
self.result = self.client.get(url, data={"sub": ta_conf_data["sub"]})
|
||||
elif self.req_counter > 5:
|
||||
raise NotImplementedError(
|
||||
"The mocked resposes seems to be not aligned with the correct flow"
|
||||
)
|
||||
|
||||
if self.result.status_code != 200:
|
||||
raise HttpError(f"Something went wrong with Http Request: {self.result.__dict__}")
|
||||
|
||||
logger.info("-------------------------------------------------")
|
||||
logger.info("")
|
||||
return self.result_as_jws()
|
||||
|
||||
|
||||
class EntityResponseWithIntermediateManyHints(EntityResponse):
|
||||
@property
|
||||
def content(self):
|
||||
if self.req_counter == 0:
|
||||
self.result = self.trust_anchor_ec()
|
||||
elif self.req_counter == 1:
|
||||
self.result = self.rp_ec()
|
||||
elif self.req_counter == 2:
|
||||
sa = FederationEntityConfiguration.objects.get(sub=intermediary_conf["sub"])
|
||||
self.result = DummyContent(sa.entity_configuration_as_jws)
|
||||
elif self.req_counter == 3:
|
||||
# Second authority hint (that-faulty): invalid response
|
||||
self.result = DummyContent("crap")
|
||||
elif self.req_counter == 4:
|
||||
# Subordinate statement: intermediate -> RP
|
||||
rp_desc = FederationDescendant.objects.get(sub=rp_onboarding_data["sub"])
|
||||
stmt_jws = rp_desc.entity_statement_as_jws(intermediary_conf["sub"])
|
||||
self.result = DummyContent(stmt_jws)
|
||||
elif self.req_counter == 5:
|
||||
# TA -> intermediate subordinate statement
|
||||
url = reverse("oidcfed_fetch")
|
||||
self.result = self.client.get(url, data={"sub": intermediary_conf["sub"]})
|
||||
elif self.req_counter > 5:
|
||||
raise NotImplementedError(
|
||||
"The mocked resposes seems to be not aligned with the correct flow"
|
||||
)
|
||||
if self.result.status_code != 200:
|
||||
raise HttpError(f"Something went wrong with Http Request: {self.result.__dict__}")
|
||||
|
||||
try:
|
||||
return self.result_as_jws()
|
||||
except:
|
||||
return self.result_as_it_is()
|
||||
@@ -0,0 +1,149 @@
|
||||
from copy import deepcopy
|
||||
from spid_cie_oidc.entity.jwks import serialize_key, new_rsa_key
|
||||
|
||||
INTERMEDIARY_RSA = new_rsa_key()
|
||||
INTERMEDIARY_JWK1 = serialize_key(INTERMEDIARY_RSA.priv_key, kind='private')
|
||||
INTERMEDIARY_JWK1_pub = serialize_key(INTERMEDIARY_RSA.pub_key)
|
||||
|
||||
RP_METADATA_JWK1 = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
|
||||
RP_METADATA_JWK1_pub = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
|
||||
|
||||
RP_METADATA_JWK2 = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
|
||||
RP_METADATA_JWK2_pub = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'}
|
||||
|
||||
rp_onboarding_data = dict(
|
||||
name="RP Test",
|
||||
sub="http://rp-test.it/oidc/rp/",
|
||||
type="openid_relying_party",
|
||||
metadata_policy={"openid_relying_party": {"scope": {"value": ["openid"]}}},
|
||||
is_active=True,
|
||||
jwks = [RP_METADATA_JWK1_pub]
|
||||
)
|
||||
|
||||
rp_conf = {
|
||||
"sub": rp_onboarding_data["sub"],
|
||||
"jwks_fed" : [RP_METADATA_JWK1],
|
||||
"jwks_core" : [RP_METADATA_JWK1, RP_METADATA_JWK2],
|
||||
"metadata": {
|
||||
"openid_relying_party": {
|
||||
"application_type": "web",
|
||||
"client_registration_types": ["automatic"],
|
||||
"client_name": "Name of this service called http://rp-test.it/oidc/rp/",
|
||||
"contacts": ["[email protected]"],
|
||||
"grant_types": ["refresh_token", "authorization_code"],
|
||||
"redirect_uris": ["http://rp-test.it/oidc/rp/callback/"],
|
||||
"response_types": ["code"],
|
||||
"subject_type": "pairwise",
|
||||
"client_id": "http://rp-test.it/oidc/rp/",
|
||||
"jwks": {"keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub]},
|
||||
}
|
||||
},
|
||||
"authority_hints": ["http://testserver/"],
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
RP_CONF_AS_JSON = {
|
||||
"iss": rp_conf["sub"],
|
||||
"sub": rp_conf["sub"],
|
||||
"jwks": {
|
||||
"keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub]
|
||||
},
|
||||
"metadata": rp_conf["metadata"],
|
||||
"authority_hints":rp_conf["authority_hints"]
|
||||
}
|
||||
|
||||
intermediary_conf = {
|
||||
"sub": "http://intermediary-test",
|
||||
"metadata": {
|
||||
"federation_entity": {
|
||||
"contacts": ["ops@localhost"],
|
||||
"federation_fetch_endpoint": "http://intermediary-test/fetch",
|
||||
"homepage_uri": "http://intermediary-test",
|
||||
"name": "example Intermediate",
|
||||
}
|
||||
},
|
||||
"trust_marks": [],
|
||||
"authority_hints": ["http://testserver/"],
|
||||
"is_active": True,
|
||||
"jwks_core": [INTERMEDIARY_JWK1],
|
||||
"jwks_fed": [INTERMEDIARY_JWK1]
|
||||
}
|
||||
intermediary_onboarding_data = dict(
|
||||
name="intermediary-test",
|
||||
sub="http://intermediary-test",
|
||||
type="federation_entity",
|
||||
# metadata_policy = {"openid_relying_party": {"scope": {"value": ["openid"]}}},
|
||||
is_active=True,
|
||||
jwks = [INTERMEDIARY_JWK1_pub]
|
||||
)
|
||||
|
||||
|
||||
TRUST_MARK_PAYLOAD = {
|
||||
"iss": "$.issuer_sub",
|
||||
"sub": "$.sub",
|
||||
"iat": 1579621160,
|
||||
"trust_mark_id": "https://www.spid.gov.it/certification/rp",
|
||||
"logo_uri": "https://www.agid.gov.it/themes/custom/agid/logo.svg",
|
||||
"ref": "https://docs.italia.it/italia/spid/spid-regole-tecniche-oidc/it/stabile/index.html",
|
||||
}
|
||||
|
||||
RP_PROFILE = {
|
||||
"name": "SPID Public SP",
|
||||
"profile_category": "openid_relying_party",
|
||||
"profile_id": "https://www.spid.gov.it/certification/rp",
|
||||
"trust_mark_template": TRUST_MARK_PAYLOAD,
|
||||
}
|
||||
|
||||
RP_METADATA = {
|
||||
"openid_relying_party": {
|
||||
"application_type": "web",
|
||||
"client_registration_types": ["automatic"],
|
||||
"client_name": f"Name of this service called {rp_onboarding_data['sub']}",
|
||||
"contacts": ["[email protected]"],
|
||||
"grant_types": ["refresh_token", "authorization_code"],
|
||||
"jwks": {"keys": [RP_METADATA_JWK1_pub]},
|
||||
"redirect_uris": [f"{rp_onboarding_data['sub']}/callback/"],
|
||||
"response_types": ["code"],
|
||||
"subject_type": "pairwise",
|
||||
}
|
||||
}
|
||||
|
||||
TA_SUB = "http://testserver.it/"
|
||||
|
||||
FETCH_REQUEST = {
|
||||
"sub": rp_conf["sub"],
|
||||
}
|
||||
|
||||
LIST_REQUEST = {
|
||||
"entity_type": "openid_relying_party",
|
||||
}
|
||||
|
||||
TRUST_MARK = "eyJhbGciOiJSUzI1NiIsImtpZCI6IkZpZll4MDNibm9zRDhtNmdZUUlmTkhOUDljTV9TYW05VGM1bkxsb0lJcmMiLCJ0eXAiOiJ0cnVzdC1tYXJrK2p3dCJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvIiwic3ViIjoiaHR0cDovLzEyNy4wLjAuMTo4MDAyL29pZGMvb3AvIiwiaWF0IjoxNjQ4NTcxMTk4LCJpZCI6Imh0dHBzOi8vd3d3LnNwaWQuZ292Lml0L2NlcnRpZmljYXRpb24vb3AiLCJtYXJrIjoiaHR0cHM6Ly93d3cuYWdpZC5nb3YuaXQvdGhlbWVzL2N1c3RvbS9hZ2lkL2xvZ28uc3ZnIiwicmVmIjoiaHR0cHM6Ly9kb2NzLml0YWxpYS5pdC9pdGFsaWEvc3BpZC9zcGlkLXJlZ29sZS10ZWNuaWNoZS1vaWRjL2l0L3N0YWJpbGUvaW5kZXguaHRtbCJ9.mN-AMOmcxOKh9w8SIw9T0N4_wsmjrnMsPp_M2k3irErz0ikrRNtOQ0iPXhJ33szXUxZQJc0jlxu3glCliViv34u6FyAUK-WyVHWf2QKHL5oPbZj45Lx6u1ZMTgU9gKEAUl6MdkGtIEU6F0-q87-BKIKTvJmErH3pdrQG7ed5fR2lvFCN7QQ4rHnVYHVMEnlZxe2oMPtRILsSHaa30yPGCYfp36BpZx9rdmrV0bb8sIsRYFin7GIhkzl4FxqEQ0J6i1A4elgP9tVQsOu1Yd4E54t62KdVtTgfI4qfmP15NoPiNZFHLIFIMsUmqHMh5nuq9KWBb2IC1dfHM6AoxCpqtQ"
|
||||
|
||||
TRUST_MARK_REQUEST = {
|
||||
"sub": rp_conf["sub"],
|
||||
"trust_mark_id" : rp_conf["sub"],
|
||||
"trust_mark" : TRUST_MARK
|
||||
}
|
||||
|
||||
TRUST_MARK_REQUEST_NO_SUB_ID = deepcopy(TRUST_MARK_REQUEST)
|
||||
TRUST_MARK_REQUEST_NO_SUB_ID.pop("sub")
|
||||
TRUST_MARK_REQUEST_NO_SUB_ID.pop("trust_mark_id")
|
||||
|
||||
TRUST_MARK_REQUEST_NO_TRUST_MARK = deepcopy(TRUST_MARK_REQUEST)
|
||||
TRUST_MARK_REQUEST_NO_TRUST_MARK.pop("trust_mark")
|
||||
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB = deepcopy(TRUST_MARK_REQUEST)
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB.pop("sub")
|
||||
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK = deepcopy(TRUST_MARK_REQUEST)
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK.pop("sub")
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK.pop("trust_mark")
|
||||
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK = deepcopy(TRUST_MARK_REQUEST)
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK.pop("trust_mark_id")
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK.pop("trust_mark")
|
||||
|
||||
ADVANCED_LIST_REQUEST = {
|
||||
"page": 1,
|
||||
}
|
||||
Vendored
+385
@@ -0,0 +1,385 @@
|
||||
from django.test import TestCase, Client, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from spid_cie_oidc.entity.exceptions import InvalidRequiredTrustMark
|
||||
from spid_cie_oidc.entity.jwtse import verify_jws, unpad_jwt_head, unpad_jwt_payload
|
||||
from spid_cie_oidc.entity.models import *
|
||||
|
||||
from spid_cie_oidc.entity.trust_chain_operations import (
|
||||
dumps_statements_from_trust_chain_to_db,
|
||||
get_or_create_trust_chain,
|
||||
trust_chain_builder,
|
||||
)
|
||||
from spid_cie_oidc.entity.statements import (
|
||||
EntityConfiguration,
|
||||
get_entity_configurations,
|
||||
TrustMark,
|
||||
)
|
||||
from spid_cie_oidc.entity.utils import get_jwks
|
||||
from spid_cie_oidc.entity.tests.settings import *
|
||||
|
||||
from spid_cie_oidc.authority.models import *
|
||||
from spid_cie_oidc.authority.tests.mocked_responses import *
|
||||
from spid_cie_oidc.authority.tests.settings import *
|
||||
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
|
||||
class TrustChainTest(TestCase):
|
||||
def setUp(self):
|
||||
self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data)
|
||||
self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf)
|
||||
|
||||
self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE)
|
||||
self.rp = FederationDescendant.objects.create(**rp_onboarding_data)
|
||||
|
||||
self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create(
|
||||
descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf
|
||||
)
|
||||
|
||||
def test_fetch_endpoint(self):
|
||||
self.rp_profile.__str__()
|
||||
self.rp_profile.trust_mark_template_as_json
|
||||
url = reverse("oidcfed_fetch")
|
||||
c = Client()
|
||||
res = c.get(url, data={"sub": self.rp.sub})
|
||||
data = verify_jws(res.content.decode(), self.ta_conf.jwks_fed[0])
|
||||
|
||||
self.assertEqual(data['source_endpoint'], 'http://testserver//fetch')
|
||||
self.assertTrue(data["jwks"])
|
||||
|
||||
def test_list_endpoint(self):
|
||||
url = reverse("oidcfed_list")
|
||||
c = Client()
|
||||
res = c.get(url, data={})
|
||||
self.assertTrue(res.json())
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseNoIntermediate())
|
||||
def test_trust_chain_valid_no_intermediaries(self, mocked):
|
||||
|
||||
self.ta_conf.constraints = {"max_path_length": 0}
|
||||
self.ta_conf.save()
|
||||
|
||||
jwt = get_entity_configurations(self.ta_conf.sub)
|
||||
trust_anchor_ec = EntityConfiguration(jwt[0])
|
||||
|
||||
trust_chain = trust_chain_builder(
|
||||
subject=rp_onboarding_data["sub"],
|
||||
trust_anchor=trust_anchor_ec
|
||||
)
|
||||
self.assertTrue(trust_chain)
|
||||
self.assertTrue(trust_chain.final_metadata)
|
||||
|
||||
for i in trust_chain.tree_of_trust.values():
|
||||
for ec in i:
|
||||
self.assertTrue(ec.is_valid)
|
||||
|
||||
for ec in trust_chain.trust_path:
|
||||
self.assertTrue(ec.is_valid)
|
||||
|
||||
self.assertTrue(len(trust_chain.trust_path) == 2)
|
||||
self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len)
|
||||
|
||||
self.assertTrue(isinstance(trust_chain.exp, int))
|
||||
self.assertTrue(isinstance(trust_chain.exp_datetime, datetime.datetime))
|
||||
|
||||
def _create_federation_with_intermediary(self) -> EntityConfiguration:
|
||||
jwt = get_entity_configurations(self.ta_conf.sub)
|
||||
trust_anchor_ec = EntityConfiguration(jwt[0])
|
||||
|
||||
self.intermediate = FederationEntityConfiguration.objects.create(
|
||||
**intermediary_conf
|
||||
)
|
||||
|
||||
self.intermediate_desc = FederationDescendant.objects.create(
|
||||
**intermediary_onboarding_data
|
||||
)
|
||||
|
||||
FederationEntityAssignedProfile.objects.create(
|
||||
descendant=self.rp, profile=self.rp_profile, issuer=self.intermediate
|
||||
)
|
||||
self.rp_conf.authority_hints = [intermediary_conf["sub"]]
|
||||
self.rp_conf.save()
|
||||
return trust_anchor_ec
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseWithIntermediate())
|
||||
def test_trust_chain_valid_with_intermediary(self, mocked):
|
||||
|
||||
trust_anchor_ec = self._create_federation_with_intermediary()
|
||||
|
||||
trust_chain = trust_chain_builder(
|
||||
subject=self.rp.sub,
|
||||
trust_anchor=trust_anchor_ec
|
||||
)
|
||||
|
||||
self.assertTrue(trust_chain)
|
||||
self.assertTrue(trust_chain.final_metadata)
|
||||
|
||||
for i in trust_chain.tree_of_trust.values():
|
||||
for ec in i:
|
||||
self.assertTrue(ec.is_valid)
|
||||
|
||||
for ec in trust_chain.trust_path:
|
||||
self.assertTrue(ec.is_valid)
|
||||
|
||||
self.assertTrue(len(trust_chain.trust_path) == 3)
|
||||
self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len)
|
||||
|
||||
tc_ser = trust_chain.serialize()
|
||||
_p0 = unpad_jwt_payload(tc_ser[0])
|
||||
_p1 = unpad_jwt_payload(tc_ser[1])
|
||||
_p2 = unpad_jwt_payload(tc_ser[2])
|
||||
_p3 = unpad_jwt_payload(tc_ser[3])
|
||||
|
||||
# Entity configurations
|
||||
self.assertEqual(_p0['iss'], _p0['sub'])
|
||||
self.assertEqual(_p3['iss'], _p3['sub'])
|
||||
|
||||
# Entity statements
|
||||
self.assertNotEqual(_p1['iss'], _p1['sub'])
|
||||
self.assertNotEqual(_p2['iss'], _p2['sub'])
|
||||
|
||||
# Chain consistency (positive)
|
||||
self.assertEqual(_p0['iss'], _p1['sub'])
|
||||
self.assertEqual(_p1['iss'], _p2['sub'])
|
||||
self.assertEqual(_p2['iss'], _p3['sub'])
|
||||
|
||||
# Chain consistency (negative)
|
||||
self.assertNotEqual(_p1['iss'], _p0['sub'])
|
||||
self.assertNotEqual(_p2['iss'], _p1['sub'])
|
||||
self.assertNotEqual(_p3['iss'], _p2['sub'])
|
||||
|
||||
dumps = dumps_statements_from_trust_chain_to_db(trust_chain)
|
||||
|
||||
self.assertTrue(isinstance(dumps, list) and len(dumps) == 5)
|
||||
|
||||
self.assertTrue("sub" in dumps[0].get_entity_configuration_as_obj().payload)
|
||||
|
||||
stored_trust_chain = TrustChain.objects.create(
|
||||
sub=trust_chain.subject,
|
||||
exp=trust_chain.exp_datetime,
|
||||
chain=trust_chain.serialize(),
|
||||
jwks = trust_chain.subject_configuration.jwks,
|
||||
metadata=trust_chain.final_metadata,
|
||||
parties_involved=[i.sub for i in trust_chain.trust_path],
|
||||
status="valid",
|
||||
trust_anchor=FetchedEntityStatement.objects.get(
|
||||
sub=trust_anchor_ec.sub, iss=trust_anchor_ec.sub
|
||||
),
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseWithIntermediateManyHints())
|
||||
def test_trust_chain_valid_with_intermediaries_many_authhints(self, mocked):
|
||||
|
||||
trust_anchor_ec = self._create_federation_with_intermediary()
|
||||
|
||||
self.rp_conf.authority_hints = [
|
||||
"http://intermediary-test",
|
||||
"http://that-faulty",
|
||||
]
|
||||
self.rp_conf.save()
|
||||
|
||||
trust_chain = trust_chain_builder(
|
||||
subject=self.rp.sub,
|
||||
trust_anchor=trust_anchor_ec
|
||||
)
|
||||
|
||||
for ec in trust_chain.trust_path:
|
||||
self.assertTrue(ec.is_valid)
|
||||
|
||||
self.assertTrue(len(trust_chain.trust_path) == 3)
|
||||
self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseWithIntermediate())
|
||||
def test_trust_chain_valid_with_intermediaries_and_trust_mark_filter(self, mocked):
|
||||
|
||||
trust_anchor_ec = self._create_federation_with_intermediary()
|
||||
|
||||
# the RP exposes a trust marks in its entity configuration
|
||||
self.rp_conf.trust_marks = [
|
||||
FederationEntityAssignedProfile.objects.filter(descendant=self.rp)
|
||||
.first()
|
||||
.trust_mark
|
||||
]
|
||||
self.rp_conf.save()
|
||||
|
||||
trust_chain = trust_chain_builder(
|
||||
subject=self.rp.sub,
|
||||
trust_anchor=trust_anchor_ec,
|
||||
required_trust_marks=["https://www.spid.gov.it/certification/rp"],
|
||||
)
|
||||
|
||||
for ec in trust_chain.trust_path:
|
||||
self.assertTrue(ec.is_valid)
|
||||
|
||||
self.assertTrue(trust_chain.verified_trust_marks)
|
||||
self.assertTrue(isinstance(trust_chain.verified_trust_marks[0], TrustMark))
|
||||
|
||||
self.assertTrue(len(trust_chain.trust_path) == 3)
|
||||
self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseWithIntermediate())
|
||||
def test_trust_chain_with_missing_trust_mark(self, mocked):
|
||||
|
||||
trust_anchor_ec = self._create_federation_with_intermediary()
|
||||
|
||||
with self.assertRaises(InvalidRequiredTrustMark):
|
||||
trust_chain = trust_chain_builder(
|
||||
subject=self.rp.sub,
|
||||
trust_anchor=trust_anchor_ec,
|
||||
required_trust_marks=["https://www.spid.gov.it/certification/rp"],
|
||||
)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseNoIntermediate())
|
||||
def test_trust_chain_valid_helpers(self, mocked):
|
||||
|
||||
gctc = get_or_create_trust_chain(
|
||||
subject=rp_conf["sub"],
|
||||
trust_anchor=self.ta_conf.sub,
|
||||
# TODO
|
||||
# required_trust_marks: list = []
|
||||
)
|
||||
|
||||
self.assertFalse(gctc.is_expired)
|
||||
self.assertTrue(gctc.is_valid)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseNoIntermediate())
|
||||
def test_resolve_endpoint(self, mocked):
|
||||
gctc = get_or_create_trust_chain(
|
||||
subject=rp_conf["sub"],
|
||||
trust_anchor=self.ta_conf.sub,
|
||||
# TODO
|
||||
# required_trust_marks: list = []
|
||||
)
|
||||
|
||||
url = reverse("oidcfed_resolve")
|
||||
|
||||
c = Client()
|
||||
res = c.get(url, data={
|
||||
"sub": self.rp.sub,
|
||||
"trust_anchor": self.ta_conf.sub
|
||||
}
|
||||
)
|
||||
self.assertTrue(res.status_code == 200)
|
||||
verify_jws(res.content.decode(), self.ta_conf.jwks_fed[0])
|
||||
|
||||
def test_trust_mark_status_endpoint(self):
|
||||
url = reverse("oidcfed_trust_mark_status")
|
||||
c = Client()
|
||||
# Legacy: sub + trust_mark_id -> 200 + JWT (application/trust-mark-status-response+jwt)
|
||||
res = c.post(
|
||||
url,
|
||||
data={
|
||||
"trust_mark_id": self.rp_assigned_profile.profile.profile_id,
|
||||
"sub": self.rp_assigned_profile.descendant.sub,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.get("Content-Type"), "application/trust-mark-status-response+jwt")
|
||||
payload = unpad_jwt_payload(res.content.decode())
|
||||
self.assertEqual(payload.get("status"), "active")
|
||||
self.assertIn("trust_mark", payload)
|
||||
self.assertIn("iss", payload)
|
||||
self.assertIn("iat", payload)
|
||||
|
||||
# Draft 48: trust_mark JWT only -> 200 + JWT
|
||||
res = c.post(
|
||||
url,
|
||||
data={"trust_mark": self.rp_assigned_profile.trust_mark["trust_mark"]},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.get("Content-Type"), "application/trust-mark-status-response+jwt")
|
||||
payload = unpad_jwt_payload(res.content.decode())
|
||||
self.assertEqual(payload.get("status"), "active")
|
||||
|
||||
# Legacy: GET with sub + trust_mark_id -> 200 + JWT
|
||||
res = c.get(
|
||||
url,
|
||||
data={
|
||||
"trust_mark_id": self.rp_assigned_profile.profile.profile_id,
|
||||
"sub": self.rp_assigned_profile.descendant.sub,
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.get("Content-Type"), "application/trust-mark-status-response+jwt")
|
||||
|
||||
# Empty request -> 400 invalid_request
|
||||
res = c.get(url, data={})
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertEqual(res.json().get("error"), "invalid_request")
|
||||
|
||||
# Invalid trust_mark JWT -> 400
|
||||
res = c.get(
|
||||
url,
|
||||
data={"trust_mark": self.rp_assigned_profile.trust_mark["trust_mark"][1:]},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
# trust_mark_id without sub -> 400
|
||||
res = c.get(
|
||||
url,
|
||||
data={"trust_mark_id": self.rp_assigned_profile.profile.profile_id},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
# Unknown trust mark (wrong sub) -> 404 not_found
|
||||
res = c.get(
|
||||
url,
|
||||
data={
|
||||
"trust_mark_id": self.rp_assigned_profile.profile.profile_id,
|
||||
"sub": "https://nonexistent.example/",
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 404)
|
||||
self.assertEqual(res.json().get("error"), "not_found")
|
||||
|
||||
class TrustChainWithSignedJwksUriTest(TestCase):
|
||||
def setUp(self):
|
||||
self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data)
|
||||
|
||||
_rp_conf = copy.deepcopy(rp_conf)
|
||||
_rp_conf['metadata']['openid_relying_party'].pop('jwks')
|
||||
_rp_conf['metadata']['openid_relying_party']['signed_jwks_uri'] = f"{_rp_conf['sub']}signed_jwks.jose"
|
||||
self.rp_conf = FederationEntityConfiguration.objects.create(**_rp_conf)
|
||||
|
||||
self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE)
|
||||
self.rp = FederationDescendant.objects.create(**rp_onboarding_data)
|
||||
|
||||
self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create(
|
||||
descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf
|
||||
)
|
||||
|
||||
@override_settings(HTTP_CLIENT_SYNC=True)
|
||||
@patch("requests.get", return_value=EntityResponseNoIntermediateSignedJwksUri())
|
||||
def test_signed_jwks_uri(self, mocked):
|
||||
|
||||
self.ta_conf.constraints = {"max_path_length": 0}
|
||||
self.ta_conf.save()
|
||||
|
||||
jwt = get_entity_configurations(self.ta_conf.sub)
|
||||
trust_anchor_ec = EntityConfiguration(jwt[0])
|
||||
|
||||
trust_chain = trust_chain_builder(
|
||||
subject=rp_onboarding_data["sub"],
|
||||
trust_anchor=trust_anchor_ec
|
||||
)
|
||||
_jwks = get_jwks(
|
||||
trust_chain.final_metadata['openid_relying_party'],
|
||||
trust_chain.subject_configuration.jwks
|
||||
)
|
||||
self.assertTrue(
|
||||
verify_jws(_jwks, self.rp_conf.jwks_fed[0])
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from spid_cie_oidc.authority.tests.settings import (
|
||||
RP_CONF_AS_JSON,
|
||||
RP_METADATA_JWK1
|
||||
)
|
||||
from spid_cie_oidc.authority.utils import random_token
|
||||
from spid_cie_oidc.authority.validators import validate_entity_configuration
|
||||
from spid_cie_oidc.entity.exceptions import MissingAuthorityHintsClaim, NotDescendant
|
||||
from spid_cie_oidc.entity.jwtse import create_jws
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
from spid_cie_oidc.entity.tests.settings import ta_conf_data
|
||||
|
||||
JWS = create_jws(RP_CONF_AS_JSON, RP_METADATA_JWK1, typ="entity-statement+jwt")
|
||||
RP_CONF_AS_JSON_NO_HINTS = deepcopy(RP_CONF_AS_JSON)
|
||||
RP_CONF_AS_JSON_NO_HINTS.pop("authority_hints")
|
||||
JWS_NO_HINTS = create_jws(RP_CONF_AS_JSON_NO_HINTS, RP_METADATA_JWK1, typ="entity-statement+jwt")
|
||||
RP_CONF_AS_JSON_WRONG_HINTS = deepcopy(RP_CONF_AS_JSON)
|
||||
RP_CONF_AS_JSON_WRONG_HINTS["authority_hints"] = ["http://wrong.url.it"]
|
||||
JWS_WRONG_HINTS = create_jws(RP_CONF_AS_JSON_WRONG_HINTS, RP_METADATA_JWK1, typ="entity-statement+jwt")
|
||||
|
||||
class ValidatorTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data)
|
||||
|
||||
@patch("spid_cie_oidc.authority.validators.get_entity_configurations", return_value = [JWS])
|
||||
def test_validator(self, mocked):
|
||||
validate_entity_configuration(["url_entity"])
|
||||
|
||||
@patch("spid_cie_oidc.authority.validators.get_entity_configurations", return_value = [JWS_NO_HINTS])
|
||||
def test_validator_no_hints(self, mocked):
|
||||
with self.assertRaises(MissingAuthorityHintsClaim):
|
||||
validate_entity_configuration(["url_entity"])
|
||||
|
||||
@patch("spid_cie_oidc.authority.validators.get_entity_configurations", return_value = [JWS_WRONG_HINTS])
|
||||
def test_validator_no_descendant(self, mocked):
|
||||
with self.assertRaises(NotDescendant):
|
||||
validate_entity_configuration(["url_entity"])
|
||||
|
||||
|
||||
class UtilityTests(TestCase):
|
||||
|
||||
def test_random_token(self):
|
||||
self.assertTrue(random_token())
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from spid_cie_oidc.authority.models import FederationDescendant
|
||||
from spid_cie_oidc.authority.tests.settings import rp_onboarding_data
|
||||
from spid_cie_oidc.entity.models import FederationEntityConfiguration
|
||||
from spid_cie_oidc.entity.tests.settings import ta_conf_data
|
||||
|
||||
|
||||
class AdvanceEntityListing(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
FederationDescendant.objects.create(**rp_onboarding_data)
|
||||
rp_onboarding_data_local = deepcopy(rp_onboarding_data)
|
||||
rp_onboarding_data_local["sub"] = "http://rp-test.it/oidc/rplocal/"
|
||||
FederationDescendant.objects.create(**rp_onboarding_data_local)
|
||||
self.ta = FederationEntityConfiguration.objects.create(**ta_conf_data)
|
||||
|
||||
@override_settings(MAX_ENTRIES_PAGE=1)
|
||||
def test_advanced_entity_listing_1(self):
|
||||
url = reverse("oidcfed_advanced_entity_listing")
|
||||
data = {"page":2}
|
||||
res = Client().get(url, data)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.json().get("iss"), "http://testserver/")
|
||||
self.assertEqual(len(res.json().get("entities")), 2)
|
||||
self.assertEqual(res.json().get("page"), 2)
|
||||
url_prev_page = reverse("oidcfed_advanced_entity_listing")
|
||||
self.assertEqual(res.json().get("prev_page_path"), f"{url_prev_page}?page=1")
|
||||
|
||||
@override_settings(MAX_ENTRIES_PAGE=1)
|
||||
def test_advanced_entity_listing_no_page(self):
|
||||
url = reverse("oidcfed_advanced_entity_listing")
|
||||
res = Client().get(url)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(res.json().get("iss"), "http://testserver/")
|
||||
self.assertEqual(len(res.json().get("entities")), 2)
|
||||
self.assertEqual(res.json().get("page"), 1)
|
||||
url_next_page = reverse("oidcfed_advanced_entity_listing")
|
||||
self.assertEqual(res.json().get("next_page_path"), f"{url_next_page}?page=2")
|
||||
|
||||
def test_advanced_entity_listing_missing_trust_anchor(self):
|
||||
FederationEntityConfiguration.objects.all().delete()
|
||||
c = Client()
|
||||
url = reverse("oidcfed_advanced_entity_listing")
|
||||
res = c.get(url)
|
||||
self.assertEqual(res.status_code, 404)
|
||||
self.assertEqual(res.json().get("error"), "Missing trust anchor")
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
from django.test import TestCase
|
||||
from spid_cie_oidc.authority.schemas.advanced_entity_list_endpoint import AdvancedEntityListRequest
|
||||
from spid_cie_oidc.authority.schemas.fetch_endpoint_request import FetchRequest
|
||||
from spid_cie_oidc.authority.schemas.list_endpoint import ListRequest
|
||||
from spid_cie_oidc.authority.schemas.trust_mark_status_endpoint import TrustMarkRequest
|
||||
from spid_cie_oidc.authority.tests.settings import (
|
||||
FETCH_REQUEST,
|
||||
LIST_REQUEST,
|
||||
TRUST_MARK_REQUEST,
|
||||
ADVANCED_LIST_REQUEST,
|
||||
TRUST_MARK_REQUEST_NO_SUB_ID,
|
||||
TRUST_MARK_REQUEST_NO_TRUST_MARK,
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK,
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB,
|
||||
TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK,
|
||||
)
|
||||
|
||||
class SchemaTest(TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
return super().setUp()
|
||||
|
||||
def test_fetch_request(self):
|
||||
FetchRequest(**FETCH_REQUEST)
|
||||
|
||||
def test_list_request(self):
|
||||
ListRequest(**LIST_REQUEST)
|
||||
|
||||
def test_trust_mark_request(self):
|
||||
TrustMarkRequest(**TRUST_MARK_REQUEST)
|
||||
|
||||
def test_trust_mark_request_no_sub_id(self):
|
||||
TrustMarkRequest(**TRUST_MARK_REQUEST_NO_SUB_ID)
|
||||
|
||||
def test_trust_mark_request_no_rust_mark(self):
|
||||
TrustMarkRequest(**TRUST_MARK_REQUEST_NO_TRUST_MARK)
|
||||
|
||||
def test_trust_mark_request_no_sub(self):
|
||||
TrustMarkRequest(**TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB)
|
||||
|
||||
def test_trust_mark_request_no_id_no_tust_mark(self):
|
||||
# Schema allows optional fields; view returns 400 if neither (sub+id) nor trust_mark provided
|
||||
TrustMarkRequest(**TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK)
|
||||
|
||||
def test_trust_mark_request_no_sub_no_tust_mark(self):
|
||||
# Schema allows optional fields; view returns 400 if neither (sub+id) nor trust_mark provided
|
||||
TrustMarkRequest(**TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK)
|
||||
|
||||
def test_advanced_list_request(self):
|
||||
AdvancedEntityListRequest(**ADVANCED_LIST_REQUEST)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""acsia_agent_proj URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.urls import path
|
||||
|
||||
from .views import (
|
||||
entity_list,
|
||||
fetch,
|
||||
trust_mark_status,
|
||||
advanced_entity_listing,
|
||||
trust_marked_list
|
||||
)
|
||||
|
||||
_PREF = getattr(settings, "OIDC_PREFIX", "")
|
||||
urlpatterns = [
|
||||
path(f"{_PREF}fetch", fetch, name="oidcfed_fetch"),
|
||||
path(f"{_PREF}list", entity_list, name="oidcfed_list"),
|
||||
path(
|
||||
f"{_PREF}trust_mark_status",
|
||||
trust_mark_status,
|
||||
name="oidcfed_trust_mark_status",
|
||||
),
|
||||
path(
|
||||
f"{_PREF}advanced_entity_listing",
|
||||
advanced_entity_listing,
|
||||
name="oidcfed_advanced_entity_listing",
|
||||
),
|
||||
path(f"{_PREF}trust_marked_list", trust_marked_list, name="oidcfed_tm_list"),
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
from secrets import token_hex
|
||||
|
||||
|
||||
def random_token(n=254):
|
||||
return token_hex(n)
|
||||
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from spid_cie_oidc.entity.statements import (
|
||||
OIDCFED_FEDERATION_WELLKNOWN_URL,
|
||||
get_entity_configurations,
|
||||
EntityConfiguration,
|
||||
)
|
||||
|
||||
from spid_cie_oidc.entity import settings as entity_settings
|
||||
from spid_cie_oidc.entity.exceptions import MissingAuthorityHintsClaim, NotDescendant
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
HTTPC_PARAMS = getattr(settings, "HTTPC_PARAMS", entity_settings.HTTPC_PARAMS)
|
||||
|
||||
|
||||
def validate_entity_configuration(value):
|
||||
"""
|
||||
value is the sub url
|
||||
"""
|
||||
jwt = None
|
||||
try:
|
||||
jwt = get_entity_configurations(value, httpc_params = HTTPC_PARAMS)[0]
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
f"Failed to fetch Entity Configuration for {value}: {e}"
|
||||
)
|
||||
if not jwt: # pragma: no cover
|
||||
raise ValidationError(
|
||||
"failed to get a valid Entity Configuration from "
|
||||
f"{value}{OIDCFED_FEDERATION_WELLKNOWN_URL}"
|
||||
)
|
||||
|
||||
try:
|
||||
ec = EntityConfiguration(jwt, httpc_params=HTTPC_PARAMS)
|
||||
ec.validate_by_itself()
|
||||
except Exception as e: # pragma: no cover
|
||||
raise ValidationError(
|
||||
f"Failed to validate the Entity Configuration for {value}: {e}"
|
||||
)
|
||||
|
||||
authority_hints = ec.payload.get("authority_hints", [])
|
||||
if not authority_hints:
|
||||
raise MissingAuthorityHintsClaim(
|
||||
"authority_hints must be present in "
|
||||
"a descendant entity configuration"
|
||||
)
|
||||
|
||||
superior_sub = import_string(
|
||||
"spid_cie_oidc.entity.models.get_first_self_trust_anchor"
|
||||
)().sub
|
||||
if superior_sub not in authority_hints:
|
||||
raise NotDescendant(
|
||||
f"This participant MUST have {superior_sub} in "
|
||||
f"its authority_hints claim. It has: {authority_hints}."
|
||||
)
|
||||
return ec
|
||||
@@ -0,0 +1,301 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user