Skip to content

Commit

Permalink
refactor(models): oauth claims and user flow
Browse files Browse the repository at this point in the history
  • Loading branch information
thekaveman committed Jan 28, 2025
1 parent 644a623 commit 9222258
Show file tree
Hide file tree
Showing 21 changed files with 270 additions and 55 deletions.
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DJANGO_SUPERUSER_PASSWORD=superuser12345!

# Django storage
DJANGO_DB_FILE=django.db
DJANGO_DB_FIXTURES="web/oauth/migrations/sample_fixtures.json"
DJANGO_DB_FIXTURES="web/oauth/migrations/sample_fixtures.json web/vital_records/migrations/sample_fixtures.json"
DJANGO_DB_RESET=true
DJANGO_STORAGE_DIR=.

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*.egg-info
*fixtures.json
!web/oauth/migrations/sample_fixtures.json
!web/vital_records/migrations/sample_fixtures.json
dist/
static/
!web/static
Expand Down
2 changes: 1 addition & 1 deletion bin/reset_db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ valid_fixtures=$(echo "$DJANGO_DB_FIXTURES" | grep -e fixtures\.json$ || test $?

if [[ -n "$valid_fixtures" ]]; then
# load data fixtures
python manage.py loaddata "$DJANGO_DB_FIXTURES"
python manage.py loaddata $DJANGO_DB_FIXTURES
else
echo "No JSON fixtures to load"
fi
3 changes: 3 additions & 0 deletions web/core/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .userflow import UserFlowAdmin

__all__ = ["UserFlowAdmin"]
12 changes: 12 additions & 0 deletions web/core/admin/userflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import logging

from django.contrib import admin

from web.core import models

logger = logging.getLogger(__name__)


@admin.register(models.UserFlow)
class UserFlowAdmin(admin.ModelAdmin):
list_display = ("label", "scopes", "eligibility_claim", "oauth_config")
86 changes: 86 additions & 0 deletions web/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Generated by Django 5.1.5 on 2025-01-27 23:58

import uuid

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("oauth", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="UserFlow",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
(
"label",
models.CharField(
help_text="A human readable label, used as the display text (user-facing)", max_length=50
),
),
(
"system_name",
models.SlugField(help_text="Internal system name for this flow, mapped to the root URL.", unique=True),
),
("urlconf_path", models.CharField(help_text="Django app path to the URLconf for this flow.", max_length=100)),
(
"scopes",
models.CharField(
help_text="A space-separated list of identifiers used to specify what information is being requested",
max_length=200,
),
),
(
"eligibility_claim",
models.CharField(help_text="The claim that is used to verify eligibility", max_length=50),
),
(
"extra_claims",
models.CharField(
blank=True, default="", help_text="A space-separated list of any additional claims", max_length=200
),
),
(
"redirect_failure",
models.CharField(
default="oauth:error",
help_text="A Django route in the form of app:endpoint to redirect to after a successful claims check",
max_length=50,
),
),
(
"redirect_success",
models.CharField(
default="oauth:success",
help_text="A Django route in the form of app:endpoint to redirect to after a successful claims check",
max_length=50,
),
),
(
"scheme_override",
models.CharField(
blank=True,
default="",
help_text="(Optional) the authentication scheme to use. Defaults to that provided by the OAuth config.", # noqa: E501
max_length=50,
verbose_name="Claims scheme",
),
),
(
"oauth_config",
models.ForeignKey(
help_text="The IdG connection details for this flow.",
on_delete=django.db.models.deletion.PROTECT,
to="oauth.ClientConfig",
),
),
],
),
]
Empty file added web/core/migrations/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions web/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .userflow import UserFlow

__all__ = ["UserFlow"]
91 changes: 91 additions & 0 deletions web/core/models/userflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import importlib
from uuid import uuid4

from django.db import models

from web.oauth.models.config import ClientConfig


class UserFlow(models.Model):
"""Represents a user journey through the DDRC app."""

id = models.UUIDField(
primary_key=True,
default=uuid4,
editable=False,
)
label = models.CharField(
help_text="A human readable label, used as the display text (user-facing)",
max_length=50,
)
system_name = models.SlugField(
help_text="Internal system name for this flow, mapped to the root URL.",
unique=True,
)
urlconf_path = models.CharField(
help_text="Django app path to the URLconf for this flow.",
max_length=100,
)
oauth_config = models.ForeignKey(
ClientConfig,
on_delete=models.PROTECT,
help_text="The IdG connection details for this flow.",
)
scopes = models.CharField(
help_text="A space-separated list of identifiers used to specify what information is being requested",
max_length=200,
)
eligibility_claim = models.CharField(
help_text="The claim that is used to verify eligibility",
max_length=50,
)
extra_claims = models.CharField(
blank=True,
default="",
help_text="A space-separated list of any additional claims",
max_length=200,
)
redirect_failure = models.CharField(
default="oauth:error",
help_text="A Django route in the form of app:endpoint to redirect to after a successful claims check",
max_length=50,
)
redirect_success = models.CharField(
default="oauth:success",
help_text="A Django route in the form of app:endpoint to redirect to after a successful claims check",
max_length=50,
)
scheme_override = models.CharField(
blank=True,
default="",
help_text="(Optional) the authentication scheme to use. Defaults to that provided by the OAuth config.",
max_length=50,
verbose_name="Claims scheme",
)

@property
def all_claims(self):
return " ".join((self.eligibility_claim, self.extra_claims))

@property
def index_url(self):
try:
match = [url for url in self.urlpatterns if url.pattern.regex.match("")]
index = match[0]
return f"{self.urlconf.app_name}:{index.name}"
except Exception:
return None

@property
def urlconf(self):
try:
return importlib.import_module(self.urlconf_path)
except Exception:
return None

@property
def urlpatterns(self):
try:
return self.urlconf.urlpatterns
except Exception:
return []
12 changes: 0 additions & 12 deletions web/oauth/admin.py

This file was deleted.

3 changes: 3 additions & 0 deletions web/oauth/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .config import ClientConfigAdmin

__all__ = ["ClientConfigAdmin"]
12 changes: 12 additions & 0 deletions web/oauth/admin/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import logging

from django.contrib import admin

from .. import models

logger = logging.getLogger(__name__)


@admin.register(models.ClientConfig)
class ClientConfigAdmin(admin.ModelAdmin):
list_display = ("client_name", "authority", "scheme")
17 changes: 10 additions & 7 deletions web/oauth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@

from authlib.integrations.django_client import OAuth

from web.oauth.models import OAuthClientConfig
from web.oauth.models import ClientConfig

logger = logging.getLogger(__name__)

oauth = OAuth()


def _client_kwargs(scope=None):
def _client_kwargs(extra_scopes: str = ""):
"""
Generate the OpenID Connect client_kwargs, with optional extra scope(s).
`scope` should be a space-separated list of scopes to add.
`extra_scopes` should be a space-separated list of scopes to add.
"""
scopes = ["openid", scope] if scope else ["openid"]
scopes = []
if "openid" not in extra_scopes:
scopes.append("openid")
scopes.append(extra_scopes)
return {"code_challenge_method": "S256", "scope": " ".join(scopes), "prompt": "login"}


Expand All @@ -41,7 +44,7 @@ def _authorize_params(scheme):
return params


def create_client(oauth_registry: OAuth, config: OAuthClientConfig):
def create_client(oauth_registry: OAuth, config: ClientConfig, scopes: str, scheme: str = ""):
"""
Returns an OAuth client, registering it if needed.
"""
Expand All @@ -55,8 +58,8 @@ def create_client(oauth_registry: OAuth, config: OAuthClientConfig):
config.client_name,
client_id=config.client_id,
server_metadata_url=_server_metadata_url(config.authority),
client_kwargs=_client_kwargs(config.scopes),
authorize_params=_authorize_params(config.scheme),
client_kwargs=_client_kwargs(scopes),
authorize_params=_authorize_params(scheme or config.scheme),
)

return client
15 changes: 3 additions & 12 deletions web/oauth/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ class Migration(migrations.Migration):

operations = [
migrations.CreateModel(
name="oauthclientconfig",
name="ClientConfig",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("client_name", models.CharField(help_text="The name of this OAuth client", max_length=50)),
("client_name", models.SlugField(help_text="The name of this OAuth client", unique=True)),
(
"client_id_secret_name",
web.oauth.models.secret_name_field.SecretNameField(
Expand All @@ -36,16 +36,7 @@ class Migration(migrations.Migration):
),
(
"scheme",
models.CharField(help_text="The authentication scheme to use for this OAuth client", max_length=50),
),
(
"scopes",
models.CharField(
blank=True,
default="",
help_text="A space-separated list of identifiers used to specify what access privileges are being requested", # noqa: E501
max_length=50,
),
models.CharField(help_text="The authentication scheme for the authority server", max_length=50),
),
],
options={
Expand Down
8 changes: 4 additions & 4 deletions web/oauth/migrations/sample_fixtures.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[
{
"model": "oauth.oauthclientconfig",
"model": "oauth.clientconfig",
"pk": "6915939f-a852-441e-aec6-9fb225156656",
"fields": {
"client_name": "dev",
"client_id_secret_name": "dev-client-id",
"authority": "https://dev.ca.gov",
"scheme": "dev-ddrc",
"scopes": "attribute:flag"
"authority": "https://dev.cdt.ca.gov",
"scheme": "dev"
}
}
]
4 changes: 2 additions & 2 deletions web/oauth/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .config import OAuthClientConfig
from .config import ClientConfig
from .secret_name_field import SecretNameField

__all__ = ["OAuthClientConfig", "SecretNameField"]
__all__ = ["ClientConfig", "SecretNameField"]
14 changes: 4 additions & 10 deletions web/oauth/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,24 @@
from .secret_name_field import SecretNameField


class OAuthClientConfig(models.Model):
class ClientConfig(models.Model):
"""OAuth Client configuration."""

class Meta:
verbose_name = "OAuth Client"

id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
client_name = models.CharField(
client_name = models.SlugField(
help_text="The name of this OAuth client",
max_length=50,
unique=True,
)
client_id_secret_name = SecretNameField(help_text="The name of the secret containing the client ID for this OAuth client")
authority = models.CharField(
help_text="The fully qualified HTTPS domain name for an OAuth authority server",
max_length=50,
)
scheme = models.CharField(
help_text="The authentication scheme to use for this OAuth client",
max_length=50,
)
scopes = models.CharField(
blank=True,
default="",
help_text="A space-separated list of identifiers used to specify what access privileges are being requested",
help_text="The authentication scheme for the authority server",
max_length=50,
)

Expand Down
Loading

0 comments on commit 9222258

Please sign in to comment.