Coverage for benefits/secrets.py: 81%

48 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-21 19:31 +0000

1import logging 

2import os 

3import re 

4import sys 

5 

6from azure.core.exceptions import ClientAuthenticationError 

7from azure.identity import DefaultAzureCredential 

8from azure.keyvault.secrets import SecretClient 

9from django.conf import settings 

10from django.core.validators import RegexValidator 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15KEY_VAULT_URL = "https://kv-cdt-pub-calitp-{env}-001.vault.azure.net/" 

16 

17 

18class SecretNameValidator(RegexValidator): 

19 """RegexValidator that validates a secret name. 

20 

21 Azure KeyVault currently enforces the following rules: 

22 

23 * The value must be between 1 and 127 characters long. 

24 * Secret names can only contain alphanumeric characters and dashes. 

25 

26 Read more about Azure KeyVault naming rules: 

27 https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftkeyvault 

28 

29 Read more about Django validators: 

30 https://docs.djangoproject.com/en/5.0/ref/validators/#module-django.core.validators 

31 """ 

32 

33 def __init__(self, *args, **kwargs): 

34 kwargs["regex"] = re.compile(r"^[-a-zA-Z0-9]{1,127}$", re.ASCII) 

35 kwargs["message"] = ( 

36 "Enter a valid secret name of between 1-127 alphanumeric ASCII characters and the hyphen character only." 

37 ) 

38 super().__init__(*args, **kwargs) 

39 

40 

41NAME_VALIDATOR = SecretNameValidator() 

42 

43 

44def get_secret_by_name(secret_name, client=None): 

45 """Read a value from the secret store, currently Azure KeyVault. 

46 

47 When `settings.RUNTIME_ENVIRONMENT() == "local"`, reads from the environment instead. 

48 """ 

49 NAME_VALIDATOR(secret_name) 

50 

51 runtime_env = settings.RUNTIME_ENVIRONMENT() 

52 

53 if runtime_env == "local": 

54 logger.debug("Runtime environment is local, reading from environment instead of Azure KeyVault.") 

55 # environment variable names cannot contain the hyphen character 

56 # assume the variable name is the same but with underscores instead 

57 env_secret_name = secret_name.replace("-", "_") 

58 secret_value = os.environ.get(env_secret_name) 

59 # we have to replace literal newlines here with the actual newline character 

60 # to support local environment variables values that span multiple lines (e.g. PEM keys/certs) 

61 # because the VS Code Python extension doesn't support multiline environment variables 

62 # https://code.visualstudio.com/docs/python/environments#_environment-variables 

63 return secret_value.replace("\\n", "\n") 

64 

65 elif client is None: 

66 # construct the KeyVault URL from the runtime environment 

67 # see https://docs.calitp.org/benefits/deployment/infrastructure/#environments 

68 # and https://github.com/cal-itp/benefits/blob/main/terraform/key_vault.tf 

69 vault_url = KEY_VAULT_URL.format(env=runtime_env[0]) 

70 logger.debug(f"Configuring Azure KeyVault secrets client for: {vault_url}") 

71 

72 credential = DefaultAzureCredential() 

73 client = SecretClient(vault_url=vault_url, credential=credential) 

74 

75 secret_value = None 

76 

77 if client is not None: 

78 try: 

79 secret = client.get_secret(secret_name) 

80 secret_value = secret.value 

81 except ClientAuthenticationError: 

82 logger.error("Could not authenticate to Azure KeyVault") 

83 else: 

84 logger.error("Azure KeyVault SecretClient was not configured") 

85 

86 return secret_value 

87 

88 

89if __name__ == "__main__": 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true

90 args = sys.argv[1:] 

91 if len(args) < 1: 

92 print("Provide the name of the secret to read") 

93 exit(1) 

94 

95 secret_name = args[0] 

96 secret_value = get_secret_by_name(secret_name) 

97 

98 print(f"[{settings.RUNTIME_ENVIRONMENT()}] {secret_name}: {secret_value}") 

99 exit(0)