diff --git a/.env/.env b/.env/.env new file mode 100644 index 0000000..cd6fe11 --- /dev/null +++ b/.env/.env @@ -0,0 +1,9 @@ +# 📝 Shared Variables 📝 +# These variables are always loaded and shared by all environments. +# +# The final value of a variable depends on the priority of where it's being set: +# 1. Deployment pipeline. +# 2. Dedicated environment (.env.{local/development/staging/production}). +# 3. Shared environment (.env). +# +# đŸšĢ DO NOT PUT SECRETS HERE đŸšĢ diff --git a/.env/.env.development b/.env/.env.development new file mode 100644 index 0000000..fc398b7 --- /dev/null +++ b/.env/.env.development @@ -0,0 +1,9 @@ +# 📝 Development Variables 📝 +# These variables are only loaded in the development environment. +# +# The final value of a variable depends on the priority of where it's being set: +# 1. Deployment pipeline. +# 2. Dedicated environment (.env.development). +# 3. Shared environment (.env). +# +# đŸšĢ DO NOT PUT SECRETS HERE đŸšĢ diff --git a/.env/.env.local b/.env/.env.local new file mode 100644 index 0000000..0be1dcb --- /dev/null +++ b/.env/.env.local @@ -0,0 +1,8 @@ +# 📝 Local Variables 📝 +# These variables are only loaded in your local environment (on your PC). +# +# The final value of a variable depends on the priority of where it's being set: +# 1. Dedicated environment (.env.local). +# 2. Shared environment (.env). +# +# đŸšĢ DO NOT PUT SECRETS HERE đŸšĢ diff --git a/.env/.env.local.secrets b/.env/.env.local.secrets new file mode 100644 index 0000000..2ddb311 --- /dev/null +++ b/.env/.env.local.secrets @@ -0,0 +1,6 @@ +# 📝 Local Secret Variables 📝 +# These secret variables are only loaded in your local environment (on your PC). +# +# This file is git-ignored intentionally to keep these variables a secret. +# +# đŸšĢ DO NOT PUSH SECRETS TO THE CODE REPO đŸšĢ diff --git a/.env/.env.production b/.env/.env.production new file mode 100644 index 0000000..6897d9b --- /dev/null +++ b/.env/.env.production @@ -0,0 +1,9 @@ +# 📝 Production Variables 📝 +# These variables are only loaded in the production environment. +# +# The final value of a variable depends on the priority of where it's being set: +# 1. Deployment pipeline. +# 2. Dedicated environment (.env.production). +# 3. Shared environment (.env). +# +# đŸšĢ DO NOT PUT SECRETS HERE đŸšĢ diff --git a/.env/.env.staging b/.env/.env.staging new file mode 100644 index 0000000..4b741f4 --- /dev/null +++ b/.env/.env.staging @@ -0,0 +1,9 @@ +# 📝 Staging Variables 📝 +# These variables are only loaded in the staging environment. +# +# The final value of a variable depends on the priority of where it's being set: +# 1. Deployment pipeline. +# 2. Dedicated environment (.env.staging). +# 3. Shared environment (.env). +# +# đŸšĢ DO NOT PUT SECRETS HERE đŸšĢ diff --git a/.gitignore b/.gitignore index a0203c8..945c5a9 100644 --- a/.gitignore +++ b/.gitignore @@ -125,7 +125,7 @@ celerybeat.pid *.sage.py # Environments -.env +.env.secrets .venv env/ venv/ diff --git a/Pipfile b/Pipfile index 3600035..d448682 100644 --- a/Pipfile +++ b/Pipfile @@ -2,7 +2,6 @@ url = "https://pypi.org/simple" verify_ssl = true name = "pypi" - ## ℹī¸ HOW-TO: Make the python-package editable. # # 1. Comment out the non-editable codeforlife package under [packages]. @@ -26,6 +25,7 @@ name = "pypi" codeforlife = "==0.23.1" # đŸšĢ Don't add [packages] below that are inherited from the CFL package. django-storages = {version = "==1.14.4", extras = ["s3"]} +python-dotenv = "*" [dev-packages] codeforlife = {version = "==0.23.1", extras = ["dev"]} diff --git a/Pipfile.lock b/Pipfile.lock index da23387..6147d4a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "54296bc5a0b1834777fc719ff1adda9313c1805eb4c7443f1ce2637c71c1451e" + "sha256": "404038a2660fa25db59360c29b215be1df82026aaa2bc21a34d82c50f3acdddb" }, "pipfile-spec": 6, "requires": { @@ -737,6 +737,15 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, + "python-dotenv": { + "hashes": [ + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.0.1" + }, "pytz": { "hashes": [ "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", diff --git a/api/urls.py b/api/urls.py index 49c587e..dad28ed 100644 --- a/api/urls.py +++ b/api/urls.py @@ -54,6 +54,11 @@ def get_health_check(self, request): health_status=health_check.health_status, additional_info=health_check.additional_info, details=[ + HealthCheck.Detail( + name="settings_secrets_keys", + description=",".join(list(settings.secrets.keys())), + health="healthy", + ), HealthCheck.Detail( name="STATIC_ROOT", description=str(settings.STATIC_ROOT), diff --git a/settings.py b/settings.py index 746c90a..9c3f510 100644 --- a/settings.py +++ b/settings.py @@ -16,11 +16,87 @@ import os from pathlib import Path + +def set_up_settings(service_name: str): + """Set up the settings for the service. + + *This needs to be called before importing the CFL settings!* + + Examples: + ``` + from codeforlife import set_up_settings + + # Must set up settings before importing them! + secrets = set_up_settings("example") + + from codeforlife.settings import * + ``` + + Args: + service_name: The name of the current service. + + Returns: + The secrets. These are not loaded as environment variables so that 3rd + party packages cannot read them. + """ + + # pylint: disable-all + + import sys + + # import typing as t + from io import StringIO + + import boto3 + from dotenv import dotenv_values, load_dotenv + + # Env = t.Literal["local", "development", "staging", "production"] + # if t.TYPE_CHECKING: + # from mypy_boto3_s3.client import S3Client + + if "codeforlife.settings" in sys.modules: + raise ImportError( + "You must set up the CFL settings before importing them." + ) + + os.environ["SERVICE_NAME"] = service_name + + os.environ.setdefault("ENV", "local") + # env = t.cast(Env, os.environ["ENV"]) + env = os.environ["ENV"] + + load_dotenv(f".env/.env.{env}", override=False) + load_dotenv(".env/.env", override=False) + + if env == "local": + _secrets = dotenv_values(".env/.env.local.secrets") + else: + _AWS_S3_APP_BUCKET = os.environ["aws_s3_app_bucket"] + _AWS_S3_APP_FOLDER = os.environ["aws_s3_app_folder"] + + # Get the secrets object. + s3: "S3Client" = boto3.client("s3") + secrets_object = s3.get_object( + Bucket=_AWS_S3_APP_BUCKET, + Key=f"{_AWS_S3_APP_FOLDER}/secure/.env.secrets", + ) + + secrets_str = secrets_object["Body"].read().decode("utf-8") + + _secrets = dotenv_values(stream=StringIO(secrets_str)) + + return {key: value for key, value in _secrets.items() if value is not None} + + # pylint: enable + + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent -# NOTE: Must come before importing CFL settings. -os.environ["SERVICE_NAME"] = "contributor" +secrets = set_up_settings(service_name="contributor") + +# pylint: disable-next=wildcard-import,unused-wildcard-import,wrong-import-position +from codeforlife.settings import * # GitHub @@ -30,9 +106,6 @@ GH_CLIENT_ID = os.getenv("GH_CLIENT_ID", "Ov23liBErSabQFqROeMg") GH_CLIENT_SECRET = os.getenv("GH_CLIENT_SECRET", "replace-me") -# pylint: disable-next=wildcard-import,unused-wildcard-import,wrong-import-position -from codeforlife.settings import * - # Installed Apps # https://docs.djangoproject.com/en/3.2/ref/settings/#installed-apps