Coverage for benefits/core/models/common.py: 97%
51 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-14 01:41 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-14 01:41 +0000
1from functools import cached_property
2import logging
3from pathlib import Path
5from django import template
6from django.conf import settings
7from django.db import models
9import requests
11from benefits.secrets import NAME_VALIDATOR, get_secret_by_name
13logger = logging.getLogger(__name__)
16class Environment(models.TextChoices):
17 QA = "qa", "QA"
18 PROD = "prod", "Production"
21def template_path(template_name: str) -> Path:
22 """Get a `pathlib.Path` for the named template, or None if it can't be found.
24 A `template_name` is the app-local name, e.g. `enrollment/success.html`.
26 Adapted from https://stackoverflow.com/a/75863472.
27 """
28 if template_name:
29 for engine in template.engines.all():
30 for loader in engine.engine.template_loaders:
31 for origin in loader.get_template_sources(template_name):
32 path = Path(origin.name)
33 if path.exists() and path.is_file():
34 return path
35 return None
38class SecretNameField(models.SlugField):
39 """Field that stores the name of a secret held in a secret store.
41 The secret value itself MUST NEVER be stored in this field.
42 """
44 description = """Field that stores the name of a secret held in a secret store.
46 Secret names must be between 1-127 alphanumeric ASCII characters or hyphen characters.
48 The secret value itself MUST NEVER be stored in this field.
49 """
51 def __init__(self, *args, **kwargs):
52 kwargs["validators"] = [NAME_VALIDATOR]
53 # although the validator also checks for a max length of 127
54 # this setting enforces the length at the database column level as well
55 kwargs["max_length"] = 127
56 # the default is False, but this is more explicit
57 kwargs["allow_unicode"] = False
58 super().__init__(*args, **kwargs)
60 def secret_value(self, instance):
61 """Get the secret value from the secret store."""
62 secret_name = getattr(instance, self.attname)
63 return get_secret_by_name(secret_name)
66class PemData(models.Model):
67 """API Certificate or Key in PEM format."""
69 id = models.AutoField(primary_key=True)
70 label = models.TextField(help_text="Human description of the PEM data")
71 text_secret_name = SecretNameField(
72 default="", blank=True, help_text="The name of a secret with data in utf-8 encoded PEM text format"
73 )
74 remote_url = models.TextField(default="", blank=True, help_text="Public URL hosting the utf-8 encoded PEM text")
76 def __str__(self):
77 return self.label
79 @cached_property
80 def data(self):
81 """
82 Attempts to get data from `remote_url` or `text_secret_name`, with the latter taking precendence if both are defined.
83 """
84 remote_data = None
85 secret_data = None
87 if self.text_secret_name:
88 try:
89 secret_field = self._meta.get_field("text_secret_name")
90 secret_data = secret_field.secret_value(self)
91 except Exception:
92 secret_data = None
94 if secret_data is None and self.remote_url:
95 remote_data = requests.get(self.remote_url, timeout=settings.REQUESTS_TIMEOUT).text
97 return secret_data if secret_data is not None else remote_data