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

1from functools import cached_property 

2import logging 

3from pathlib import Path 

4 

5from django import template 

6from django.conf import settings 

7from django.db import models 

8 

9import requests 

10 

11from benefits.secrets import NAME_VALIDATOR, get_secret_by_name 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16class Environment(models.TextChoices): 

17 QA = "qa", "QA" 

18 PROD = "prod", "Production" 

19 

20 

21def template_path(template_name: str) -> Path: 

22 """Get a `pathlib.Path` for the named template, or None if it can't be found. 

23 

24 A `template_name` is the app-local name, e.g. `enrollment/success.html`. 

25 

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 

36 

37 

38class SecretNameField(models.SlugField): 

39 """Field that stores the name of a secret held in a secret store. 

40 

41 The secret value itself MUST NEVER be stored in this field. 

42 """ 

43 

44 description = """Field that stores the name of a secret held in a secret store. 

45 

46 Secret names must be between 1-127 alphanumeric ASCII characters or hyphen characters. 

47 

48 The secret value itself MUST NEVER be stored in this field. 

49 """ 

50 

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) 

59 

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) 

64 

65 

66class PemData(models.Model): 

67 """API Certificate or Key in PEM format.""" 

68 

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") 

75 

76 def __str__(self): 

77 return self.label 

78 

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 

86 

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 

93 

94 if secret_data is None and self.remote_url: 

95 remote_data = requests.get(self.remote_url, timeout=settings.REQUESTS_TIMEOUT).text 

96 

97 return secret_data if secret_data is not None else remote_data