Coverage for benefits/secrets.py: 81%
48 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 18:00 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 18:00 +0000
1import logging
2import os
3import re
4import sys
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
12logger = logging.getLogger(__name__)
15KEY_VAULT_URL = "https://kv-cdt-pub-calitp-{env}-001.vault.azure.net/"
18class SecretNameValidator(RegexValidator):
19 """RegexValidator that validates a secret name.
21 Azure KeyVault currently enforces the following rules:
23 * The value must be between 1 and 127 characters long.
24 * Secret names can only contain alphanumeric characters and dashes.
26 Read more about Azure KeyVault naming rules:
27 https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftkeyvault
29 Read more about Django validators:
30 https://docs.djangoproject.com/en/5.0/ref/validators/#module-django.core.validators
31 """
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)
41NAME_VALIDATOR = SecretNameValidator()
44def get_secret_by_name(secret_name, client=None):
45 """Read a value from the secret store, currently Azure KeyVault.
47 When `settings.RUNTIME_ENVIRONMENT() == "local"`, reads from the environment instead.
48 """
49 NAME_VALIDATOR(secret_name)
51 runtime_env = settings.RUNTIME_ENVIRONMENT()
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")
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}")
72 credential = DefaultAzureCredential()
73 client = SecretClient(vault_url=vault_url, credential=credential)
75 secret_value = None
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")
86 return secret_value
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)
95 secret_name = args[0]
96 secret_value = get_secret_by_name(secret_name)
98 print(f"[{settings.RUNTIME_ENVIRONMENT()}] {secret_name}: {secret_value}")
99 exit(0)