feat: link local library
This commit is contained in:
+5
-1
@@ -9,11 +9,15 @@ RUN apt-get update \
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
&& pip install --no-cache-dir \
|
||||
"git+https://github.com/italia/spid-cie-oidc-django.git" \
|
||||
"design-django-theme==v1.4.8"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY vendor/spid-cie-oidc-django/spid_cie_oidc /opt/spid_cie_oidc/spid_cie_oidc
|
||||
COPY vendor/spid-cie-oidc-django/setup.py /opt/spid_cie_oidc/setup.py
|
||||
COPY vendor/spid-cie-oidc-django/README.md /opt/spid_cie_oidc/README.md
|
||||
RUN pip install --no-cache-dir /opt/spid_cie_oidc
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
COPY scripts /app/scripts
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
# SPID/CIE OIDC Federation SDK
|
||||
|
||||

|
||||

|
||||

|
||||
[](https://github.com/italia/spid-cie-oidc-django/issues)
|
||||
[](https://slack.developers.italia.it/)
|
||||
[](https://developersitalia.slack.com/archives/C7E85ED1N/)
|
||||
|
||||
|
||||
SPID/CIE OIDC Federation is a suite of Django applications designed to
|
||||
make it easy to build an [Openid Connect Federation](https://openid.net/specs/openid-connect-federation-1_0.html),
|
||||
each of these can be installed separately within a django project. These are the following:
|
||||
|
||||
| Application | Description |
|
||||
| :--- | --- |
|
||||
| __spid_cie_oidc.accounts__ | Customizable application that extends the django User model. |
|
||||
| __spid_cie_oidc.entity__ | OpenID Connect Federation django app that implements OIDC Federation 1.0 Entity Statements, metadata discovery, Trust Chain, Trust Marks and Metadata policy. Technical specifications: [__OIDC Federation Entity__](docs/technical_specifications/ENTITY.md) |
|
||||
| __spid_cie_oidc.authority__ | OpenID Connect Federation API and models for __OIDC Federation Trust Chain/Intermediate__, [Technical specifications](docs/technical_specifications/AUTHORITY.md) and [tutorial](docs/CREATE_A_FEDERATION.md).
|
||||
| __spid_cie_oidc.onboarding__ | [__OpenID Connect Federation onboarding demo service__](docs/technical_specifications/ONBOARDING.md) and tools|
|
||||
| __spid_cie_oidc.relying_party__ | [__OpenID Connect Relying Party__](docs/technical_specifications/RELYING_PARTY.md) and test suite for OIDC Providers |
|
||||
| __spid_cie_oidc.provider__ | [__OpenID Connect Provider__](docs/technical_specifications/PROVIDER.md) and test suite for OIDC Relying Parties |
|
||||
|
||||
## Summary
|
||||
|
||||
* [Features](#features)
|
||||
* [Setup](#setup)
|
||||
* [Docker](#docker)
|
||||
* [Usage](#usage)
|
||||
* [OpenAPI Schema 3](#openapi-schema-3)
|
||||
* [Tools](#tools)
|
||||
* [Contribute](#contribute)
|
||||
* [Contribute as end user](#contribute-as-end-user)
|
||||
* [Contribute as developer](#contribute-as-developer)
|
||||
* [Implementations notes](#implementation-notes)
|
||||
* [License and Authors](#license-and-authors)
|
||||
|
||||
------------------------------------------------
|
||||
|
||||

|
||||
_An onboarded Relying Party with a succesful authentication._
|
||||
|
||||
## Setup
|
||||
|
||||
All the Django apps are available in the folder `spid_cie_oidc/`.
|
||||
The examples projects are available in the folder `examples/`.
|
||||
|
||||
There is a substantial difference between an app and a project.
|
||||
The app is installed using a common python package manager, such as _poetry_ or _pip_,
|
||||
and can be used, inherited, and integrated into other projects.
|
||||
|
||||
A project is a service configuration that integrates one or more applications.
|
||||
In this repository we have three example projects:
|
||||
|
||||
- federation_authority
|
||||
- relying_party
|
||||
- provider
|
||||
|
||||
Federation Authority loads all the applications for development needs, acting as both authority, SPID RP and SPID OP.
|
||||
This allows us to make a demo by starting a single service. See admin page `http://127.0.0.1:8000/admin/` and user login page `http://127.0.0.1:8000/oidc/rp/landing/`.
|
||||
|
||||
Then we have also another Relying Party, as indipendent project, and another Provider configured with the CIE profile.
|
||||
Relying party and Provider are examples that only integrate
|
||||
__spid_cie_oidc.entity__ and __spid_cie_oidc.provider__ or __.relying_party__ as applications.
|
||||
|
||||
Read the [setup documentation](docs/SETUP.md) to get started.
|
||||
|
||||
## Docker
|
||||
|
||||
### Docker image
|
||||
|
||||
````
|
||||
docker pull ghcr.io/italia/spid-cie-oidc-django:latest
|
||||
````
|
||||
|
||||
### Docker compose
|
||||
|
||||
Please do your customizations in each _settingslocal.py_ files and/or in the example dumps json file.
|
||||
|
||||
Change hostnames from 127.0.0.1 to which one configured in the compose file, in the settingslocal.py files and in the dumps/example.json files.
|
||||
In our example we rename:
|
||||
|
||||
- http://127.0.0.1:8000 to http://trust-anchor.org:8000/
|
||||
- http://127.0.0.1:8001 to http://relying-party.org:8001/
|
||||
- http://127.0.0.1:8002 to http://cie-provider.org:8002/
|
||||
|
||||
We can do that with the following steps:
|
||||
|
||||
- Execute `bash docker-prepare.sh`
|
||||
- Customize the example data and settings contained in `examples-docker/` if needed (not necessary for a quick demo)
|
||||
|
||||
|
||||
Run the stack
|
||||
````
|
||||
sudo docker-compose up
|
||||
````
|
||||
|
||||
Configure a proper DNS resolution for trust-anchor.org. In GNU/Linux we can configure it in `/etc/hosts`:
|
||||
````
|
||||
127.0.0.1 localhost trust-anchor.org relying-party.org cie-provider.org wallet.trust-anchor.org
|
||||
````
|
||||
|
||||
Point your web browser to `http://relying-party.org:8001/oidc/rp/landing` and do your first oidc authentication.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
The demo proposes a small federation composed by the following entities:
|
||||
|
||||
- Federation Authority, acts as trust anchor and onboarding system. It's available at `http://127.0.0.1:8000/`. It has also an embedded Spid provider and a embedded Relying Party available at `/oidc/rp/landing`.
|
||||
- OpenID Relying Party, available at `http://127.0.0.1:8001/`
|
||||
- CIE OpenID Provider, available at `http://127.0.0.1:8002/`
|
||||
|
||||
In the docker example we have only the Federation Authority with an embedded SPID OP and a RP.
|
||||
|
||||
Examples Users and Passwords:
|
||||
|
||||
- __admin__ __oidcadmin__
|
||||
- __user__ __oidcuser__
|
||||
|
||||
|
||||
## OpenAPI Schema 3
|
||||
|
||||
Each application has an exportable OAS3 available at `/rest/schema.json` with a browsable reDoc UI at `/rest/api/docs`.
|
||||
|
||||

|
||||
_The reDoc OAS3 browsable page._
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
The OnBoarding app comes with the following collection of tools:
|
||||
|
||||
- JWK
|
||||
- Create a jwk
|
||||
- Convert a private JWK to PEM
|
||||
- Convert a public JWK to PEM
|
||||
- Convert a private PEM to JWK
|
||||
- Convert a public PEM to JWK
|
||||
- JWT decode and verification
|
||||
- Federation
|
||||
- Resolve entity statement
|
||||
- Apply policy
|
||||
- Validators
|
||||
- Validate OP metadata spid
|
||||
- Validate OP metadata cie
|
||||
- Validate RP metadata spid
|
||||
- Validate RP metadata cie
|
||||
- Validate Authn Request spid
|
||||
- Validate Authn Request cie
|
||||
- Validate Entity Configuration
|
||||
- Trust mark validation
|
||||
- Schemas
|
||||
- Authorization Endpoint
|
||||
- Introspection Endpoint
|
||||
- Metadata
|
||||
- Token Endpoint
|
||||
- Revocation Endpoint
|
||||
- Jwt client Assertion
|
||||
|
||||

|
||||
_OIDC tools facilitates the lives of developers and service operators, here a simple interface to decode and verify a JWT._
|
||||
|
||||
To explore a federation on the commandline, use the [ofcli](https://github.com/dianagudu/ofcli) tool. It can be used to export federation metadata to json files for further analysis.
|
||||
|
||||
## Contribute
|
||||
|
||||
Your contribution is welcome, no question is useless and no answer is obvious, we need you.
|
||||
|
||||
#### Contribute as end user
|
||||
|
||||
Please open an issue if you've discoveerd a bug or if you want to ask some features.
|
||||
|
||||
#### Contribute as developer
|
||||
|
||||
Please open your Pull Requests on the __dev__ branch.
|
||||
Please consider the following branches:
|
||||
|
||||
- __main__: where we merge the code before tag a new stable release.
|
||||
- __dev__: where we push our code during development.
|
||||
- __other-custom-name__: where a new feature/contribution/bugfix will be handled, revisioned and then merged to dev branch.
|
||||
|
||||
Backup and share your demo data
|
||||
````
|
||||
# backup your data (upgrade example data), -e excludes.
|
||||
./manage.py dumpdata -e admin -e spid_cie_oidc_relying_party -e spid_cie_oidc_provider -e spid_cie_oidc_relying_party_test -e auth -e contenttypes -e sessions --indent 2 > dumps/example.json
|
||||
````
|
||||
|
||||
In this project we adopt [Semver](https://semver.org/lang/it/) and
|
||||
[Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specifications.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
All the operation related to JWT signature and encryption are built on top of [IdentityPython](https://idpy.org/)
|
||||
[cryptojwt](https://github.com/IdentityPython/JWTConnect-Python-CryptoJWT)
|
||||
|
||||
This project proposes an implementation of the italian OIDC Federation profile with
|
||||
__automatic_client_registration__ and the adoption of the trust marks as mandatory.
|
||||
|
||||
If you're looking for a fully compliant implementation of OIDC Federation 1.0,
|
||||
with a full support of explicit client registration, please look at idpy's
|
||||
[fedservice](https://github.com/rohe/fedservice).
|
||||
|
||||
#### General Features
|
||||
|
||||
- SPID and CIE OpenID Connect Provider
|
||||
- SPID and CIE OpenID Connect Relying Party
|
||||
- OIDC Federation onboarding demo service
|
||||
- OIDC Federation 1.0
|
||||
- Trust Anchor and Intermediary
|
||||
- Automatic client registration
|
||||
- Entity profiles and Trust marks
|
||||
- Trust chain storage and discovery
|
||||
- Entity statement resolve endpoint
|
||||
- Fetch statement endpoing
|
||||
- List entities endpoint
|
||||
- Advanced List endpoint
|
||||
- Federation CLI
|
||||
- RP: build trust chains for all the available OPs
|
||||
- OP: build trust chains for all the available RPs
|
||||
- Multitenancy, a single service can configure many entities like RPs, OP, Trust Anchors and intermediaries
|
||||
- gettext compliant (i18n)
|
||||
- Bootstrap Italia Design templates
|
||||
|
||||
## License and Authors
|
||||
|
||||
This software is released under the Apache 2 License by:
|
||||
|
||||
- Giuseppe De Marco <gi.demarco@innovazione.gov.it>.
|
||||
|
||||
In this project we use the
|
||||
[metadata policy code](https://github.com/peppelinux/spid-cie-oidc-django/blob/main/spid_cie_oidc/entity/policy.py)
|
||||
written by Roland Hedberg and licensed under the same Apache 2 license.
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
import re
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from glob import glob
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
SRC_FOLDER = '.'
|
||||
PKG_NAME = 'spid_cie_oidc'
|
||||
|
||||
INSTALL_REQUIRES = [
|
||||
"Django>=4.0,<5.0",
|
||||
"cryptojwt>=1.8.2",
|
||||
"pydantic>=1.8.2,<2.0",
|
||||
"pytz>=2021.3",
|
||||
"aiohttp>=3.9,<4",
|
||||
"requests>=2.32.0,<3.0",
|
||||
"pydantic[email]",
|
||||
"djagger",
|
||||
# Security: pin transitive deps to fix known CVEs (pip-audit)
|
||||
"certifi>=2024.7.4",
|
||||
"idna>=3.7",
|
||||
"sqlparse>=0.4.4",
|
||||
"urllib3>=2.2.0",
|
||||
"setuptools>=78.1.1",
|
||||
]
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf-8') as readme:
|
||||
README = readme.read()
|
||||
|
||||
with open(f'{SRC_FOLDER}{os.path.sep}{PKG_NAME}/__init__.py', 'r') as fd:
|
||||
VERSION = re.search(
|
||||
r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
|
||||
fd.read(), re.MULTILINE
|
||||
).group(1)
|
||||
|
||||
LICENSE = "License :: OSI Approved :: Apache Software License"
|
||||
|
||||
setup(
|
||||
name=PKG_NAME,
|
||||
version=VERSION,
|
||||
description="SPID/CIE OIDC Federation Entity",
|
||||
long_description=README,
|
||||
long_description_content_type='text/markdown',
|
||||
author='Giuseppe De Marco',
|
||||
author_email='[email protected]',
|
||||
license=LICENSE,
|
||||
url=f"https://github.com/peppelinux/{PKG_NAME.replace('_', '-')}",
|
||||
packages=[PKG_NAME, ],
|
||||
package_dir={f"{PKG_NAME}": f"{SRC_FOLDER}/{PKG_NAME}"},
|
||||
|
||||
package_data={
|
||||
f"{PKG_NAME}": [
|
||||
i.replace(
|
||||
f'{SRC_FOLDER}{os.path.sep}{PKG_NAME}{os.path.sep}', ''
|
||||
)
|
||||
for i in glob(
|
||||
f'{SRC_FOLDER}{os.path.sep}{PKG_NAME}{os.path.sep}**',
|
||||
recursive=True
|
||||
)
|
||||
if i and '__pycache__' not in i
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
LICENSE,
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules"],
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
zip_safe=False,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "1.6.3"
|
||||
@@ -0,0 +1,74 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
readonly_fields = (
|
||||
"date_joined",
|
||||
"last_login",
|
||||
)
|
||||
list_display = (
|
||||
"username",
|
||||
"email",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
)
|
||||
list_editable = (
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
)
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
(
|
||||
"username",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
),
|
||||
("password"),
|
||||
("origin"),
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
_("Anagrafica"),
|
||||
{"fields": (("first_name", "last_name"), "email", ("attributes",))},
|
||||
),
|
||||
(
|
||||
_("Permissions"),
|
||||
{"fields": ("groups", "user_permissions"), "classes": ("collapse",)},
|
||||
),
|
||||
(
|
||||
_("Date accessi sistema"),
|
||||
{
|
||||
"fields": (
|
||||
(
|
||||
"date_joined",
|
||||
"last_login",
|
||||
)
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"classes": ("wide",),
|
||||
"fields": ("username", "password1", "password2"),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = "spid_cie_oidc.accounts"
|
||||
verbose_name = _("User accounts")
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
label = "spid_cie_oidc_accounts"
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-06 15:45
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=96, null=True, verbose_name="Name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=96, null=True, verbose_name="Surname"
|
||||
),
|
||||
),
|
||||
("is_active", models.BooleanField(default=True, verbose_name="active")),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True,
|
||||
max_length=254,
|
||||
null=True,
|
||||
verbose_name="email address",
|
||||
),
|
||||
),
|
||||
(
|
||||
"taxpayer_id",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=32,
|
||||
null=True,
|
||||
verbose_name="Taxpayer's identification number",
|
||||
),
|
||||
),
|
||||
(
|
||||
"origin",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=254,
|
||||
null=True,
|
||||
verbose_name="from which connector this user come from",
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Users",
|
||||
"ordering": ["username"],
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-02 17:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_accounts", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="taxpayer_id",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="attributes",
|
||||
field=models.JSONField(blank=True, default={}, null=True),
|
||||
),
|
||||
]
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-02 17:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_accounts", "0002_remove_user_taxpayer_id_user_attributes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="attributes",
|
||||
field=models.JSONField(blank=True, default=dict, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
first_name = models.CharField(_("Name"), max_length=96, blank=True, null=True)
|
||||
last_name = models.CharField(_("Surname"), max_length=96, blank=True, null=True)
|
||||
is_active = models.BooleanField(_("active"), default=True)
|
||||
email = models.EmailField("email address", blank=True, null=True)
|
||||
origin = models.CharField(
|
||||
_("from which connector this user come from"),
|
||||
max_length=254,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
attributes = models.JSONField(default=dict, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["username"]
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
def clear_sessions(self): # pragma: no cover
|
||||
user_sessions = []
|
||||
for session in Session.objects.all():
|
||||
if str(self.pk) == session.get_decoded().get("_auth_user_id"):
|
||||
user_sessions.append(session.pk)
|
||||
return Session.objects.filter(pk__in=user_sessions).delete()
|
||||
|
||||
def __str__(self): # pragma: no cover
|
||||
return "{} {}".format(
|
||||
self.first_name or self.attributes.get('given_name'),
|
||||
self.last_name or self.attributes.get('family_name', "")
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
from django import template
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="has_group")
|
||||
def has_group(user, group_name): # pragma: no cover
|
||||
group = Group.objects.get(name=group_name)
|
||||
return group in user.groups.all()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class KeyUsage(str, Enum):
|
||||
signature = "sig"
|
||||
encryption = "enc"
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimeStampedModel(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
modified = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class CreatedModifiedBy(models.Model):
|
||||
created_by = models.ForeignKey(
|
||||
get_user_model(),
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="%(class)s_created_by",
|
||||
)
|
||||
modified_by = models.ForeignKey(
|
||||
get_user_model(),
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="%(class)s_modified_by",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,202 @@
|
||||
import logging
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.contrib import messages
|
||||
|
||||
# from prettyjson import PrettyJSONWidget
|
||||
|
||||
from django.conf import settings
|
||||
from .jwtse import unpad_jwt_payload
|
||||
from .statements import EntityConfiguration, get_entity_configurations, get_entity_statements
|
||||
from .models import (
|
||||
FederationEntityConfiguration,
|
||||
FederationHistoricalKey,
|
||||
FetchedEntityStatement,
|
||||
TrustChain,
|
||||
StaffToken
|
||||
)
|
||||
from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain
|
||||
|
||||
from . settings import HTTPC_PARAMS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@admin.register(FederationHistoricalKey)
|
||||
class FederationHistoricalKey(admin.ModelAdmin):
|
||||
readonly_fields = ('jwk', "as_json")
|
||||
list_display = ("entity", "kid", "inactive_from")
|
||||
list_filter = ("created", "modified", "inactive_from")
|
||||
search_fields = ("entity",)
|
||||
|
||||
|
||||
@admin.register(FederationEntityConfiguration)
|
||||
class FederationEntityConfigurationAdmin(admin.ModelAdmin):
|
||||
# formfield_overrides = {
|
||||
# JSONField: {
|
||||
# "widget": PrettyJSONWidget(
|
||||
# attrs={"initial": "parsed", "disabled": True}
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
|
||||
@admin.action(description='update trust marks')
|
||||
def update_trust_marks(modeladmin, request, queryset): # pragma: no cover
|
||||
"""
|
||||
fetch trust marks from all the authorities
|
||||
"""
|
||||
trust_marks = {}
|
||||
|
||||
for obj in queryset:
|
||||
jwts = get_entity_configurations(obj.authority_hints, HTTPC_PARAMS)
|
||||
for jwt in jwts:
|
||||
|
||||
try:
|
||||
ec = EntityConfiguration(jwt, httpc_params=HTTPC_PARAMS)
|
||||
except Exception as e:
|
||||
_msg = f"Failed getting Entity Configuration for {jwt}: {e}"
|
||||
logger.warning(_msg)
|
||||
messages.error(_msg)
|
||||
continue
|
||||
|
||||
try:
|
||||
# get superior fetch url
|
||||
fetch_api_url = ec.payload["metadata"]["federation_entity"][
|
||||
"federation_fetch_endpoint"
|
||||
]
|
||||
except KeyError:
|
||||
_msg = (
|
||||
"Missing federation_fetch_endpoint in "
|
||||
f"federation_entity metadata for {obj.sub} by {ec.sub}."
|
||||
)
|
||||
logger.warning(_msg)
|
||||
messages.error(_msg)
|
||||
continue
|
||||
|
||||
_url = f"{fetch_api_url}?sub={obj.sub}"
|
||||
try:
|
||||
logger.info(f"Getting entity statements from {_url}")
|
||||
_jwts = get_entity_statements([_url], HTTPC_PARAMS)
|
||||
|
||||
payload = unpad_jwt_payload(_jwts[0])
|
||||
for i in payload.get("trust_marks", []):
|
||||
trust_marks[i['id']] = i['trust_mark']
|
||||
except Exception as e:
|
||||
_msg = f"Error getting entity statements from {_url}: {e}"
|
||||
logger.warning(_msg)
|
||||
messages.error(_msg)
|
||||
continue
|
||||
|
||||
positions = {}
|
||||
count = 0
|
||||
for i in obj.trust_marks:
|
||||
positions[i['id']] = count
|
||||
count += 1
|
||||
|
||||
if positions:
|
||||
for k,v in trust_marks.items():
|
||||
if positions.get(k, None):
|
||||
obj.trust_marks[positions[k]] = {k:v}
|
||||
else:
|
||||
obj.trust_marks.append({k:v})
|
||||
else:
|
||||
obj.trust_marks = [
|
||||
{"trust_mark_id":k, "trust_mark":v} for k,v in trust_marks.items()
|
||||
]
|
||||
|
||||
obj.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Trust mark reloaded succesfully: {', '.join(trust_marks.keys())}"
|
||||
)
|
||||
|
||||
list_display = (
|
||||
"sub",
|
||||
"type",
|
||||
"kids",
|
||||
"is_active",
|
||||
"created",
|
||||
)
|
||||
list_filter = ("created", "modified", "is_active")
|
||||
# search_fields = ('command__name',)
|
||||
readonly_fields = (
|
||||
"created",
|
||||
"modified",
|
||||
"entity_configuration_as_json",
|
||||
"pems_as_html",
|
||||
"kids",
|
||||
"type",
|
||||
)
|
||||
actions = [update_trust_marks]
|
||||
|
||||
def pems_as_html(self, obj):
|
||||
res = ""
|
||||
data = dict()
|
||||
for k, v in obj.pems_as_dict.items():
|
||||
data[k] = {}
|
||||
for i in ("public", "private"):
|
||||
data[k][i] = v[i].replace("\n", "<br>")
|
||||
res += (
|
||||
f"<b>{k}</b><br><br>"
|
||||
f"{data[k]['public']}<br>"
|
||||
f"{data[k]['private']}<br><hr>"
|
||||
)
|
||||
return mark_safe(res) # nosec
|
||||
|
||||
|
||||
@admin.register(TrustChain)
|
||||
class TrustChainAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.action(description='reload trust chain')
|
||||
def update_trust_chain(modeladmin, request, queryset): # pragma: no cover
|
||||
for tc in queryset:
|
||||
sub = tc.sub
|
||||
ta = tc.trust_anchor.sub
|
||||
try :
|
||||
get_or_create_trust_chain(
|
||||
subject=sub,
|
||||
trust_anchor=ta,
|
||||
httpc_params=settings.HTTPC_PARAMS,
|
||||
required_trust_marks=getattr(
|
||||
settings, "OIDCFED_REQUIRED_TRUST_MARKS", [],
|
||||
),
|
||||
force=True
|
||||
)
|
||||
messages.success(
|
||||
request, f"Trust chain successfully reloaded for {sub}"
|
||||
)
|
||||
except Exception as e:
|
||||
messages.error(request, f"Failed to update {sub} due to: {e}")
|
||||
continue
|
||||
|
||||
list_display = ("sub", "exp", "modified", "is_valid")
|
||||
list_filter = ("exp", "modified", "is_active")
|
||||
search_fields = ("sub",)
|
||||
readonly_fields = (
|
||||
"created",
|
||||
"modified",
|
||||
"parties_involved",
|
||||
"status",
|
||||
"log",
|
||||
"chain",
|
||||
"iat",
|
||||
)
|
||||
actions = [update_trust_chain]
|
||||
|
||||
|
||||
@admin.register(FetchedEntityStatement)
|
||||
class FetchedEntityStatementAdmin(admin.ModelAdmin):
|
||||
list_display = ("sub", "iss", "exp", "iat", "created", "modified")
|
||||
list_filter = ("created", "modified", "exp", "iat")
|
||||
search_fields = ("sub", "iss")
|
||||
readonly_fields = ("sub", "statement", "created", "modified", "iat", "exp", "iss")
|
||||
|
||||
|
||||
@admin.register(StaffToken)
|
||||
class StaffTokenAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "expire_at", "is_valid")
|
||||
list_filter = ("created", "modified", "expire_at")
|
||||
search_fields = ("token", )
|
||||
readonly_fields = ("is_valid",)
|
||||
raw_id_fields = ('user',)
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SpidCieFederationEntityConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "spid_cie_oidc.entity"
|
||||
verbose_name = "SPID/CIE OIDC Federation Entity"
|
||||
label = "spid_cie_oidc_entity"
|
||||
@@ -0,0 +1,53 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class HttpError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TrustChainHttpError(HttpError):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownKid(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingJwksClaim(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingAuthorityHintsClaim(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class NotDescendant(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class TrustAnchorNeeded(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class MetadataDiscoveryException(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingTrustMark(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRequiredTrustMark(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidTrustchain(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class TrustchainMissingMetadata(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEntityConfiguration(ValidationError):
|
||||
pass
|
||||
@@ -0,0 +1,44 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
|
||||
async def fetch(session, url, httpc_params: dict = {}):
|
||||
async with session.get(url, **httpc_params.get("connection", {})) as response:
|
||||
if response.status != 200: # pragma: no cover
|
||||
# response.raise_for_status()
|
||||
return ""
|
||||
return await response.text()
|
||||
|
||||
|
||||
async def fetch_all(session, urls, httpc_params):
|
||||
tasks = []
|
||||
for url in urls:
|
||||
task = asyncio.create_task(fetch(session, url, httpc_params))
|
||||
tasks.append(task)
|
||||
results = await asyncio.gather(*tasks)
|
||||
return results
|
||||
|
||||
|
||||
async def http_get(urls, httpc_params: dict = {}):
|
||||
_con = aiohttp.TCPConnector(**httpc_params.get("connection", {}))
|
||||
async with aiohttp.ClientSession(
|
||||
connector=_con,
|
||||
**httpc_params.get("session", {})
|
||||
) as session:
|
||||
text = await fetch_all(session, urls, httpc_params)
|
||||
return text
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
httpc_params = {
|
||||
"connection": {"ssl": True},
|
||||
"session": {"timeout": aiohttp.ClientTimeout(total=4)},
|
||||
}
|
||||
urls = [
|
||||
"http://127.0.0.1:8001/.well-known/openid-federation",
|
||||
"http://127.0.0.1:8000/.well-known/openid-federation",
|
||||
"http://asdasd.it",
|
||||
"http://127.0.0.1:8001/.well-known/openid-federation",
|
||||
"http://google.it",
|
||||
]
|
||||
asyncio.run(http_get(urls, httpc_params=httpc_params))
|
||||
@@ -0,0 +1,86 @@
|
||||
import base64
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
from typing import Union
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, ed448, rsa
|
||||
from cryptojwt.jwk.jwk import key_from_jwk_dict
|
||||
|
||||
from spid_cie_oidc.entity.x509 import X509Issuer
|
||||
|
||||
|
||||
def _normalize_jwks(jwks):
|
||||
"""
|
||||
Normalize jwks to a list of JWK dicts. Handles:
|
||||
- {"keys": [...]} (JWKS structure from entity config)
|
||||
- [...] (list of keys)
|
||||
- Single JWK dict (as item that may be JSON string)
|
||||
"""
|
||||
if isinstance(jwks, dict):
|
||||
if "keys" in jwks:
|
||||
jwks = jwks["keys"]
|
||||
else:
|
||||
jwks = [jwks]
|
||||
if not isinstance(jwks, list):
|
||||
jwks = [jwks]
|
||||
result = []
|
||||
for item in jwks:
|
||||
if isinstance(item, dict):
|
||||
result.append(item)
|
||||
elif isinstance(item, str):
|
||||
result.append(json.loads(item))
|
||||
else:
|
||||
raise TypeError(f"JWK item must be dict or JSON string, got {type(item)}")
|
||||
return result
|
||||
|
||||
|
||||
def _public_key_from_jwk_dict(jwk_dict: dict):
|
||||
"""
|
||||
Extract public key from a JWK dict. Handles keys with x5c but no kty
|
||||
(e.g. from entity configurations that publish keys via x5c only).
|
||||
"""
|
||||
if "kty" in jwk_dict:
|
||||
return key_from_jwk_dict(jwk_dict).public_key()
|
||||
# JWK has x5c but no kty - derive from certificate
|
||||
x5c = jwk_dict.get("x5c")
|
||||
if not x5c:
|
||||
raise ValueError("JWK must have either kty or x5c")
|
||||
der = base64.b64decode(x5c[0])
|
||||
cert = x509.load_der_x509_certificate(der)
|
||||
pub_key = cert.public_key()
|
||||
# Add kty so the JWK is valid for downstream consumers
|
||||
if isinstance(pub_key, rsa.RSAPublicKey):
|
||||
jwk_dict["kty"] = "RSA"
|
||||
elif isinstance(pub_key, ec.EllipticCurvePublicKey):
|
||||
jwk_dict["kty"] = "EC"
|
||||
elif isinstance(pub_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)):
|
||||
jwk_dict["kty"] = "OKP"
|
||||
else:
|
||||
jwk_dict["kty"] = "RSA" # fallback for unknown types
|
||||
return pub_key
|
||||
|
||||
|
||||
def update_jwks_with_x5c(
|
||||
jwks: list,
|
||||
private_key: bytes,
|
||||
subject: str,
|
||||
is_ca_or_int: bool,
|
||||
path_length: Union[int, None] = None,
|
||||
) -> dict:
|
||||
jwks = _normalize_jwks(jwks)
|
||||
subject_data: dict = dict(
|
||||
X509_COMMON_NAME=urlparse(subject).hostname,
|
||||
# TODO: add COUNTRY_NAME, X509_STATE_OR_PROVINCE_NAME, X509_LOCALITY_NAME, X509_ORGANIZATION_NAME
|
||||
entity_id=subject
|
||||
)
|
||||
|
||||
for i in jwks:
|
||||
i['x5c'] = X509Issuer(
|
||||
private_key = private_key,
|
||||
public_key = _public_key_from_jwk_dict(i),
|
||||
subject_data = subject_data,
|
||||
is_ca_or_int = is_ca_or_int,
|
||||
path_length = path_length
|
||||
).x5c
|
||||
return jwks
|
||||
@@ -0,0 +1,121 @@
|
||||
from cryptojwt.jwk.jwk import key_from_jwk_dict
|
||||
from cryptojwt.jwk.rsa import new_rsa_key
|
||||
from cryptojwt.jwk.ec import new_ec_key
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptojwt.jwk.rsa import RSAKey
|
||||
from cryptojwt.jwk.ec import ECKey
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from . import settings as local_settings
|
||||
|
||||
|
||||
DEFAULT_HASH_FUNC = getattr(
|
||||
settings, "DEFAULT_HASH_FUNC", local_settings.DEFAULT_HASH_FUNC
|
||||
)
|
||||
|
||||
|
||||
def create_jwk(key=None, hash_func=None, typ: str = getattr(settings, "PRIVATE_KEY_TYPE", None)):
|
||||
if key:
|
||||
key = key
|
||||
elif typ and typ.lower() == 'ec':
|
||||
key = new_ec_key(crv="P-256")
|
||||
else:
|
||||
key = new_rsa_key()
|
||||
|
||||
thumbprint = key.thumbprint(hash_function=hash_func or DEFAULT_HASH_FUNC)
|
||||
jwk = key.to_dict()
|
||||
jwk["kid"] = thumbprint.decode()
|
||||
return jwk
|
||||
|
||||
|
||||
def public_jwk_from_private_jwk(jwk_dict: dict):
|
||||
# exports public
|
||||
_k = key_from_jwk_dict(jwk_dict)
|
||||
jwk = _k.serialize()
|
||||
jwk["kid"] = jwk_dict['kid']
|
||||
return jwk
|
||||
|
||||
|
||||
def private_pem_from_jwk(jwk_dict: dict):
|
||||
# exports private
|
||||
|
||||
_k = key_from_jwk_dict(jwk_dict)
|
||||
pk = _k.private_key()
|
||||
pem = pk.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
return pem.decode()
|
||||
|
||||
|
||||
def public_pem_from_jwk(jwk_dict: dict):
|
||||
# exports private
|
||||
|
||||
_k = key_from_jwk_dict(jwk_dict)
|
||||
pk = _k.public_key()
|
||||
cert = pk.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
return cert.decode()
|
||||
|
||||
|
||||
def serialize_key(key, kind="public", hash_func="SHA-256"):
|
||||
"""
|
||||
key can be:
|
||||
- cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey
|
||||
- cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey
|
||||
- cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey
|
||||
- cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
|
||||
- str or bytes (PEM format)
|
||||
"""
|
||||
data = {}
|
||||
|
||||
# Handle RSA keys
|
||||
if isinstance(key, rsa.RSAPublicKey):
|
||||
data = {"pub_key": key}
|
||||
elif isinstance(key, rsa.RSAPrivateKey):
|
||||
data = {"priv_key": key}
|
||||
# Handle EC keys
|
||||
elif isinstance(key, ec.EllipticCurvePublicKey):
|
||||
data = {"pub_key": key}
|
||||
elif isinstance(key, ec.EllipticCurvePrivateKey):
|
||||
data = {"priv_key": key}
|
||||
# Handle PEM strings/bytes
|
||||
elif isinstance(key, (str, bytes)):
|
||||
if kind == "private":
|
||||
if isinstance(key, rsa.RSAPrivateKey) or (isinstance(key, str) and "RSA" in key):
|
||||
data = {"priv_key": private_jwk_from_pem(key)}
|
||||
else:
|
||||
data = {"priv_key": private_jwk_from_pem(key)}
|
||||
else:
|
||||
if isinstance(key, rsa.RSAPublicKey) or (isinstance(key, str) and "RSA" in key):
|
||||
data = {"pub_key": public_jwk_from_pem(key)}
|
||||
else:
|
||||
data = {"pub_key": public_jwk_from_pem(key)}
|
||||
|
||||
# Create appropriate JWK object based on key type
|
||||
if isinstance(key, (rsa.RSAPublicKey, rsa.RSAPrivateKey)) or (isinstance(key, str) and "RSA" in key):
|
||||
jwk_obj = RSAKey(**data)
|
||||
else:
|
||||
jwk_obj = ECKey(**data)
|
||||
|
||||
thumbprint = jwk_obj.thumbprint(hash_function=hash_func)
|
||||
jwk = jwk_obj.to_dict()
|
||||
jwk["kid"] = thumbprint.hex() # Use hex() instead of decode()
|
||||
return jwk
|
||||
|
||||
|
||||
def private_jwk_from_pem(content:str, password:str = None):
|
||||
content = content.encode() if isinstance(content, str) else content
|
||||
key = serialization.load_pem_private_key(content, password=password)
|
||||
return serialize_key(key, kind='private')
|
||||
|
||||
|
||||
def public_jwk_from_pem(content:str, password:str = None):
|
||||
content = content.encode() if isinstance(content, str) else content
|
||||
key = serialization.load_pem_public_key(content)
|
||||
return serialize_key(key, kind='public')
|
||||
@@ -0,0 +1,137 @@
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
|
||||
import cryptojwt
|
||||
from cryptojwt.exception import UnsupportedAlgorithm, VerificationError
|
||||
from cryptojwt.jwe.jwe import factory
|
||||
from cryptojwt.jwe.jwe_ec import JWE_EC
|
||||
from cryptojwt.jwe.jwe_rsa import JWE_RSA
|
||||
from cryptojwt.jwk.jwk import key_from_jwk_dict
|
||||
from cryptojwt.jws.jws import JWS
|
||||
from cryptojwt.jws.utils import left_hash
|
||||
from typing import Union
|
||||
|
||||
from .settings import (
|
||||
DEFAULT_JWE_ALG,
|
||||
DEFAULT_JWE_ENC,
|
||||
ENCRYPTION_ALG_VALUES_SUPPORTED,
|
||||
SIGNING_ALG_VALUES_SUPPORTED,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def unpad_jwt_element(jwt: str, position: int) -> dict:
|
||||
b = jwt.split(".")[position]
|
||||
padded = f"{b}{'=' * divmod(len(b), 4)[1]}"
|
||||
data = json.loads(base64.urlsafe_b64decode(padded))
|
||||
return data
|
||||
|
||||
|
||||
def unpad_jwt_head(jwt: str) -> dict:
|
||||
return unpad_jwt_element(jwt, position=0)
|
||||
|
||||
|
||||
def unpad_jwt_payload(jwt: str) -> dict:
|
||||
return unpad_jwt_element(jwt, position=1)
|
||||
|
||||
|
||||
def create_jwe(plain_dict: Union[dict, str, int, None], jwk_dict: dict, **kwargs) -> str:
|
||||
logger.debug(f"Encrypting dict as JWE: " f"{plain_dict}")
|
||||
_key = key_from_jwk_dict(jwk_dict)
|
||||
|
||||
if isinstance(_key, cryptojwt.jwk.rsa.RSAKey):
|
||||
JWE_CLASS = JWE_RSA
|
||||
elif isinstance(_key, cryptojwt.jwk.ec.ECKey):
|
||||
JWE_CLASS = JWE_EC
|
||||
|
||||
if isinstance(plain_dict, dict):
|
||||
_payload = json.dumps(plain_dict).encode()
|
||||
elif not plain_dict:
|
||||
logger.warning("create_jwe with null payload!")
|
||||
_payload = ""
|
||||
elif isinstance(plain_dict, (str, int)):
|
||||
_payload = plain_dict
|
||||
else:
|
||||
logger.error("create_jwe with unsupported payload type!")
|
||||
_payload = ""
|
||||
|
||||
_keyobj = JWE_CLASS(
|
||||
_payload,
|
||||
alg=DEFAULT_JWE_ALG,
|
||||
enc=DEFAULT_JWE_ENC,
|
||||
kid=_key.kid,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
jwe = _keyobj.encrypt(_key.public_key())
|
||||
logger.debug(f"Encrypted dict as JWE: {jwe}")
|
||||
return jwe
|
||||
|
||||
|
||||
def decrypt_jwe(jwe: str, jwk_dict: dict) -> dict:
|
||||
# get header
|
||||
try:
|
||||
jwe_header = unpad_jwt_head(jwe)
|
||||
except (binascii.Error, Exception) as e: # pragma: no cover
|
||||
logger.error(f"Failed to extract JWT header: {e}")
|
||||
raise VerificationError("The JWT is not valid")
|
||||
|
||||
_alg = jwe_header.get("alg", DEFAULT_JWE_ALG)
|
||||
_enc = jwe_header.get("enc", DEFAULT_JWE_ENC)
|
||||
jwe_header.get("kid")
|
||||
|
||||
if _alg not in ENCRYPTION_ALG_VALUES_SUPPORTED: # pragma: no cover
|
||||
raise UnsupportedAlgorithm(f"{_alg} has beed disabled for security reason")
|
||||
|
||||
_decryptor = factory(jwe, alg=_alg, enc=_enc)
|
||||
|
||||
# _dkey = RSAKey(priv_key=PRIV_KEY)
|
||||
_dkey = key_from_jwk_dict(jwk_dict)
|
||||
msg = _decryptor.decrypt(jwe, [_dkey])
|
||||
|
||||
try:
|
||||
msg_dict = json.loads(msg)
|
||||
logger.debug(f"Decrypted JWT as: {json.dumps(msg_dict, indent=2)}")
|
||||
except json.decoder.JSONDecodeError:
|
||||
msg_dict = msg
|
||||
logger.debug(f"Decrypted JWT as: {msg_dict}")
|
||||
return msg_dict
|
||||
|
||||
|
||||
def create_jws(payload: dict, jwk_dict: dict, alg: str = "RS256", protected:dict = {}, **kwargs) -> str:
|
||||
_key = key_from_jwk_dict(jwk_dict)
|
||||
_signer = JWS(payload, alg=alg, **kwargs)
|
||||
|
||||
signature = _signer.sign_compact([_key], protected=protected, **kwargs)
|
||||
return signature
|
||||
|
||||
|
||||
def verify_jws(jws: str, pub_jwk: dict, **kwargs) -> str:
|
||||
_key = key_from_jwk_dict(pub_jwk)
|
||||
|
||||
_head = unpad_jwt_head(jws)
|
||||
if _head.get("kid") != pub_jwk["kid"]: # pragma: no cover
|
||||
raise Exception(
|
||||
f"kid error: {_head.get('kid')} != {pub_jwk['kid']}"
|
||||
)
|
||||
|
||||
_alg = _head["alg"]
|
||||
if _alg not in SIGNING_ALG_VALUES_SUPPORTED or not _alg: # pragma: no cover
|
||||
raise UnsupportedAlgorithm(f"{_alg} has beed disabled for security reason")
|
||||
|
||||
verifier = JWS(alg=_head["alg"], **kwargs)
|
||||
msg = verifier.verify_compact(jws, [_key])
|
||||
return msg
|
||||
|
||||
|
||||
def verify_at_hash(id_token, access_token) -> bool:
|
||||
id_token_at_hash = id_token['at_hash']
|
||||
at_hash = left_hash(access_token, "HS256")
|
||||
if at_hash != id_token_at_hash:
|
||||
raise Exception(
|
||||
f"at_hash error: {at_hash} != {id_token_at_hash}"
|
||||
)
|
||||
return True
|
||||
@@ -0,0 +1,243 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-13 09:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.entity.models
|
||||
import spid_cie_oidc.entity.validators
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FederationEntityConfiguration",
|
||||
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)),
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||
),
|
||||
(
|
||||
"sub",
|
||||
models.URLField(
|
||||
help_text="URL that identifies this Entity in the Federation. This value and iss are the same in the Entity Configuration.",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"default_exp",
|
||||
models.PositiveIntegerField(
|
||||
default=33,
|
||||
help_text="how many minutes from now() an issued statement must expire",
|
||||
),
|
||||
),
|
||||
(
|
||||
"default_signature_alg",
|
||||
models.CharField(
|
||||
default="RS256",
|
||||
help_text="default signature algorithm, eg: RS256",
|
||||
max_length=16,
|
||||
),
|
||||
),
|
||||
(
|
||||
"authority_hints",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="only required if this Entity is an intermediary",
|
||||
),
|
||||
),
|
||||
(
|
||||
"jwks",
|
||||
models.JSONField(
|
||||
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
|
||||
help_text="a list of public keys",
|
||||
),
|
||||
),
|
||||
(
|
||||
"trust_marks",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="which trust marks MUST be exposed in its entity configuration",
|
||||
),
|
||||
),
|
||||
(
|
||||
"trust_mark_issuers",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text='Only usable for Trust Anchors and intermediates. Which issuers are allowed to issue trust marks for the descendants. Example: {"https://www.spid.gov.it/certification/rp": ["https://registry.spid.agid.gov.it", "https://intermediary.spid.it"],"https://sgd.aa.it/onboarding": ["https://sgd.aa.it", ]}',
|
||||
),
|
||||
),
|
||||
(
|
||||
"metadata",
|
||||
models.JSONField(
|
||||
default=dict,
|
||||
help_text='federation_entity metadata, eg: {"federation_entity": { ... },"openid_provider": { ... },"openid_relying_party": { ... },"oauth_resource": { ... }}',
|
||||
validators=[
|
||||
spid_cie_oidc.entity.validators.validate_entity_metadata
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="If this configuration is active. At least one configuration must be active",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Entity Configuration",
|
||||
"verbose_name_plural": "Federation Entity Configurations",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PublicJwk",
|
||||
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)),
|
||||
(
|
||||
"kid",
|
||||
models.CharField(
|
||||
help_text="unique code that identifies this jwks. ",
|
||||
max_length=1024,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"jwk",
|
||||
models.JSONField(
|
||||
default=dict,
|
||||
help_text="Public jwk",
|
||||
validators=[
|
||||
spid_cie_oidc.entity.validators.validate_public_jwks
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Public JWKs",
|
||||
"verbose_name_plural": "Public JWKs",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TrustChain",
|
||||
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)),
|
||||
(
|
||||
"sub",
|
||||
models.URLField(
|
||||
help_text="URL that identifies this Entity in the Federation. ",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"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_provider",
|
||||
help_text="OpenID Connect Federation entity type",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
("exp", models.DateTimeField()),
|
||||
("iat", models.DateTimeField()),
|
||||
(
|
||||
"chain",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="A list of entity statements collected during the metadata discovery",
|
||||
),
|
||||
),
|
||||
(
|
||||
"resultant_metadata",
|
||||
models.JSONField(
|
||||
default=dict,
|
||||
help_text="The final metadata applied with the metadata policy built over the chain",
|
||||
),
|
||||
),
|
||||
(
|
||||
"parties_involved",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="subjects involved in the metadata discovery",
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("unreachable", "unreachable"),
|
||||
("valid", "valid"),
|
||||
("signature_failed", "signature_failed"),
|
||||
("not_valid", "not_valid"),
|
||||
("unknown", "unknown"),
|
||||
("expired", "expired"),
|
||||
],
|
||||
default=False,
|
||||
help_text="Status of this trust chain, on each update.",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status_log",
|
||||
models.JSONField(blank=True, default=dict, help_text="status log"),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="If you need to disable the trust to this subject, deacticate this",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Trust Chain",
|
||||
"verbose_name_plural": "Trust Chains",
|
||||
"unique_together": {("sub", "type")},
|
||||
},
|
||||
),
|
||||
]
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-13 11:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FetchedEntityStatement",
|
||||
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)),
|
||||
(
|
||||
"iss",
|
||||
models.URLField(
|
||||
help_text="URL that identifies the issuer of this statement in the Federation. ",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"sub",
|
||||
models.URLField(
|
||||
help_text="URL that identifies this Entity in the Federation. ",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("exp", models.DateTimeField()),
|
||||
("iat", models.DateTimeField()),
|
||||
(
|
||||
"statement",
|
||||
models.JSONField(default=dict, help_text="Entity statement"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Fetched Entity Statement",
|
||||
"verbose_name_plural": "Fetched Entity Statement",
|
||||
},
|
||||
),
|
||||
]
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-13 23:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0002_fetchedentitystatement"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="constraints",
|
||||
field=models.JSONField(
|
||||
default=dict,
|
||||
help_text='\n{\n "naming_constraints": {\n "permitted": [\n "https://.example.com"\n ],\n "excluded": [\n "https://east.example.com"\n ]\n },\n "max_path_length": 2\n}\n',
|
||||
),
|
||||
),
|
||||
]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-14 15:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0003_federationentityconfiguration_constraints"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="authority_hints",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="only required if this Entity is an intermediary or leaf.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="default_exp",
|
||||
field=models.PositiveIntegerField(
|
||||
default=2880,
|
||||
help_text="how many minutes from now() an issued statement must expire",
|
||||
),
|
||||
),
|
||||
]
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-14 23:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_entity",
|
||||
"0004_alter_federationentityconfiguration_authority_hints_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="constraints",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text='\n{\n "naming_constraints": {\n "permitted": [\n "https://.example.com"\n ],\n "excluded": [\n "https://east.example.com"\n ]\n },\n "max_path_length": 2\n}\n',
|
||||
),
|
||||
),
|
||||
]
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-22 00:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_entity",
|
||||
"0005_alter_federationentityconfiguration_constraints",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="trustchain",
|
||||
name="resultant_metadata",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="trustchain",
|
||||
name="status_log",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="trustchain",
|
||||
name="log",
|
||||
field=models.TextField(blank=True, default="", help_text="status log"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="trustchain",
|
||||
name="metadata",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="The final metadata applied with the metadata policy built over the chain",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="trustchain",
|
||||
name="processing_start",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.localtime,
|
||||
help_text="When the metadata discovery started for this Trust Chain. It should prevent concurrent processing for the same sub/type.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="trustchain",
|
||||
name="is_active",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="If you need to disable the trust to this subject, uncheck this",
|
||||
),
|
||||
),
|
||||
]
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-25 14:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0006_remove_trustchain_resultant_metadata_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="trustchain",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="trustchain",
|
||||
name="trust_anchor",
|
||||
field=models.ForeignKey(
|
||||
default=1,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_entity.fetchedentitystatement",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="trustchain",
|
||||
name="iat",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="trustchain",
|
||||
unique_together={("sub", "type", "trust_anchor")},
|
||||
),
|
||||
]
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-25 14:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0007_alter_trustchain_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="fetchedentitystatement",
|
||||
name="jwt",
|
||||
field=models.CharField(default="", max_length=2048),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-26 18:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.entity.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0008_fetchedentitystatement_jwt"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="trustchain",
|
||||
name="trust_marks",
|
||||
field=models.JSONField(
|
||||
blank=True, default=list, help_text="verified trust marks"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="jwks",
|
||||
field=models.JSONField(
|
||||
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
|
||||
help_text="a list of private keys",
|
||||
),
|
||||
),
|
||||
]
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-26 23:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0009_trustchain_trust_marks_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="entity_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"),
|
||||
],
|
||||
default="openid_relying_party",
|
||||
help_text="OpenID Connect Federation entity type",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
]
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-01 16:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0010_federationentityconfiguration_entity_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="trustchain",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("unreachable", "unreachable"),
|
||||
("valid", "valid"),
|
||||
("signature_failed", "signature_failed"),
|
||||
("not_valid", "not_valid"),
|
||||
("unknown", "unknown"),
|
||||
("expired", "expired"),
|
||||
],
|
||||
default="unreachable",
|
||||
help_text="Status of this trust chain, on each update.",
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
]
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-06 22:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0011_alter_trustchain_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='PublicJwk',
|
||||
),
|
||||
]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-13 00:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.entity.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0012_delete_publicjwk'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='federationentityconfiguration',
|
||||
name='metadata',
|
||||
field=models.JSONField(default=dict, help_text='federation_entity metadata, eg: {"federation_entity": { ... },"openid_provider": { ... },"openid_relying_party": { ... },"oauth_resource": { ... }}', validators=[spid_cie_oidc.entity.validators.validate_entity_metadata, spid_cie_oidc.entity.validators.validate_metadata_algs]),
|
||||
),
|
||||
]
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.0.3 on 2022-03-20 13:27
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0013_alter_federationentityconfiguration_metadata'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='trustchain',
|
||||
unique_together={('sub', 'trust_anchor')},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='trustchain',
|
||||
name='type',
|
||||
),
|
||||
]
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.0.3 on 2022-03-20 13:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0014_alter_trustchain_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='trustchain',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trustchain',
|
||||
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')], default='openid_provider', help_text='OpenID Connect Federation entity type', max_length=33),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='trustchain',
|
||||
unique_together={('sub', 'type', 'trust_anchor')},
|
||||
),
|
||||
]
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.0.3 on 2022-03-20 14:25
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0015_alter_trustchain_unique_together_trustchain_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='trustchain',
|
||||
unique_together={('sub', 'trust_anchor')},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='trustchain',
|
||||
name='type',
|
||||
),
|
||||
]
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.0.3 on 2022-04-23 14:48
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.entity.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0016_alter_trustchain_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='federationentityconfiguration',
|
||||
name='jwks',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='federationentityconfiguration',
|
||||
name='jwks_core',
|
||||
field=models.JSONField(default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, help_text='a list of private keys for Core ops'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='federationentityconfiguration',
|
||||
name='jwks_fed',
|
||||
field=models.JSONField(default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, help_text='a list of private keys for Federation ops'),
|
||||
),
|
||||
]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.5 on 2022-07-03 11:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('spid_cie_oidc_entity', '0017_remove_federationentityconfiguration_jwks_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trustchain',
|
||||
name='jwks',
|
||||
field=models.JSONField(default=[], help_text='jwks of this federation entity'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.0.5 on 2022-10-18 09:08
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import spid_cie_oidc.entity.utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('spid_cie_oidc_entity', '0018_trustchain_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.entity.utils.random_token, help_text='it will be generated automatically.', max_length=255)),
|
||||
('expire_at', models.DateTimeField(blank=True, null=True)),
|
||||
('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',
|
||||
},
|
||||
),
|
||||
]
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 11:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import spid_cie_oidc.entity.models
|
||||
import spid_cie_oidc.entity.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0019_stafftoken"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="jwks_core",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
|
||||
help_text="a list of private keys for Core ops",
|
||||
validators=[spid_cie_oidc.entity.validators.validate_private_jwks],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="jwks_fed",
|
||||
field=models.JSONField(
|
||||
default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks,
|
||||
help_text="a list of private keys for Federation ops",
|
||||
validators=[spid_cie_oidc.entity.validators.validate_private_jwks],
|
||||
),
|
||||
),
|
||||
]
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 13:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_entity",
|
||||
"0020_alter_federationentityconfiguration_jwks_core_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FederationHistoricalKey",
|
||||
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)),
|
||||
(
|
||||
"kid",
|
||||
models.CharField(
|
||||
choices=[
|
||||
(0, "unspecified"),
|
||||
(1, "keyCompromise"),
|
||||
(2, "cACompromise"),
|
||||
(3, "affiliationChanged"),
|
||||
(4, "superseded"),
|
||||
(5, "cessationOfOperation"),
|
||||
(6, "certificateHold"),
|
||||
(8, "removeFromCRL"),
|
||||
(9, "privilegeWithdrawn"),
|
||||
(10, "aACompromise"),
|
||||
],
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"inactive_from",
|
||||
models.DateTimeField(
|
||||
help_text="Expired or Revocation date if revocation motivation is configured"
|
||||
),
|
||||
),
|
||||
(
|
||||
"revocation_motivation",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(0, "unspecified"),
|
||||
(1, "keyCompromise"),
|
||||
(2, "cACompromise"),
|
||||
(3, "affiliationChanged"),
|
||||
(4, "superseded"),
|
||||
(5, "cessationOfOperation"),
|
||||
(6, "certificateHold"),
|
||||
(8, "removeFromCRL"),
|
||||
(9, "privilegeWithdrawn"),
|
||||
(10, "aACompromise"),
|
||||
],
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"entity",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_entity.federationentityconfiguration",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Historical Key",
|
||||
"verbose_name_plural": "Federation Historical Keys",
|
||||
},
|
||||
),
|
||||
]
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 14:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [
|
||||
("spid_cie_oidc_entity", "0021_federationhistoricalkey"),
|
||||
("spid_cie_oidc_entity", "0022_federationhistoricalkey_jwk_and_more"),
|
||||
("spid_cie_oidc_entity", "0023_alter_federationhistoricalkey_jwk"),
|
||||
("spid_cie_oidc_entity", "0024_alter_federationhistoricalkey_kid"),
|
||||
("spid_cie_oidc_entity", "0025_alter_federationhistoricalkey_jwk"),
|
||||
("spid_cie_oidc_entity", "0026_alter_federationhistoricalkey_jwk"),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_entity",
|
||||
"0020_alter_federationentityconfiguration_jwks_core_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FederationHistoricalKey",
|
||||
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)),
|
||||
("kid", models.CharField(max_length=128)),
|
||||
(
|
||||
"inactive_from",
|
||||
models.DateTimeField(
|
||||
help_text="Expired or Revocation date if revocation motivation is configured"
|
||||
),
|
||||
),
|
||||
(
|
||||
"revocation_motivation",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(0, "unspecified"),
|
||||
(1, "keyCompromise"),
|
||||
(2, "cACompromise"),
|
||||
(3, "affiliationChanged"),
|
||||
(4, "superseded"),
|
||||
(5, "cessationOfOperation"),
|
||||
(6, "certificateHold"),
|
||||
(8, "removeFromCRL"),
|
||||
(9, "privilegeWithdrawn"),
|
||||
(10, "aACompromise"),
|
||||
],
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
(
|
||||
"entity",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="spid_cie_oidc_entity.federationentityconfiguration",
|
||||
),
|
||||
),
|
||||
("jwk", models.JSONField(default=dict, help_text="private jwk")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Federation Historical Key",
|
||||
"verbose_name_plural": "Federation Historical Keys",
|
||||
},
|
||||
),
|
||||
]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 13:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0021_federationhistoricalkey"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="jwk",
|
||||
field=models.TextField(default={}, help_text="private jwk"),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="kid",
|
||||
field=models.CharField(max_length=33),
|
||||
),
|
||||
]
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 13:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0022_federationhistoricalkey_jwk_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="jwk",
|
||||
field=models.TextField(default={}, help_text="private jwk"),
|
||||
),
|
||||
]
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 13:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0023_alter_federationhistoricalkey_jwk"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="kid",
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
]
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 14:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0024_alter_federationhistoricalkey_kid"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="jwk",
|
||||
field=models.JSONField(default={}, help_text="private jwk"),
|
||||
),
|
||||
]
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 14:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("spid_cie_oidc_entity", "0025_alter_federationhistoricalkey_jwk"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="jwk",
|
||||
field=models.JSONField(default=dict, help_text="private jwk"),
|
||||
),
|
||||
]
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 4.2.1 on 2023-07-08 14:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_entity",
|
||||
"0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationhistoricalkey",
|
||||
name="revocation_motivation",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("0", "unspecified"),
|
||||
("1", "keyCompromise"),
|
||||
("2", "cACompromise"),
|
||||
("3", "affiliationChanged"),
|
||||
("4", "superseded"),
|
||||
("5", "cessationOfOperation"),
|
||||
("6", "certificateHold"),
|
||||
("8", "removeFromCRL"),
|
||||
("9", "privilegeWithdrawn"),
|
||||
("10", "aACompromise"),
|
||||
],
|
||||
max_length=33,
|
||||
),
|
||||
),
|
||||
]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# 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_entity",
|
||||
"0027_alter_federationhistoricalkey_revocation_motivation",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="issue_x509",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="If TA/Intermediate: issues x509 in both entity statements and configuration. if leaf, issues x509 certificates in configuration.",
|
||||
),
|
||||
),
|
||||
]
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# 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_entity", "0028_federationentityconfiguration_issue_x509"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="entity_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="federationentityconfiguration",
|
||||
name="issue_x509",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Issues x509 certificates in the Entity Configuration.",
|
||||
),
|
||||
),
|
||||
]
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# 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_entity",
|
||||
"0029_alter_federationentityconfiguration_entity_type_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="issue_x509",
|
||||
),
|
||||
]
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# 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_entity",
|
||||
"0030_remove_federationentityconfiguration_issue_x509",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="federationentityconfiguration",
|
||||
name="entity_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,
|
||||
),
|
||||
),
|
||||
]
|
||||
vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0032_alter_fetchedentitystatement_jwt.py
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.3 on 2023-12-13 14:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
(
|
||||
"spid_cie_oidc_entity",
|
||||
"0031_alter_federationentityconfiguration_entity_type",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="fetchedentitystatement",
|
||||
name="jwt",
|
||||
field=models.TextField(blank=False, null=False),
|
||||
),
|
||||
]
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# 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_entity', '0032_alter_fetchedentitystatement_jwt'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='federationentityconfiguration',
|
||||
name='entity_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),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,550 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Union
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from spid_cie_oidc.entity.abstract_models import TimeStampedModel
|
||||
from cryptojwt.jwk.jwk import key_from_jwk_dict
|
||||
from spid_cie_oidc.entity.jwks import (
|
||||
create_jwk,
|
||||
private_pem_from_jwk,
|
||||
public_pem_from_jwk,
|
||||
)
|
||||
from spid_cie_oidc.entity.jwtse import create_jws
|
||||
from spid_cie_oidc.entity.settings import (
|
||||
ENTITY_STATUS,
|
||||
ENTITY_TYPE_LEAFS,
|
||||
ENTITY_TYPES,
|
||||
FEDERATION_DEFAULT_EXP
|
||||
)
|
||||
from spid_cie_oidc.entity.statements import EntityConfiguration
|
||||
from spid_cie_oidc.entity.utils import exp_from_now, iat_now, random_token
|
||||
from spid_cie_oidc.entity.validators import (
|
||||
validate_entity_metadata,
|
||||
validate_metadata_algs,
|
||||
validate_private_jwks
|
||||
)
|
||||
|
||||
from .jwk_x5c import update_jwks_with_x5c
|
||||
from .jwks import public_jwk_from_private_jwk
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_leaf(statement_metadata):
|
||||
for _typ in ENTITY_TYPE_LEAFS:
|
||||
if _typ in statement_metadata:
|
||||
return True # pragma: no cover
|
||||
|
||||
|
||||
class FederationEntityConfiguration(TimeStampedModel):
|
||||
"""
|
||||
Federation Authority configuration.
|
||||
"""
|
||||
|
||||
def _create_jwks():
|
||||
return [create_jwk()]
|
||||
|
||||
uuid = models.UUIDField(
|
||||
blank=False, null=False, default=uuid.uuid4, unique=True, editable=False
|
||||
)
|
||||
sub = models.URLField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_(
|
||||
"URL that identifies this Entity in the Federation. "
|
||||
"This value and iss are the same in the Entity Configuration."
|
||||
),
|
||||
)
|
||||
default_exp = models.PositiveIntegerField(
|
||||
default=FEDERATION_DEFAULT_EXP,
|
||||
help_text=_("how many minutes from now() an issued statement must expire"),
|
||||
)
|
||||
default_signature_alg = models.CharField(
|
||||
max_length=16,
|
||||
default=(
|
||||
"ES256" if (
|
||||
hasattr(settings, "PRIVATE_KEY_TYPE")
|
||||
and getattr(settings, "PRIVATE_KEY_TYPE") == "EC"
|
||||
) else "RS256"
|
||||
),
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_("default signature algorithm, eg: RS256"),
|
||||
)
|
||||
authority_hints = models.JSONField(
|
||||
blank=True,
|
||||
null=False,
|
||||
help_text=_("only required if this Entity is an intermediary or leaf."),
|
||||
default=list,
|
||||
)
|
||||
jwks_fed = models.JSONField(
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_("a list of private keys for Federation ops"),
|
||||
default=_create_jwks,
|
||||
validators = [validate_private_jwks],
|
||||
)
|
||||
jwks_core = models.JSONField(
|
||||
blank=True,
|
||||
null=False,
|
||||
help_text=_("a list of private keys for Core ops"),
|
||||
default=_create_jwks,
|
||||
validators = [validate_private_jwks],
|
||||
)
|
||||
trust_marks = models.JSONField(
|
||||
blank=True,
|
||||
help_text=_("which trust marks MUST be exposed in its entity configuration"),
|
||||
default=list,
|
||||
)
|
||||
trust_mark_issuers = models.JSONField(
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Only usable for Trust Anchors and intermediates. "
|
||||
"Which issuers are allowed to issue trust marks for the descendants. "
|
||||
'Example: {"https://www.spid.gov.it/certification/rp": '
|
||||
'["https://registry.spid.agid.gov.it", "https://intermediary.spid.it"],'
|
||||
'"https://sgd.aa.it/onboarding": ["https://sgd.aa.it", ]}'
|
||||
),
|
||||
default=dict,
|
||||
)
|
||||
entity_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"),
|
||||
)
|
||||
metadata = models.JSONField(
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_(
|
||||
"federation_entity metadata, eg: "
|
||||
'{"federation_entity": { ... },'
|
||||
'"openid_provider": { ... },'
|
||||
'"openid_relying_party": { ... },'
|
||||
'"oauth_resource": { ... }'
|
||||
"}"
|
||||
),
|
||||
default=dict,
|
||||
validators=[
|
||||
validate_entity_metadata,
|
||||
validate_metadata_algs
|
||||
],
|
||||
)
|
||||
constraints = models.JSONField(
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"""
|
||||
{
|
||||
"naming_constraints": {
|
||||
"permitted": [
|
||||
"https://.example.com"
|
||||
],
|
||||
"excluded": [
|
||||
"https://east.example.com"
|
||||
]
|
||||
},
|
||||
"max_path_length": 2
|
||||
}
|
||||
"""
|
||||
),
|
||||
default=dict,
|
||||
# TODO
|
||||
# validators=[validate_entity_metadata,]
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"If this configuration is active. "
|
||||
"At least one configuration must be active"
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Federation Entity Configuration"
|
||||
verbose_name_plural = "Federation Entity Configurations"
|
||||
|
||||
@classmethod
|
||||
def get_active_conf(cls):
|
||||
"""
|
||||
returns the first available active acsia engine configuration found
|
||||
"""
|
||||
return cls.objects.filter(is_active=True).first()
|
||||
|
||||
@property
|
||||
def public_jwks(self):
|
||||
"""
|
||||
Export public JWKs from private jwks_fed.
|
||||
Uses public_jwk_from_private_jwk to avoid SerializationNotPossible
|
||||
when serialize_rsa_key+RSAKey.thumbprint() fails with cryptojwt.
|
||||
"""
|
||||
res = []
|
||||
for i in self.jwks_fed:
|
||||
skey = public_jwk_from_private_jwk(i)
|
||||
res.append(skey)
|
||||
return res
|
||||
|
||||
@property
|
||||
def pems_as_dict(self):
|
||||
res = {}
|
||||
for i in self.jwks_fed:
|
||||
res[i["kid"]] = {
|
||||
"private": private_pem_from_jwk(i),
|
||||
"public": public_pem_from_jwk(i),
|
||||
}
|
||||
return res
|
||||
|
||||
@property
|
||||
def pems_as_json(self):
|
||||
return json.dumps(self.pems_as_dict, indent=2)
|
||||
|
||||
@property
|
||||
def kids(self) -> list:
|
||||
return [i["kid"] for i in self.jwks_fed]
|
||||
|
||||
@property
|
||||
def type(self) -> list:
|
||||
return [i for i in self.metadata.keys()]
|
||||
|
||||
@property
|
||||
def is_leaf(self):
|
||||
return is_leaf(self.metadata)
|
||||
|
||||
@property
|
||||
def entity_configuration_as_dict(self):
|
||||
conf = {
|
||||
"exp": exp_from_now(self.default_exp),
|
||||
"iat": iat_now(),
|
||||
"iss": self.sub,
|
||||
"sub": self.sub,
|
||||
"jwks": {"keys": self.public_jwks},
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
if self.trust_mark_issuers:
|
||||
conf["trust_mark_issuers"] = self.trust_mark_issuers
|
||||
|
||||
if self.trust_marks:
|
||||
conf["trust_marks"] = self.trust_marks
|
||||
|
||||
if self.constraints:
|
||||
conf["constraints"] = self.constraints
|
||||
|
||||
if self.authority_hints:
|
||||
conf["authority_hints"] = self.authority_hints
|
||||
elif self.is_leaf: # pragma: no cover
|
||||
_msg = f"Entity {self.sub} is a leaf and requires authority_hints valued"
|
||||
logger.error(_msg)
|
||||
|
||||
if hasattr(settings, "X509_COMMON_NAME"):
|
||||
conf['jwks'] = {"keys": update_jwks_with_x5c(
|
||||
jwks=self.public_jwks,
|
||||
private_key=key_from_jwk_dict(self.jwks_fed[0]).private_key(),
|
||||
subject=self.sub,
|
||||
is_ca_or_int=True
|
||||
)}
|
||||
|
||||
return conf
|
||||
|
||||
@property
|
||||
def entity_configuration_as_json(self):
|
||||
return json.dumps(self.entity_configuration_as_dict)
|
||||
|
||||
@property
|
||||
def entity_configuration_as_jws(self, **kwargs):
|
||||
return create_jws(
|
||||
self.entity_configuration_as_dict,
|
||||
self.jwks_fed[0],
|
||||
alg=self.default_signature_alg,
|
||||
typ="entity-statement+jwt",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def fetch_endpoint(self) -> Union[str, None]:
|
||||
metadata = self.entity_configuration_as_dict.get('metadata', {})
|
||||
if 'federation_entity' in metadata:
|
||||
return metadata['federation_entity'].get("federation_fetch_endpoint", None)
|
||||
|
||||
def set_jwks_as_array(self):
|
||||
for i in ('jwks_fed','jwks_core'):
|
||||
value = getattr(self, i)
|
||||
if not isinstance(value, list):
|
||||
setattr(self, i, [value])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.set_jwks_as_array()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return "{} [{}]".format(
|
||||
self.sub, "active" if self.is_active else "--"
|
||||
)
|
||||
|
||||
|
||||
class FetchedEntityStatement(TimeStampedModel):
|
||||
"""
|
||||
Entity Statement acquired by a third party
|
||||
"""
|
||||
|
||||
iss = models.URLField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
help_text=_(
|
||||
"URL that identifies the issuer of this statement in the Federation. "
|
||||
),
|
||||
)
|
||||
sub = models.URLField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
help_text=_("URL that identifies this Entity in the Federation. "),
|
||||
)
|
||||
exp = models.DateTimeField()
|
||||
iat = models.DateTimeField()
|
||||
|
||||
statement = models.JSONField(
|
||||
blank=False, null=False, help_text=_("Entity statement"), default=dict
|
||||
)
|
||||
jwt = models.TextField(null=False, blank=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Fetched Entity Statement"
|
||||
verbose_name_plural = "Fetched Entity Statement"
|
||||
|
||||
def get_entity_configuration_as_obj(self):
|
||||
return EntityConfiguration(self.jwt)
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.exp <= timezone.localtime()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.sub} issued by {self.iss}"
|
||||
|
||||
|
||||
class TrustChain(TimeStampedModel):
|
||||
"""
|
||||
Federation Trust Chain
|
||||
"""
|
||||
|
||||
sub = models.URLField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
help_text=_("URL that identifies this Entity in the Federation. "),
|
||||
)
|
||||
trust_anchor = models.ForeignKey(FetchedEntityStatement, on_delete=models.CASCADE)
|
||||
exp = models.DateTimeField()
|
||||
iat = models.DateTimeField(auto_now_add=True)
|
||||
chain = models.JSONField(
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"A list of entity statements collected during the metadata discovery"
|
||||
),
|
||||
default=list,
|
||||
)
|
||||
jwks = models.JSONField(
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text=_("jwks of this federation entity")
|
||||
)
|
||||
metadata = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_(
|
||||
"The final metadata applied with the metadata policy built over the chain"
|
||||
),
|
||||
default=dict,
|
||||
)
|
||||
trust_marks = models.JSONField(
|
||||
blank=True, help_text=_("verified trust marks"), default=list
|
||||
)
|
||||
parties_involved = models.JSONField(
|
||||
blank=True,
|
||||
help_text=_("subjects involved in the metadata discovery"),
|
||||
default=list,
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=33,
|
||||
default="unreachable",
|
||||
help_text=_("Status of this trust chain, on each update."),
|
||||
choices=[(i, i) for i in list(ENTITY_STATUS.keys())],
|
||||
)
|
||||
log = models.TextField(blank=True, help_text=_("status log"), default="")
|
||||
processing_start = models.DateTimeField(
|
||||
help_text=_(
|
||||
"When the metadata discovery started for this Trust Chain. "
|
||||
"It should prevent concurrent processing for the same sub/type."
|
||||
),
|
||||
default=timezone.localtime,
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("If you need to disable the trust to this subject, uncheck this"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Trust Chain"
|
||||
verbose_name_plural = "Trust Chains"
|
||||
unique_together = ("sub", "trust_anchor")
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return self.sub # pragma: no cover
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.exp <= timezone.localtime()
|
||||
|
||||
@property
|
||||
def iat_as_timestamp(self):
|
||||
return int(self.iat.timestamp())
|
||||
|
||||
@property
|
||||
def exp_as_timestamp(self):
|
||||
return int(self.exp.timestamp())
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.is_active and ENTITY_STATUS[self.status]
|
||||
|
||||
def __str__(self):
|
||||
return "{} [{}] [{}]".format(
|
||||
self.sub, self.trust_anchor, self.is_valid
|
||||
)
|
||||
|
||||
|
||||
class StaffToken(TimeStampedModel):
|
||||
"""
|
||||
Token provisioned to staffs operators for protected resources
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
get_user_model(),
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_("The user responsible of thi token"),
|
||||
)
|
||||
token = models.CharField(
|
||||
max_length=255,
|
||||
blank=False,
|
||||
null=False,
|
||||
default = random_token,
|
||||
help_text=_("it will be generated automatically."),
|
||||
)
|
||||
expire_at = models.DateTimeField(blank=True, null=True)
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
blank=False,
|
||||
null=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Staff Token"
|
||||
verbose_name_plural = "Staff Tokens"
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
if self.is_active and not self.expire_at:
|
||||
return True
|
||||
elif self.is_active and self.expire_at > timezone.localtime():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} {self.is_active}"
|
||||
|
||||
|
||||
class FederationHistoricalKey(TimeStampedModel):
|
||||
"""
|
||||
https://openid.net/specs/openid-connect-federation-1_0.html#name-federation-historical-keys-
|
||||
"""
|
||||
|
||||
REVOCATION_REASONS_MAP = (
|
||||
("0", 'unspecified'),
|
||||
("1", 'keyCompromise'),
|
||||
("2", 'cACompromise'),
|
||||
("3", 'affiliationChanged'),
|
||||
("4", 'superseded'),
|
||||
("5", 'cessationOfOperation'),
|
||||
("6", 'certificateHold'),
|
||||
# 7 is unused in rfc5280
|
||||
("8", 'removeFromCRL'),
|
||||
("9", 'privilegeWithdrawn'),
|
||||
("10", 'aACompromise')
|
||||
)
|
||||
|
||||
# REVOCATION_REASONS_CODES = {k:k for k in REVOCATION_REASONS_MAP.keys()}
|
||||
|
||||
entity = models.ForeignKey(FederationEntityConfiguration, on_delete=models.CASCADE)
|
||||
kid = models.CharField(
|
||||
blank=False, null=False, max_length=128
|
||||
)
|
||||
inactive_from = models.DateTimeField(help_text=_(
|
||||
"Expired or Revocation date if revocation motivation is configured"
|
||||
))
|
||||
revocation_motivation = models.CharField(
|
||||
blank=True, null=False, max_length=33, choices=REVOCATION_REASONS_MAP
|
||||
)
|
||||
|
||||
jwk = models.JSONField(help_text=_("private jwk"), default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Federation Historical Key"
|
||||
verbose_name_plural = "Federation Historical Keys"
|
||||
|
||||
@property
|
||||
def as_dict(self):
|
||||
if not self.jwk:
|
||||
return {}
|
||||
|
||||
kdict = public_jwk_from_private_jwk(self.jwk)
|
||||
if self.revocation_motivation:
|
||||
kdict["revoked"] = {
|
||||
"revoked_at": int(self.inactive_from.timestamp()),
|
||||
"reason": dict(self.REVOCATION_REASONS_MAP)[self.revocation_motivation],
|
||||
"reason_code": self.revocation_motivation
|
||||
}
|
||||
else:
|
||||
kdict["exp"] = int(self.inactive_from.timestamp())
|
||||
return kdict
|
||||
|
||||
@property
|
||||
def as_json(self):
|
||||
return json.dumps(self.as_dict)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.kid:
|
||||
for jwk in self.entity.jwks_fed:
|
||||
if not jwk.get('kid', None):
|
||||
continue
|
||||
elif self.kid == jwk['kid']:
|
||||
_indx = self.entity.jwks_fed.index(jwk)
|
||||
self.jwk = self.entity.jwks_fed.pop(_indx)
|
||||
self.entity.save()
|
||||
break
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.entity} {self.kid}"
|
||||
|
||||
|
||||
def get_first_self_trust_anchor(
|
||||
sub: str = None,
|
||||
) -> Union[FederationEntityConfiguration, None]:
|
||||
"""
|
||||
get the first available Trust Anchor that represent self
|
||||
as a qualified issuer
|
||||
"""
|
||||
lk = dict(metadata__federation_entity__isnull=False, is_active=True)
|
||||
if sub:
|
||||
lk["sub"] = sub
|
||||
return FederationEntityConfiguration.objects.filter(**lk).first()
|
||||
@@ -0,0 +1,355 @@
|
||||
__author__ = "Roland Hedberg"
|
||||
__license__ = "Apache 2.0"
|
||||
__version__ = ""
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PolicyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def combine_subset_of(s1, s2): # pragma: no cover
|
||||
return list(set(s1).intersection(set(s2)))
|
||||
|
||||
|
||||
def combine_superset_of(s1, s2): # pragma: no cover
|
||||
return list(set(s1).intersection(set(s2)))
|
||||
|
||||
|
||||
def combine_one_of(s1, s2): # pragma: no cover
|
||||
return list(set(s1).intersection(set(s2)))
|
||||
|
||||
|
||||
def combine_add(s1, s2): # pragma: no cover
|
||||
if isinstance(s1, list):
|
||||
set1 = set(s1)
|
||||
else:
|
||||
set1 = {s1}
|
||||
if isinstance(s2, list):
|
||||
set2 = set(s2)
|
||||
else:
|
||||
set2 = {s2}
|
||||
return list(set1.union(set2))
|
||||
|
||||
|
||||
POLICY_FUNCTIONS = {
|
||||
"subset_of",
|
||||
"superset_of",
|
||||
"one_of",
|
||||
"add",
|
||||
"value",
|
||||
"default",
|
||||
"essential",
|
||||
}
|
||||
|
||||
OP2FUNC = {
|
||||
"subset_of": combine_subset_of,
|
||||
"superset_of": combine_superset_of,
|
||||
"one_of": combine_one_of,
|
||||
"add": combine_add,
|
||||
}
|
||||
|
||||
|
||||
def do_sub_one_super_add(superior, child, policy): # pragma: no cover
|
||||
if policy in superior and policy in child:
|
||||
comb = OP2FUNC[policy](superior[policy], child[policy])
|
||||
if comb:
|
||||
return comb
|
||||
else:
|
||||
raise PolicyError("Value sets doesn't overlap")
|
||||
elif policy in superior:
|
||||
return superior[policy]
|
||||
elif policy in child:
|
||||
return child[policy]
|
||||
|
||||
|
||||
def do_value(superior, child, policy): # pragma: no cover
|
||||
if policy in superior and policy in child:
|
||||
if superior[policy] == child[policy]:
|
||||
return superior[policy]
|
||||
else:
|
||||
raise PolicyError("Not allowed to combine values")
|
||||
elif policy in superior:
|
||||
return superior[policy]
|
||||
elif policy in child:
|
||||
return child[policy]
|
||||
|
||||
|
||||
def do_default(superior, child, policy): # pragma: no cover
|
||||
# A child's default can not override a superiors
|
||||
if policy in superior and policy in child:
|
||||
if superior["default"] == child["default"]:
|
||||
return superior["default"]
|
||||
else:
|
||||
raise PolicyError("Not allowed to change default")
|
||||
elif policy in superior:
|
||||
return superior[policy]
|
||||
elif policy in child:
|
||||
return child[policy]
|
||||
|
||||
|
||||
def do_essential(superior, child, policy): # pragma: no cover
|
||||
# essential: an child can make it True if a superior has states False
|
||||
# but not the other way around
|
||||
|
||||
if policy in superior and policy in child:
|
||||
if not superior[policy] and child["essential"]:
|
||||
return True
|
||||
else:
|
||||
return superior[policy]
|
||||
elif policy in superior:
|
||||
return superior[policy]
|
||||
elif policy in child: # Not in superior is the same as essential=True
|
||||
return True
|
||||
|
||||
|
||||
DO_POLICY = {
|
||||
"superset_of": do_sub_one_super_add,
|
||||
"subset_of": do_sub_one_super_add,
|
||||
"one_of": do_sub_one_super_add,
|
||||
"add": do_sub_one_super_add,
|
||||
"value": do_value,
|
||||
"default": do_default,
|
||||
"essential": do_essential,
|
||||
}
|
||||
|
||||
|
||||
def combine_claim_policy(superior, child): # noqa: C901 # pragma: no cover
|
||||
"""
|
||||
Combine policy rules.
|
||||
Applying the child policy can only make the combined policy more restrictive.
|
||||
:param superior: Superior policy
|
||||
:param child: Intermediates policy
|
||||
"""
|
||||
# weed out everything I don't recognize
|
||||
|
||||
superior_set = set(superior).intersection(POLICY_FUNCTIONS)
|
||||
child_set = set(child).intersection(POLICY_FUNCTIONS)
|
||||
if "value" in superior_set: # An exact value can not be restricted.
|
||||
if child_set:
|
||||
if "essential" in child_set:
|
||||
if len(child_set) == 1:
|
||||
return {"value": superior["value"], "essential": child["essential"]}
|
||||
else:
|
||||
raise PolicyError(
|
||||
"value can only be combined with essential, not {}".format(
|
||||
child_set
|
||||
)
|
||||
)
|
||||
elif "value" in child_set:
|
||||
if child["value"] != superior["value"]: # Not OK
|
||||
raise PolicyError("Child can not set another value then superior")
|
||||
else:
|
||||
return superior
|
||||
else:
|
||||
raise PolicyError(
|
||||
"Not allowed combination of policies: {} + {}".format(
|
||||
superior, child
|
||||
)
|
||||
)
|
||||
return superior
|
||||
else:
|
||||
if "essential" in superior_set and "essential" in child_set:
|
||||
# can only go from False to True
|
||||
if (
|
||||
superior["essential"] != child["essential"]
|
||||
and child["essential"] is False
|
||||
):
|
||||
raise PolicyError("Essential can not go from True to False")
|
||||
|
||||
comb_policy = superior_set.union(child_set)
|
||||
if "one_of" in comb_policy:
|
||||
if "subset_of" in comb_policy or "superset_of" in comb_policy:
|
||||
raise PolicyError(
|
||||
"one_of can not be combined with subset_of/superset_of"
|
||||
)
|
||||
|
||||
rule = {}
|
||||
for policy in comb_policy:
|
||||
rule[policy] = DO_POLICY[policy](superior, child, policy)
|
||||
|
||||
if comb_policy == {"superset_of", "subset_of"}:
|
||||
# make sure the subset_of is a superset of superset_of.
|
||||
if set(rule["superset_of"]).difference(set(rule["subset_of"])):
|
||||
raise PolicyError("superset_of not a super set of subset_of")
|
||||
elif comb_policy == {"superset_of", "subset_of", "default"}:
|
||||
# make sure the subset_of is a superset of superset_of.
|
||||
if set(rule["superset_of"]).difference(set(rule["subset_of"])):
|
||||
raise PolicyError("superset_of not a super set of subset_of")
|
||||
if set(rule["default"]).difference(set(rule["subset_of"])):
|
||||
raise PolicyError("default not a sub set of subset_of")
|
||||
if set(rule["superset_of"]).difference(set(rule["default"])):
|
||||
raise PolicyError("default not a super set of subset_of")
|
||||
elif comb_policy == {"subset_of", "default"}:
|
||||
if set(rule["default"]).difference(set(rule["subset_of"])):
|
||||
raise PolicyError("default not a sub set of subset_of")
|
||||
elif comb_policy == {"superset_of", "default"}:
|
||||
if set(rule["superset_of"]).difference(set(rule["default"])):
|
||||
raise PolicyError("default not a super set of subset_of")
|
||||
elif comb_policy == {"one_of", "default"}:
|
||||
if isinstance(rule["default"], list):
|
||||
if set(rule["default"]).difference(set(rule["one_of"])):
|
||||
raise PolicyError("default not a super set of one_of")
|
||||
else:
|
||||
if {rule["default"]}.difference(set(rule["one_of"])):
|
||||
raise PolicyError("default not a super set of one_of")
|
||||
return rule
|
||||
|
||||
|
||||
def combine_policy(superior, child):
|
||||
res = {}
|
||||
sup_set = set(superior.keys())
|
||||
chi_set = set(child.keys())
|
||||
|
||||
for claim in set(sup_set).intersection(chi_set):
|
||||
res[claim] = combine_claim_policy(superior[claim], child[claim])
|
||||
|
||||
for claim in sup_set.difference(chi_set):
|
||||
res[claim] = superior[claim]
|
||||
|
||||
for claim in chi_set.difference(sup_set):
|
||||
res[claim] = child[claim]
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def gather_policies(chain, entity_type):
|
||||
"""
|
||||
Gather and combine all the metadata policies that are defined in the trust chain
|
||||
:param chain: A list of Entity Statements
|
||||
:return: The combined metadata policy
|
||||
"""
|
||||
|
||||
try:
|
||||
combined_policy = chain[0]["metadata_policy"][entity_type]
|
||||
except KeyError:
|
||||
combined_policy = {}
|
||||
|
||||
for es in chain[1:]:
|
||||
try:
|
||||
child = es["metadata_policy"][entity_type]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
combined_policy = combine_policy(combined_policy, child)
|
||||
|
||||
return combined_policy
|
||||
|
||||
|
||||
def union(val1, val2):
|
||||
if isinstance(val1, list):
|
||||
base = set(val1)
|
||||
else:
|
||||
base = {val1}
|
||||
|
||||
if isinstance(val2, list):
|
||||
ext = set(val2)
|
||||
else:
|
||||
ext = {val2}
|
||||
return base.union(ext)
|
||||
|
||||
|
||||
def apply_policy(metadata, policy):
|
||||
"""
|
||||
Apply a metadata policy to a metadata statement.
|
||||
The order is value, add, default and then the checks subset_of/superset_of and one_of
|
||||
:param metadata: A metadata statement
|
||||
:param policy: A metadata policy
|
||||
:return: A metadata statement that adheres to a metadata policy
|
||||
"""
|
||||
metadata_set = set(metadata.keys())
|
||||
policy_set = set(policy.keys())
|
||||
|
||||
# Metadata claims that there exists a policy for
|
||||
for claim in metadata_set.intersection(policy_set):
|
||||
if "value" in policy[claim]: # value overrides everything
|
||||
metadata[claim] = policy[claim]["value"]
|
||||
else:
|
||||
if "one_of" in policy[claim]:
|
||||
# The is for claims that can have only one value
|
||||
if isinstance(metadata[claim], list): # Should not be but ...
|
||||
_claim = [
|
||||
c for c in metadata[claim] if c in policy[claim]["one_of"]
|
||||
]
|
||||
if _claim:
|
||||
metadata[claim] = _claim[0]
|
||||
else:
|
||||
raise PolicyError(
|
||||
"{}: None of {} among {}".format(
|
||||
claim, metadata[claim], policy[claim]["one_of"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
if metadata[claim] in policy[claim]["one_of"]:
|
||||
pass
|
||||
else:
|
||||
raise PolicyError(
|
||||
"{} not among {}".format(
|
||||
metadata[claim], policy[claim]["one_of"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
# The following is for claims that can have lists of values
|
||||
if "add" in policy[claim]:
|
||||
metadata[claim] = list(union(metadata[claim], policy[claim]["add"]))
|
||||
|
||||
if "subset_of" in policy[claim]:
|
||||
_val = set(policy[claim]["subset_of"]).intersection(
|
||||
set(metadata[claim])
|
||||
)
|
||||
if _val:
|
||||
metadata[claim] = list(_val)
|
||||
else:
|
||||
raise PolicyError(
|
||||
"{} not subset of {}".format(
|
||||
metadata[claim], policy[claim]["subset_of"]
|
||||
)
|
||||
)
|
||||
if "superset_of" in policy[claim]:
|
||||
if set(policy[claim]["superset_of"]).difference(
|
||||
set(metadata[claim])
|
||||
):
|
||||
raise PolicyError(
|
||||
"{} not superset of {}".format(
|
||||
metadata[claim], policy[claim]["superset_of"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
pass
|
||||
|
||||
# In policy but not in metadata
|
||||
for claim in policy_set.difference(metadata_set):
|
||||
if "value" in policy[claim]:
|
||||
metadata[claim] = policy[claim]["value"]
|
||||
elif "add" in policy[claim]:
|
||||
metadata[claim] = policy[claim]["add"]
|
||||
elif "default" in policy[claim]:
|
||||
metadata[claim] = policy[claim]["default"]
|
||||
|
||||
if claim not in metadata:
|
||||
if "essential" in policy[claim] and policy[claim]["essential"]:
|
||||
raise PolicyError("Essential claim '{}' missing".format(claim))
|
||||
|
||||
# All that are in metadata but not in policy should just remain
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def diff2policy(new, old):
|
||||
res = {}
|
||||
for claim in set(new).intersection(set(old)):
|
||||
if new[claim] == old[claim]:
|
||||
continue
|
||||
else:
|
||||
res[claim] = {"value": new[claim]}
|
||||
|
||||
for claim in set(new).difference(set(old)):
|
||||
if claim in ["contacts"]:
|
||||
res[claim] = {"add": new[claim]}
|
||||
else:
|
||||
res[claim] = {"value": new[claim]}
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, HttpUrl, EmailStr
|
||||
|
||||
|
||||
class FAMetadata(BaseModel):
|
||||
contacts: List[EmailStr]
|
||||
federation_fetch_endpoint: HttpUrl
|
||||
federation_resolve_endpoint: HttpUrl
|
||||
federation_list_endpoint: HttpUrl
|
||||
homepage_uri: HttpUrl
|
||||
organization_name: str
|
||||
@@ -0,0 +1,72 @@
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
||||
class Jwk(BaseModel):
|
||||
kid: str # Base64url-encoded thumbprint string
|
||||
kty: Literal["EC", "RSA"]
|
||||
# TODO: verify if is optional
|
||||
alg: Optional[
|
||||
Literal[
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"RSA-OAEP",
|
||||
"RSA-OAEP-256",
|
||||
"ECDH-ES",
|
||||
"ECDH-ES+A128KW",
|
||||
"ECDH-ES+A256KW"
|
||||
]
|
||||
]
|
||||
use: Optional[Literal["sig", "enc"]]
|
||||
n: Optional[str] # Base64urlUInt-encoded
|
||||
e: Optional[str] # Base64urlUInt-encoded
|
||||
|
||||
def check_value_for_rsa(value, name, values):
|
||||
if "EC" == values.get("kty") and value:
|
||||
raise ValueError(f"{name} must be present only for kty = RSA")
|
||||
|
||||
def check_value_for_ec(value, name, values):
|
||||
if "RSA" == values.get("kty") and value:
|
||||
raise ValueError(f"{name} must be present only for kty = EC")
|
||||
|
||||
@validator("n")
|
||||
def validate_n(cls, n_value, values):
|
||||
cls.check_value_for_rsa(n_value, "n", values)
|
||||
|
||||
@validator("e")
|
||||
def validate_e(cls, e_value, values):
|
||||
cls.check_value_for_rsa(e_value, "e", values)
|
||||
|
||||
|
||||
class JwkCie(Jwk):
|
||||
x: Optional[str] # Base64url-encoded
|
||||
y: Optional[str] # Base64url-encoded
|
||||
crv: Optional[Literal["P-256", "P-384", "P-521"]]
|
||||
|
||||
@validator("x")
|
||||
def validate_x(cls, x_value, values):
|
||||
cls.check_value_for_ec(x_value, "x", values)
|
||||
|
||||
@validator("y")
|
||||
def validate_y(cls, y_value, values):
|
||||
cls.check_value_for_ec(y_value, "y", values)
|
||||
|
||||
@validator("crv")
|
||||
def validate_crv(cls, crv_value, values):
|
||||
cls.check_value_for_ec(crv_value, "crv", values)
|
||||
|
||||
|
||||
class JwksCie(BaseModel):
|
||||
keys: List[JwkCie]
|
||||
|
||||
|
||||
class JwksSpid(BaseModel):
|
||||
keys: List[Jwk]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user