Skip to content

Commit

Permalink
Merge pull request #115 from maykinmedia/feature/114-add-django-setup…
Browse files Browse the repository at this point in the history
…-configuration

Feature/114 add django setup configuration
  • Loading branch information
stevenbal authored Dec 3, 2024
2 parents d0630df + d6c8846 commit 2382725
Show file tree
Hide file tree
Showing 27 changed files with 910 additions and 22 deletions.
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']

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:
""""""""""""""""


* ``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:
""""""""""""""""

.. 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",
"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(
_("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

0 comments on commit 2382725

Please sign in to comment.