Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/114 add django setup configuration #115

Merged
merged 16 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ jobs:
python: ['3.10', '3.11', '3.12']
django: ['4.2']
mozilla_django_oidc: ['4.0']
setup_config_enabled: ['no', 'yes']
sergei-maertens marked this conversation as resolved.
Show resolved Hide resolved

name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }}, mozilla-django-oidc ${{ matrix.mozilla_django_oidc }})
name: "Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }},
mozilla-django-oidc ${{ matrix.mozilla_django_oidc }}, Setup Config: ${{ matrix.setup_config_enabled }}))"

services:
postgres:
Expand All @@ -41,18 +43,21 @@ jobs:
run: pip install tox tox-gh-actions

- name: Run tests
run: tox
run: |
tox -- ${{ matrix.setup_config_enabled != 'yes' && '--ignore tests/setupconfig' || '' }}
env:
PYTHON_VERSION: ${{ matrix.python }}
DJANGO: ${{ matrix.django }}
MOZILLA_DJANGO_OIDC: ${{ matrix.mozilla_django_oidc }}
PGUSER: postgres
PGHOST: localhost
SETUP_CONFIG_ENABLED: ${{ matrix.setup_config_enabled }}

- name: Publish coverage report
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: ${{ matrix.setup_config_enabled == 'yes' && 'setupconfig' || 'base' }}

publish:
name: Publish package to PyPI
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,6 @@ venv.bak/

# mypy
.mypy_cache/

# Pycharfiles
.idea/
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
Changelog
=========

0.20.0 (????)
=============

New Features:

* Add optional support for `django-setup-configuration`_

.. _django-setup-configuration: https://pypi.org/project/django-setup-configuration/

0.19.0 (2024-07-02)
===================

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Using ``email`` as the unique identifier is not recommended, as mentioned in the

quickstart
customizing
setup_configuration
reference
architecture
changelog
Expand Down
8 changes: 8 additions & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ This will also install the following packages:
- ``django-solo``
- ``django-jsonform``

You can optionally install `django-setup-configuration`_ support with:

.. code-block:: bash

pip install mozilla-django-oidc-db[setup-configuration]

Django settings
---------------

Expand Down Expand Up @@ -267,3 +273,5 @@ and ``OIDCAuthenticationBackend.config_class`` to be this new class.
.. _mozilla-django-oidc settings documentation: https://mozilla-django-oidc.readthedocs.io/en/stable/settings.html

.. _OIDC spec: https://openid.net/specs/openid-connect-discovery-1_0.html#WellKnownRegistry

.. _django-setup-configuration: https://pypi.org/project/django-setup-configuration/
119 changes: 119 additions & 0 deletions docs/setup_configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
==========================
Django Setup Configuration
==========================

There is optional support for `django-setup-configuration`_ that allows you to automatically configure the
OpenID Connect configuration using that package's ``setup_configuration`` command.

You must install the ``setup-configuration`` dependency group:

.. _django-setup-configuration: https://pypi.org/project/django-setup-configuration/


.. code-block:: bash

pip install mozilla-django-oidc-db[setup-configuration]


You must then define the required and any optional django settings mentioned below and
put the ``AdminOIDCConfigurationStep`` in your django-setup-configuration steps:

.. code-block:: python

SETUP_CONFIGURATION_STEPS = [
...
"mozilla_django_oidc_db.setup_configuration.steps.AdminOIDCConfigurationStep",
...
]

Setup Configuration Settings:
=============================


The setup configuration source must contain the following base keys to use this setup configuration step (using ``yaml`` as an example):

* ``oidc_db_config_enable``: enable setup configuration step boolean

* ``oidc_db_config_admin_auth``: Dictionary that maps OIDC fields to their values.


Example: *setup_config.yml*

.. code-block:: YAML

other_enable: True
other_config:
...
oidc_db_config_enable: True
oidc_db_config_admin_auth:
oidc_rp_client_id: client-id
oidc_rp_client_secret: secret
endpoint_config:
oidc_op_discovery_endpoint: https://keycloak.local/protocol/openid-connect/
...

This is file is then used with the setup configuration command setup the OIDC admin:

.. code-block:: Bash

python manage.py setup_configuration --yaml-file path/to/setup_config.yml


Any field from the ``OpenIDConnectConfig`` can be added to ``oidc_db_config_admin_auth`` (except endpoints, see below)

Required Fields:
Coperh marked this conversation as resolved.
Show resolved Hide resolved
""""""""""""""""


* ``oidc_rp_client_id``: OpenID Connect client ID from the OIDC Provider.
* ``oidc_rp_client_secret``: OpenID Connect secret from the OIDC Provider.
* ``endpoint_config``: Dictionary containing endpoint information

* ``oidc_op_discovery_endpoint``: URL of your OpenID Connect provider discovery endpoint ending with a slash (`.well-known/...` will be added automatically).

**OR**

* ``oidc_op_authorization_endpoint``: URL of your OpenID Connect provider authorization endpoint
* ``oidc_op_token_endpoint``: URL of your OpenID Connect provider token endpoint
* ``oidc_op_user_endpoint``: URL of your OpenID Connect provider userinfo endpoint


The endpoints must be provided in the ``endpoint_config`` dictionary.
You can add the discovery endpoint to automatically fetch the other endpoints.
Otherwise the endpoints must be specified individually.
Providing both will cause the validation to fail.

Optional Fields:
""""""""""""""""
Comment on lines +86 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future - there seems to be a lot of duplication/repetition of model field help texts. It would be a lot nicer if we can just use Sphinx (custom directive) to extract the help text and default value from the model field definition.

RST:

* :model_field:`oidc_op_jwks_endpoint`

and then the python code for the directive could make use of:

>>> field = OIDCConfig._meta.get_field("oidc_op_jwks_endpoint") 
>>> field.help_text
...
>>> field.default
...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some attempt at this. Added it to a separate PR ( branched form this one) #117

We could create a common repo with sphinx roles/directives

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swrichards perhaps this could be an alternative to the generate documentation functionality?


.. warning::

Values that are not provided will use the default or empty value and will overwrite any setting changed in the admin.
Make sure settings that were manually changed in the admin are added to the configuration yaml.

All the following keys are placed in the ``oidc_db_config_admin_auth`` dictionary.

* ``enabled``: whether OIDC is enabled for admin login. Defaults to ``True``.
* ``oidc_op_jwks_endpoint``: URL of your OpenID Connect provider JSON Web Key Set endpoint.
Required if ``RS256`` is used as signing algorithm. No default value.
* ``claim_mapping``: Mapping from user-model fields to OIDC claims.
Defaults to ``{"email": ["email"], "first_name": ["given_name"], "last_name": ["family_name"]}``
* ``username_claim``: The name of the OIDC claim that is used as the username. Defaults to ``["sub"]``
* ``groups_claim``: The name of the OIDC claim that holds the values to map to local user groups. Defaults to ``["roles"]``
* ``default_groups``: The default groups to which every user logging in with OIDC will be assigned. No default values.
* ``superuser_group_names``: If any of these group names are present in the claims upon login, the user will be marked as a superuser.
If none of these groups are present the user will lose superuser permissions. Defaults to empty list.
* ``make_users_staff``: Users will be flagged as being a staff user automatically.
This allows users to login to the admin interface. Defaults to ``False``.
* ``oidc_use_nonce``: Controls whether the OpenID Connect client uses nonce verification. Defaults to ``True``.
* ``oidc_nonce_size``: Sets the length of the random string used for OpenID Connect nonce verification. Defaults to ``32``.
* ``oidc_state_size``: Sets the length of the random string used for OpenID Connect state verification. Defaults to ``32``.
* ``oidc_rp_idp_sign_key``: Key the Identity Provider uses to sign ID tokens in the case of an RSA sign algorithm.
Should be the signing key in PEM or DER format. No default.
* ``oidc_rp_scopes_list``: OpenID Connect scopes that are requested during login. Defaults to ``["openid", "email", "profile"]``.
* ``oidc_rp_sign_algo``: Algorithm the Identity Provider uses to sign ID tokens. Defaults to ``"HS256"``.
* ``sync_groups``: If checked, local user groups will be created for group names present in the groups claim,
if they do not exist yet locally. Defaults to ``True``.
* ``sync_groups_glob_pattern``: The glob pattern that groups must match to be synchronized to the local database. Defaults to ``"*"``.
* ``userinfo_claims_source``: Indicates the source from which the user information claims should be extracted
(``"userinfo_endpoint"`` or ``"id_token"``). Defaults to ``"userinfo_endpoint"``.
16 changes: 3 additions & 13 deletions mozilla_django_oidc_db/backends.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import fnmatch
import logging
from collections.abc import Collection
from typing import Any, TypeAlias, cast
Expand All @@ -25,7 +24,7 @@
from .jwt import verify_and_decode_token
from .models import OpenIDConnectConfigBase, UserInformationClaimsSources
from .typing import ClaimPath, JSONObject
from .utils import extract_content_type, obfuscate_claims
from .utils import extract_content_type, get_groups_by_name, obfuscate_claims

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -385,18 +384,9 @@ def _set_user_groups(
return

# Create missing groups if required
existing_groups = set(Group.objects.filter(name__in=desired_group_names))
existing_group_names = {group.name for group in existing_groups}
filtered_names = fnmatch.filter(
set(desired_group_names) - existing_group_names, sync_groups_glob
existing_groups = get_groups_by_name(
desired_group_names, sync_groups_glob, sync_missing_groups
)
groups_to_create = (
[Group(name=name) for name in filtered_names] if sync_missing_groups else []
)
if groups_to_create:
# postgres sets the PK after bulk_create
Group.objects.bulk_create(groups_to_create)
existing_groups |= set(groups_to_create)

# at this point, existing_groups is the full collection of groups that should be
# set on the user model, because:
Expand Down
13 changes: 13 additions & 0 deletions mozilla_django_oidc_db/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,16 @@
OPEN_ID_CONFIG_PATH = ".well-known/openid-configuration"

CONFIG_CLASS_SESSION_KEY = "_OIDCDB_CONFIG_CLASS"

CLAIM_MAPPING_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Claim Mapping",
"description": "Mapping from user-model fields to OIDC claims",
"type": "object",
"properties": {},
"additionalProperties": {
"description": "mapping",
"type": "array",
stevenbal marked this conversation as resolved.
Show resolved Hide resolved
"items": {"type": "string"},
},
}
4 changes: 3 additions & 1 deletion mozilla_django_oidc_db/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ def __init__(self, *bits: str):
self.bits = list(bits)

def __eq__(self, other) -> bool:
return self.bits == other.bits
if isinstance(other, ClaimFieldDefault):
return self.bits == other.bits
return False

def __call__(self) -> list[str]:
return self.bits
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.15 on 2024-10-25 14:15

from django.db import migrations
import django_jsonform.models.fields
import mozilla_django_oidc_db.models


class Migration(migrations.Migration):

dependencies = [
("mozilla_django_oidc_db", "0004_remove_openidconnectconfig_oidc_exempt_urls"),
]

operations = [
migrations.AlterField(
model_name="openidconnectconfig",
name="claim_mapping",
field=django_jsonform.models.fields.JSONField(
default=mozilla_django_oidc_db.models.get_claim_mapping,
help_text="Mapping from user-model fields to OIDC claims",
verbose_name="claim mapping",
),
),
]
8 changes: 5 additions & 3 deletions mozilla_django_oidc_db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _

from django_jsonform.models.fields import ArrayField
from django_jsonform.models.fields import ArrayField, JSONField
from solo import settings as solo_settings
from solo.models import SingletonModel

from .constants import CLAIM_MAPPING_SCHEMA
from .fields import ClaimField, ClaimFieldDefault
from .typing import ClaimPath, DjangoView

Expand Down Expand Up @@ -249,10 +250,11 @@ class OpenIDConnectConfig(OpenIDConnectConfigBase):
help_text=_("The name of the OIDC claim that is used as the username"),
)

claim_mapping = models.JSONField(
claim_mapping = JSONField(
Coperh marked this conversation as resolved.
Show resolved Hide resolved
_("claim mapping"),
default=get_claim_mapping,
help_text=("Mapping from user-model fields to OIDC claims"),
help_text=_("Mapping from user-model fields to OIDC claims"),
schema=CLAIM_MAPPING_SCHEMA,
)
groups_claim = ClaimField(
verbose_name=_("groups claim"),
Expand Down
Empty file.
Loading
Loading