Skip to content

Commit

Permalink
♻️[#114] refactor AdminOIDCConfigurationStep
Browse files Browse the repository at this point in the history
  • Loading branch information
Coperh committed Nov 22, 2024
1 parent 6db9491 commit c09cf9d
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 176 deletions.
19 changes: 5 additions & 14 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 create_missing_groups, extract_content_type, obfuscate_claims

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -385,18 +384,10 @@ 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
)
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)
if sync_missing_groups:
existing_groups = create_missing_groups(desired_group_names, sync_groups_glob)
else:
existing_groups = set(Group.objects.filter(name__in=desired_group_names))

# at this point, existing_groups is the full collection of groups that should be
# set on the user model, because:
Expand Down
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
78 changes: 0 additions & 78 deletions mozilla_django_oidc_db/setup_config.py

This file was deleted.

Empty file.
92 changes: 92 additions & 0 deletions mozilla_django_oidc_db/setup_configuration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from typing import Optional, Union

from django_setup_configuration.fields import DjangoModelRef
from django_setup_configuration.models import ConfigurationModel
from pydantic import AnyUrl, Discriminator, Tag
from typing_extensions import Annotated

from mozilla_django_oidc_db.models import OpenIDConnectConfig


class OIDCFullEndpointConfig(ConfigurationModel):
oidc_op_authorization_endpoint: AnyUrl = DjangoModelRef(
OpenIDConnectConfig, "oidc_op_authorization_endpoint"
)
oidc_op_token_endpoint: AnyUrl = DjangoModelRef(
OpenIDConnectConfig, "oidc_op_token_endpoint"
)
oidc_op_user_endpoint: AnyUrl = DjangoModelRef(
OpenIDConnectConfig, "oidc_op_user_endpoint"
)


class OIDCDiscoveryEndpoint(ConfigurationModel):
oidc_op_discovery_endpoint: AnyUrl = DjangoModelRef(
OpenIDConnectConfig, "oidc_op_discovery_endpoint", default=None
)


def get_endpoint_endpoint_model(endpoint_data):

if isinstance(endpoint_data, dict):
discovery_endpoint = endpoint_data.get("oidc_op_discovery_endpoint")
else:
discovery_endpoint = getattr(endpoint_data, "oidc_op_discovery_endpoint", None)
if discovery_endpoint:
return "discovery"
return "all"


EndpointConfigUnion = Annotated[
Union[
Annotated[OIDCFullEndpointConfig, Tag("all")],
Annotated[OIDCDiscoveryEndpoint, Tag("discovery")],
],
Discriminator(get_endpoint_endpoint_model),
]


class AdminOIDCConfigurationModel(ConfigurationModel):

# Json
claim_mapping: Optional[dict] = DjangoModelRef(OpenIDConnectConfig, "claim_mapping")

# Arrays are overridden to make the typing simpler (the underlying Django field is an ArrayField, which is non-standard)
username_claim: Optional[list[str]] = DjangoModelRef(
OpenIDConnectConfig, "username_claim"
)
groups_claim: Optional[list[str]] = DjangoModelRef(
OpenIDConnectConfig, "groups_claim"
)
superuser_group_names: Optional[list[str]] = DjangoModelRef(
OpenIDConnectConfig, "superuser_group_names"
)
default_groups: Optional[list[str]] = DjangoModelRef(
OpenIDConnectConfig, "superuser_group_names"
)
oidc_rp_scopes_list: Optional[list[str]] = DjangoModelRef(
OpenIDConnectConfig, "oidc_rp_scopes_list"
)

endpoint_config: EndpointConfigUnion

class Meta:
django_model_refs = {
OpenIDConnectConfig: [
"oidc_rp_client_id",
"oidc_rp_client_secret",
"oidc_token_use_basic_auth",
"oidc_rp_sign_algo",
"oidc_rp_idp_sign_key",
"oidc_op_logout_endpoint",
"oidc_op_jwks_endpoint",
"oidc_use_nonce",
"oidc_nonce_size",
"oidc_state_size",
"oidc_keycloak_idp_hint",
"userinfo_claims_source",
"sync_groups",
"sync_groups_glob_pattern",
"make_users_staff",
]
}
53 changes: 53 additions & 0 deletions mozilla_django_oidc_db/setup_configuration/steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django_setup_configuration.configuration import BaseConfigurationStep
from django_setup_configuration.exceptions import ConfigurationRunFailed

from mozilla_django_oidc_db.forms import OIDCSetupConfigForm
from mozilla_django_oidc_db.models import OpenIDConnectConfig
from mozilla_django_oidc_db.setup_configuration.models import (
AdminOIDCConfigurationModel,
)
from mozilla_django_oidc_db.utils import create_missing_groups


class AdminOIDCConfigurationStep(BaseConfigurationStep[AdminOIDCConfigurationModel]):
"""
Configure admin login via OpenID Connect
"""

verbose_name = "Configuration for admin login via OpenID Connect"
config_model = AdminOIDCConfigurationModel
namespace = "ADMIN_OIDC"
enable_setting = "ADMIN_OIDC_CONFIG_ENABLE"

def execute(self, model: AdminOIDCConfigurationModel) -> None:

config = OpenIDConnectConfig.get_solo()

base_model_data = model.model_dump()
endpoint_config_data = base_model_data.pop("endpoint_config")

all_settings = {
"sync_groups": config.sync_groups,
"oidc_use_nonce": config.oidc_use_nonce,
"enabled": True,
"claim_mapping": config.claim_mapping, # JSONFormField widget cannot handle blank values with object schema
"sync_groups_glob_pattern": config.sync_groups_glob_pattern,
**base_model_data,
**endpoint_config_data,
}

if groups := all_settings.get("default_groups"):
all_settings["default_groups"] = create_missing_groups(
groups, all_settings["sync_groups_glob_pattern"]
)

form = OIDCSetupConfigForm(
instance=config,
data=all_settings,
)
if not form.is_valid():
raise ConfigurationRunFailed(
"Admin OIDC configuration field validation failed",
form.errors.as_json(),
)
form.save()
24 changes: 23 additions & 1 deletion mozilla_django_oidc_db/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import fnmatch
import logging
from collections.abc import Collection
from collections.abc import Collection, Iterable
from copy import deepcopy

from django.contrib.auth.models import Group

import requests
from glom import Path, PathAccessError, assign, glom
from requests.utils import _parse_content_type_header # type: ignore
Expand Down Expand Up @@ -89,3 +92,22 @@ def do_op_logout(config: OpenIDConnectConfigBase, id_token: str) -> None:
"status_code": response.status_code,
},
)


def create_missing_groups(
group_names: Iterable[str], sync_groups_glob: str = "*"
) -> set[Group]:

existing_groups = set(Group.objects.filter(name__in=group_names))
existing_group_names = {group.name for group in existing_groups}

filtered_names = fnmatch.filter(
set(group_names) - existing_group_names, sync_groups_glob
)

groups_to_create = [Group(name=name) for name in filtered_names]
if groups_to_create:
# postgres sets the PK after bulk_create
Group.objects.bulk_create(groups_to_create)
existing_groups |= set(groups_to_create)
return existing_groups
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Changelog = "https://github.com/maykinmedia/mozilla-django-oidc-db/blob/master/C

[project.optional-dependencies]
setupconfig = [
"django-setup-configuration>=0.3.0",
"django-setup-configuration@git+https://github.com/maykinmedia/django-setup-configuration.git@ba0ed8a14acf0ffe05244f53b78fb343c6e5c3df",
]
tests = [
"psycopg2",
Expand Down
2 changes: 1 addition & 1 deletion testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@

OIDC_DB_CONFIG_ENABLE = True
SETUP_CONFIGURATION_STEPS = [
"mozilla_django_oidc_db.setup_config.AdminOIDCConfigurationStep",
"mozilla_django_oidc_db.setup_configuration.steps.AdminOIDCConfigurationStep",
]
except ImportError:
pass
Loading

0 comments on commit c09cf9d

Please sign in to comment.