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

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 

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

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

18 

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

20 

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 

31 

32 

33class SecretNameField(models.SlugField): 

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

35 

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

37 """ 

38 

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

40 

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

42 

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

44 """ 

45 

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) 

54 

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) 

59 

60 

61class PemData(models.Model): 

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

63 

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

70 

71 def __str__(self): 

72 return self.label 

73 

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 

81 

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 

88 

89 if secret_data is None and self.remote_url: 

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

91 

92 return secret_data if secret_data is not None else remote_data