Skip to content

Commit

Permalink
refactor(models): split into multiple files
Browse files Browse the repository at this point in the history
Take all models defined in benefits/core/models.py and split into files
to group models by domain:

* benefits/core/models/common.py: common fields/models, helper functions
* benefits/core/models/claims.py: ClaimProvider model
* benefits/core/models/enrollment.py: EnrollmentFlow, EnrollmentEvent models
* benefits/core/models/transit.py: TransitProvider, TransitAgency models

Maintain existing imports via top-level benefits/core/models/__init__.py
  • Loading branch information
thekaveman committed Jan 3, 2025
1 parent 9817033 commit dcde528
Show file tree
Hide file tree
Showing 12 changed files with 796 additions and 711 deletions.
36 changes: 36 additions & 0 deletions benefits/core/migrations/0033_pemdata_helptext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 5.1.4 on 2025-01-03 01:59

import benefits.core.models.common
import benefits.secrets
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0032_optionalfields"),
]

operations = [
migrations.AlterField(
model_name="pemdata",
name="label",
field=models.TextField(help_text="Human description of the PEM data"),
),
migrations.AlterField(
model_name="pemdata",
name="remote_url",
field=models.TextField(blank=True, default="", help_text="Public URL hosting the utf-8 encoded PEM text"),
),
migrations.AlterField(
model_name="pemdata",
name="text_secret_name",
field=benefits.core.models.common.SecretNameField(
blank=True,
default="",
help_text="The name of a secret with data in utf-8 encoded PEM text format",
max_length=127,
validators=[benefits.secrets.SecretNameValidator()],
),
),
]
18 changes: 18 additions & 0 deletions benefits/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .common import template_path, SecretNameField, PemData
from .claims import ClaimsProvider
from .transit import agency_logo_large, agency_logo_small, TransitProcessor, TransitAgency
from .enrollment import EnrollmentMethods, EnrollmentFlow, EnrollmentEvent

__all__ = [
"template_path",
"SecretNameField",
"PemData",
"ClaimsProvider",
"agency_logo_large",
"agency_logo_small",
"TransitProcessor",
"TransitAgency",
"EnrollmentMethods",
"EnrollmentFlow",
"EnrollmentEvent",
]
29 changes: 29 additions & 0 deletions benefits/core/models/claims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.db import models

from .common import SecretNameField


class ClaimsProvider(models.Model):
"""An entity that provides claims for eligibility verification."""

id = models.AutoField(primary_key=True)
sign_out_button_template = models.TextField(default="", blank=True, help_text="Template that renders sign-out button")
sign_out_link_template = models.TextField(default="", blank=True, help_text="Template that renders sign-out link")
client_name = models.TextField(help_text="Unique identifier used to register this claims provider with Authlib registry")
client_id_secret_name = SecretNameField(
help_text="The name of the secret containing the client ID for this claims provider"
)
authority = models.TextField(help_text="The fully qualified HTTPS domain name for an OAuth authority server")
scheme = models.TextField(help_text="The authentication scheme to use")

@property
def supports_sign_out(self):
return bool(self.sign_out_button_template) or bool(self.sign_out_link_template)

@property
def client_id(self):
secret_name_field = self._meta.get_field("client_id_secret_name")
return secret_name_field.secret_value(self)

def __str__(self) -> str:
return self.client_name
91 changes: 91 additions & 0 deletions benefits/core/models/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from functools import cached_property
import logging
from pathlib import Path

from django import template
from django.conf import settings
from django.db import models

import requests

from benefits.secrets import NAME_VALIDATOR, get_secret_by_name

logger = logging.getLogger(__name__)


def template_path(template_name: str) -> Path:
"""Get a `pathlib.Path` for the named template, or None if it can't be found.
A `template_name` is the app-local name, e.g. `enrollment/success.html`.
Adapted from https://stackoverflow.com/a/75863472.
"""
if template_name:
for engine in template.engines.all():
for loader in engine.engine.template_loaders:
for origin in loader.get_template_sources(template_name):
path = Path(origin.name)
if path.exists() and path.is_file():
return path
return None


class SecretNameField(models.SlugField):
"""Field that stores the name of a secret held in a secret store.
The secret value itself MUST NEVER be stored in this field.
"""

description = """Field that stores the name of a secret held in a secret store.
Secret names must be between 1-127 alphanumeric ASCII characters or hyphen characters.
The secret value itself MUST NEVER be stored in this field.
"""

def __init__(self, *args, **kwargs):
kwargs["validators"] = [NAME_VALIDATOR]
# although the validator also checks for a max length of 127
# this setting enforces the length at the database column level as well
kwargs["max_length"] = 127
# the default is False, but this is more explicit
kwargs["allow_unicode"] = False
super().__init__(*args, **kwargs)

def secret_value(self, instance):
"""Get the secret value from the secret store."""
secret_name = getattr(instance, self.attname)
return get_secret_by_name(secret_name)


class PemData(models.Model):
"""API Certificate or Key in PEM format."""

id = models.AutoField(primary_key=True)
label = models.TextField(help_text="Human description of the PEM data")
text_secret_name = SecretNameField(
default="", blank=True, help_text="The name of a secret with data in utf-8 encoded PEM text format"
)
remote_url = models.TextField(default="", blank=True, help_text="Public URL hosting the utf-8 encoded PEM text")

def __str__(self):
return self.label

@cached_property
def data(self):
"""
Attempts to get data from `remote_url` or `text_secret_name`, with the latter taking precendence if both are defined.
"""
remote_data = None
secret_data = None

if self.remote_url:
remote_data = requests.get(self.remote_url, timeout=settings.REQUESTS_TIMEOUT).text
if self.text_secret_name:
try:
secret_field = self._meta.get_field("text_secret_name")
secret_data = secret_field.secret_value(self)
except Exception:
secret_data = None

return secret_data if secret_data is not None else remote_data
Loading

0 comments on commit dcde528

Please sign in to comment.