Coverage for benefits/core/models/common.py: 97%
48 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-29 21:21 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-29 21:21 +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__)
16def template_path(template_name: str) -> Path:
17 """Get a `pathlib.Path` for the named template, or None if it can't be found.
19 A `template_name` is the app-local name, e.g. `enrollment/success.html`.
21 Adapted from https://stackoverflow.com/a/75863472.
22 """
23 if template_name:
24 for engine in template.engines.all():
25 for loader in engine.engine.template_loaders:
26 for origin in loader.get_template_sources(template_name):
27 path = Path(origin.name)
28 if path.exists() and path.is_file():
29 return path
30 return None
33class SecretNameField(models.SlugField):
34 """Field that stores the name of a secret held in a secret store.
36 The secret value itself MUST NEVER be stored in this field.
37 """
39 description = """Field that stores the name of a secret held in a secret store.
41 Secret names must be between 1-127 alphanumeric ASCII characters or hyphen characters.
43 The secret value itself MUST NEVER be stored in this field.
44 """
46 def __init__(self, *args, **kwargs):
47 kwargs["validators"] = [NAME_VALIDATOR]
48 # although the validator also checks for a max length of 127
49 # this setting enforces the length at the database column level as well
50 kwargs["max_length"] = 127
51 # the default is False, but this is more explicit
52 kwargs["allow_unicode"] = False
53 super().__init__(*args, **kwargs)
55 def secret_value(self, instance):
56 """Get the secret value from the secret store."""
57 secret_name = getattr(instance, self.attname)
58 return get_secret_by_name(secret_name)
61class PemData(models.Model):
62 """API Certificate or Key in PEM format."""
64 id = models.AutoField(primary_key=True)
65 label = models.TextField(help_text="Human description of the PEM data")
66 text_secret_name = SecretNameField(
67 default="", blank=True, help_text="The name of a secret with data in utf-8 encoded PEM text format"
68 )
69 remote_url = models.TextField(default="", blank=True, help_text="Public URL hosting the utf-8 encoded PEM text")
71 def __str__(self):
72 return self.label
74 @cached_property
75 def data(self):
76 """
77 Attempts to get data from `remote_url` or `text_secret_name`, with the latter taking precendence if both are defined.
78 """
79 remote_data = None
80 secret_data = None
82 if self.text_secret_name:
83 try:
84 secret_field = self._meta.get_field("text_secret_name")
85 secret_data = secret_field.secret_value(self)
86 except Exception:
87 secret_data = None
89 if secret_data is None and self.remote_url:
90 remote_data = requests.get(self.remote_url, timeout=settings.REQUESTS_TIMEOUT).text
92 return secret_data if secret_data is not None else remote_data