From b960b6007fdea104d25d154d8bf3dcc716276d9f Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Fri, 6 Dec 2024 13:08:36 +0000 Subject: [PATCH] feat: secrets and storages (#148) * feat: secrets and storages * cast env * fix: key errors * fix: urls * AWS_S3_OBJECT_PARAMETERS * fix: types * fix: linting * refresh pipfile.lock * fix: imports --- Pipfile | 2 + Pipfile.lock | 194 +++++++++++++---------- codeforlife/__init__.py | 113 +++++++++++++ codeforlife/settings/custom.py | 28 ++-- codeforlife/settings/django.py | 57 ++++--- codeforlife/settings/third_party.py | 45 ++++++ codeforlife/types.py | 2 + codeforlife/urls/patterns.py | 68 +++----- codeforlife/views/__init__.py | 3 +- codeforlife/views/{common.py => csrf.py} | 9 -- codeforlife/views/session.py | 30 ++++ 11 files changed, 360 insertions(+), 191 deletions(-) rename codeforlife/views/{common.py => csrf.py} (69%) create mode 100644 codeforlife/views/session.py diff --git a/Pipfile b/Pipfile index ac9c1e3d..de4f3ff5 100644 --- a/Pipfile +++ b/Pipfile @@ -13,7 +13,9 @@ django-two-factor-auth = "==1.13.2" django-cors-headers = "==4.1.0" django-csp = "==3.7" django-import-export = "==4.0.3" +django-storages = {version = "==1.14.4", extras = ["s3"]} pyotp = "==2.9.0" +python-dotenv = "==1.0.1" psycopg2-binary = "==2.9.9" requests = "==2.32.2" gunicorn = "==23.0.0" diff --git a/Pipfile.lock b/Pipfile.lock index 3dc0cbbc..fb7fa0f5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "95bf4e62cd12de65fdc81a5326dc1bbfa6a6807c50067ed0c2dcd8fa7b6bd35e" + "sha256": "172ff0e75008059427fdc3d00096720a746c11fc54340cea0363e723992b3fca" }, "pipfile-spec": 6, "requires": { @@ -317,6 +317,17 @@ ], "version": "==2.0.0" }, + "django-storages": { + "extras": [ + "s3" + ], + "hashes": [ + "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f", + "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3" + ], + "markers": "python_version >= '3.7'", + "version": "==1.14.4" + }, "django-treebeard": { "hashes": [ "sha256:83aebc34a9f06de7daaec330d858d1c47887e81be3da77e3541fe7368196dd8a" @@ -732,9 +743,18 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "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", @@ -895,11 +915,11 @@ }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" }, "sqlparse": { "hashes": [ @@ -1026,11 +1046,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:7e938bb169c28faf05ce14e67bb0b5e5583092ab6ccc9d3d68d698530edb6584", - "sha256:c5f7208b20ae19400fa73eb569017f1e372990f7a5505a72116ed6420904f666" + "sha256:617508d023e0bc98901e0189b794c4b3f289c1747c7cc410173ad698c819a716", + "sha256:c977a049481d50a14bf2db0ef15020b76734ff628d4b8e0e77b8d1c65318369e" ], "markers": "python_version >= '3.8'", - "version": "==1.35.71" + "version": "==1.35.76" }, "certifi": { "hashes": [ @@ -1164,71 +1184,71 @@ "toml" ], "hashes": [ - "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", - "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", - "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", - "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", - "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", - "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", - "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", - "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", - "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", - "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", - "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", - "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", - "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", - "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", - "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", - "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", - "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", - "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", - "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", - "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", - "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", - "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", - "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", - "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", - "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", - "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", - "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", - "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", - "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", - "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", - "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", - "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", - "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", - "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", - "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", - "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", - "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", - "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", - "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", - "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", - "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", - "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", - "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", - "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", - "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", - "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", - "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", - "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", - "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", - "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", - "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", - "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", - "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", - "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", - "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", - "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", - "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", - "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", - "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", - "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", - "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", - "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9" + "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4", + "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c", + "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f", + "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b", + "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6", + "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", + "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692", + "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4", + "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", + "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717", + "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", + "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198", + "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1", + "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3", + "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", + "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", + "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08", + "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf", + "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", + "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710", + "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", + "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae", + "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", + "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", + "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb", + "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", + "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", + "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", + "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6", + "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", + "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9", + "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa", + "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", + "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b", + "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a", + "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8", + "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", + "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678", + "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015", + "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902", + "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97", + "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845", + "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", + "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464", + "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be", + "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9", + "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7", + "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", + "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1", + "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", + "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5", + "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073", + "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4", + "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", + "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", + "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3", + "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599", + "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0", + "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b", + "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec", + "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", + "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3" ], "markers": "python_version >= '3.9'", - "version": "==7.6.8" + "version": "==7.6.9" }, "dill": { "hashes": [ @@ -1379,17 +1399,17 @@ }, "mypy-boto3-dynamodb": { "hashes": [ - "sha256:187915c781f352bc79d35b08a094605515ecc54f30107f629972c3358b864a5c", - "sha256:92eac35c49e9f3ff23a4ad6dee5dc54e410e0c49a98b4d93493c7000ebe74568" + "sha256:a815d044b8f5f4ba308ea3114916565fbd932fcaf218f8d0288b2840415f9c46", + "sha256:b693b459abb1910cbb28f3a478ced8c6e6515f1bf136b45aca1a76b6146b5adb" ], - "version": "==1.35.60" + "version": "==1.35.74" }, "mypy-boto3-ec2": { "hashes": [ - "sha256:93f9ddadac303d63f34cd4c0a60a682c008d655d3b2cfa74d1234fbde9a0b401", - "sha256:d5b27b79b1749fb10a4eb9508069995a8e4bf2614f4e171224637596403e42c8" + "sha256:3206cd6da473647cdefa5dcec4121b4a83778f49ee540ca4b8aeb6c337975b69", + "sha256:d2ff43ad1c42655cbcbb06d11dff74b3827503d80a99a78098ab52ba0fbb7235" ], - "version": "==1.35.70" + "version": "==1.35.72" }, "mypy-boto3-lambda": { "hashes": [ @@ -1400,17 +1420,17 @@ }, "mypy-boto3-rds": { "hashes": [ - "sha256:0850cb5bddda1853c6ba44bb8dc1bf0d303ea4729f8cdf982d0e4d91f08ab2d9", - "sha256:7bfeadfbd361aaf53a5f161c571886d3cadbdf05c15591761280fe6f079ab273" + "sha256:4c345e616a7767953284a0d54ab6dbabd8b068fe353b34194b79364b47176b61", + "sha256:acd87fdfd12cc8f7298f586734f5c5e7afb0fcd6da8154a10cccb5730cc5c799" ], - "version": "==1.35.66" + "version": "==1.35.72" }, "mypy-boto3-s3": { "hashes": [ - "sha256:11a34259983e09d67e4d3a322fd47904a006bbfff19984e4e36a77e30f2014bb", - "sha256:97f7944a84a4a49282825bef1483a25680dcdce75da6017745d709d2cf2aa1c0" + "sha256:35f9ae109c3cb64ac6b44596dffc429058085ddb82f4daaf5be0a39e5cc1b576", + "sha256:6cf1f034985fe610754c3e6ef287490629870d508ada13b7d61e7b9aaeb46108" ], - "version": "==1.35.69" + "version": "==1.35.76" }, "mypy-boto3-sqs": { "hashes": [ @@ -1606,11 +1626,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:0d362a5d62d68ca4216f458172f41c1123ec04791d68364de8ee8b61b528b262", - "sha256:a20b425dabb258bc3d07a5e7de503fd9558dd1542d72de796e74e402c6d493b2" + "sha256:043c0ae0fe5d272618294cbeaf1c349a654a9f7c00121be64d27486933ac4a26", + "sha256:cc0057885cb7ce1e66856123a4c2861b051e9f0716b1767ad72bfe4ca26bbcd4" ], "markers": "python_version >= '3.8'", - "version": "==0.23.1" + "version": "==0.23.3" }, "types-pytz": { "hashes": [ diff --git a/codeforlife/__init__.py b/codeforlife/__init__.py index cce09239..ad0c625f 100644 --- a/codeforlife/__init__.py +++ b/codeforlife/__init__.py @@ -3,10 +3,123 @@ Created on 20/02/2024 at 09:28:27(+00:00). """ +import os +import sys +import typing as t +from io import StringIO from pathlib import Path +from types import SimpleNamespace +from .types import Env from .version import __version__ BASE_DIR = Path(__file__).resolve().parent DATA_DIR = BASE_DIR.joinpath("data") USER_DIR = BASE_DIR.joinpath("user") + + +if t.TYPE_CHECKING: + from mypy_boto3_s3.client import S3Client + + +# pylint: disable-next=too-few-public-methods +class Secrets(SimpleNamespace): + """The secrets for this service. + + If a key does not exist, the value None will be returned. + """ + + def __getattribute__(self, name: str) -> t.Optional[str]: + try: + return super().__getattribute__(name) + except AttributeError: + return None + + +def set_up_settings(service_base_dir: Path, service_name: str): + """Set up the settings for the service. + + *This needs to be called before importing the CFL settings!* + + To expose a secret to your Django project, you'll need to create a setting + for it following Django's conventions. + + Examples: + ``` + from codeforlife import set_up_settings + + # Must set up settings before importing them! + secrets = set_up_settings("my-service") + + from codeforlife.settings import * + + # Expose secret to django project. + MY_SECRET = secrets.MY_SECRET + ``` + + Args: + service_base_dir: The base directory of the service. + service_name: The name of the service. + + Returns: + The secrets. These are not loaded as environment variables so that 3rd + party packages cannot read them. + """ + + # Validate CFL settings have not been imported yet. + if "codeforlife.settings" in sys.modules: + raise ImportError( + "You must set up the CFL settings before importing them." + ) + + # pylint: disable-next=import-outside-toplevel + from dotenv import dotenv_values, load_dotenv + + # Set required environment variables. + os.environ["SERVICE_BASE_DIR"] = str(service_base_dir) + os.environ["SERVICE_NAME"] = service_name + + # Get environment name. + os.environ.setdefault("ENV", "local") + env = t.cast(Env, os.environ["ENV"]) + + # Load environment variables. + load_dotenv(f".env/.env.{env}", override=False) + load_dotenv(".env/.env", override=False) + + # Get secrets. + if env == "local": + secrets_path = ".env/.env.local.secrets" + # TODO: move this to the dev container setup script. + if not os.path.exists(secrets_path): + # pylint: disable=line-too-long + secrets_file_comment = ( + "# 📝 Local Secret Variables 📝\n" + "# These secret variables are only loaded in your local environment (on your PC).\n" + "#\n" + "# This file is git-ignored intentionally to keep these variables a secret.\n" + "#\n" + "# 🚫 DO NOT PUSH SECRETS TO THE CODE REPO 🚫\n" + "\n" + ) + # pylint: enable=line-too-long + + with open(secrets_path, "w+", encoding="utf-8") as secrets_file: + secrets_file.write(secrets_file_comment) + + secrets = dotenv_values(secrets_path) + else: + # pylint: disable-next=import-outside-toplevel + import boto3 + + s3: "S3Client" = boto3.client("s3") + secrets_object = s3.get_object( + Bucket=os.environ["aws_s3_app_bucket"], + Key=f"{os.environ['aws_s3_app_folder']}/secure/.env.secrets", + ) + + secrets = dotenv_values( + stream=StringIO(secrets_object["Body"].read().decode("utf-8")) + ) + + return Secrets(**secrets) diff --git a/codeforlife/settings/custom.py b/codeforlife/settings/custom.py index f90ac722..e693ffb7 100644 --- a/codeforlife/settings/custom.py +++ b/codeforlife/settings/custom.py @@ -3,13 +3,20 @@ """ import os +import typing as t +from pathlib import Path + +from ..types import Env + +# The name of the current environment. +ENV = t.cast(Env, os.getenv("ENV", "local")) + +# The base directory of the current service. +SERVICE_BASE_DIR = Path(os.getenv("SERVICE_BASE_DIR", "/")) # The name of the current service. SERVICE_NAME = os.getenv("SERVICE_NAME", "REPLACE_ME") -# If the current service the root service. This will only be true for portal. -SERVICE_IS_ROOT = bool(int(os.getenv("SERVICE_IS_ROOT", "0"))) - # The protocol, domain and port of the current service. SERVICE_PROTOCOL = os.getenv("SERVICE_PROTOCOL", "http") SERVICE_DOMAIN = os.getenv("SERVICE_DOMAIN", "localhost") @@ -18,18 +25,9 @@ # The base url of the current service. # The root service does not need its name included in the base url. SERVICE_BASE_URL = f"{SERVICE_PROTOCOL}://{SERVICE_DOMAIN}:{SERVICE_PORT}" -if not SERVICE_IS_ROOT: - SERVICE_BASE_URL += f"/{SERVICE_NAME}" - -# The api url of the current service. -SERVICE_API_URL = f"{SERVICE_BASE_URL}/api" - -# The website url of the current service. -SERVICE_SITE_URL = ( - "http://localhost:5173" - if SERVICE_DOMAIN == "localhost" - else SERVICE_BASE_URL -) + +# The frontend url of the current service. +SERVICE_SITE_URL = os.getenv("SERVICE_SITE_URL", "http://localhost:5173") # The authorization bearer token used to authenticate with Dotdigital. MAIL_AUTH = os.getenv("MAIL_AUTH", "REPLACE_ME") diff --git a/codeforlife/settings/django.py b/codeforlife/settings/django.py index 62bd8e80..64ece417 100644 --- a/codeforlife/settings/django.py +++ b/codeforlife/settings/django.py @@ -6,13 +6,12 @@ import json import os import typing as t -from pathlib import Path import boto3 from django.utils.translation import gettext_lazy as _ from ..types import JsonDict -from .custom import SERVICE_API_URL, SERVICE_NAME +from .custom import ENV, SERVICE_BASE_DIR, SERVICE_BASE_URL, SERVICE_NAME from .otp import APP_ID, AWS_S3_APP_BUCKET, AWS_S3_APP_FOLDER if t.TYPE_CHECKING: @@ -41,11 +40,17 @@ def get_databases(): The database configs. """ - if AWS_S3_APP_BUCKET and AWS_S3_APP_FOLDER and APP_ID: + if ENV == "local": + name = os.getenv("DB_NAME", SERVICE_NAME) + user = os.getenv("DB_USER", "root") + password = os.getenv("DB_PASSWORD", "password") + host = os.getenv("DB_HOST", "localhost") + port = int(os.getenv("DB_PORT", "5432")) + else: # Get the dbdata object. s3: "S3Client" = boto3.client("s3") db_data_object = s3.get_object( - Bucket=AWS_S3_APP_BUCKET, + Bucket=t.cast(str, AWS_S3_APP_BUCKET), Key=f"{AWS_S3_APP_FOLDER}/dbMetadata/{APP_ID}/app.dbdata", ) @@ -56,17 +61,11 @@ def get_databases(): if not db_data or db_data["DBEngine"] != "postgres": raise ConnectionAbortedError("Invalid database data.") - name = db_data["Database"] - user = db_data["user"] - password = db_data["password"] - host = db_data["Endpoint"] - port = db_data["Port"] - else: - name = os.getenv("DB_NAME", SERVICE_NAME) - user = os.getenv("DB_USER", "root") - password = os.getenv("DB_PASSWORD", "password") - host = os.getenv("DB_HOST", "localhost") - port = int(os.getenv("DB_PORT", "5432")) + name = t.cast(str, db_data["Database"]) + user = t.cast(str, db_data["user"]) + password = t.cast(str, db_data["password"]) + host = t.cast(str, db_data["Endpoint"]) + port = t.cast(int, db_data["Port"]) return { "default": { @@ -104,7 +103,7 @@ def get_databases(): # Auth # https://docs.djangoproject.com/en/3.2/topics/auth/default/ -LOGIN_URL = f"{SERVICE_API_URL}/session/expired/" +LOGIN_URL = f"{SERVICE_BASE_URL}/session/expired/" # Authentication backends # https://docs.djangoproject.com/en/3.2/ref/settings/#authentication-backends @@ -243,25 +242,14 @@ def get_databases(): "corsheaders", "rest_framework", "django_filters", + "storages", ] # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ - -def get_static_root(base_dir: Path): - """Get the static root for the Django project. - - Args: - base_dir: The base directory of the Django project. - - Returns: - The static root for the django project. - """ - return base_dir / "static" - - -STATIC_URL = "/static/" +STATIC_ROOT = SERVICE_BASE_DIR / "static" +STATIC_URL = os.getenv("STATIC_URL", "/static/") # Templates # https://docs.djangoproject.com/en/3.2/ref/templates/ @@ -281,3 +269,12 @@ def get_static_root(base_dir: Path): }, }, ] + +# File storage +# https://docs.djangoproject.com/en/3.2/topics/files/#file-storage + +DEFAULT_FILE_STORAGE = ( + "django.core.files.storage.FileSystemStorage" + if ENV == "local" + else "storages.backends.s3.S3Storage" +) diff --git a/codeforlife/settings/third_party.py b/codeforlife/settings/third_party.py index 700f6eeb..b81fac77 100644 --- a/codeforlife/settings/third_party.py +++ b/codeforlife/settings/third_party.py @@ -2,6 +2,9 @@ This file contains custom settings defined by third party extensions. """ +import json +import os + from .django import DEBUG # CORS @@ -23,3 +26,45 @@ ], "DEFAULT_PAGINATION_CLASS": "codeforlife.pagination.LimitOffsetPagination", } + +# Django storages +# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings + +AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME") +AWS_S3_OBJECT_PARAMETERS = json.loads( + os.getenv("AWS_S3_OBJECT_PARAMETERS", "{}") +) +AWS_DEFAULT_ACL = os.getenv("AWS_DEFAULT_ACL") +AWS_QUERYSTRING_AUTH = bool(int(os.getenv("AWS_QUERYSTRING_AUTH", "1"))) +AWS_S3_MAX_MEMORY_SIZE = int(os.getenv("AWS_S3_MAX_MEMORY_SIZE", "0")) +AWS_QUERYSTRING_EXPIRE = int(os.getenv("AWS_QUERYSTRING_EXPIRE", "3600")) +AWS_S3_URL_PROTOCOL = os.getenv("AWS_S3_URL_PROTOCOL", "https:") +AWS_S3_FILE_OVERWRITE = bool(int(os.getenv("AWS_S3_FILE_OVERWRITE", "1"))) +AWS_LOCATION = os.getenv("AWS_LOCATION", "") +AWS_IS_GZIPPED = bool(int(os.getenv("AWS_IS_GZIPPED", "0"))) +GZIP_CONTENT_TYPES = os.getenv( + "GZIP_CONTENT_TYPES", + "(" + + ",".join( + [ + "text/css", + "text/javascript", + "application/javascript", + "application/x-javascript", + "image/svg+xml", + ] + ) + + ")", +) +AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME") +AWS_S3_USE_SSL = bool(int(os.getenv("AWS_S3_USE_SSL", "1"))) +AWS_S3_VERIFY = os.getenv("AWS_S3_VERIFY") +AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL") +AWS_S3_ADDRESSING_STYLE = os.getenv("AWS_S3_ADDRESSING_STYLE") +AWS_S3_PROXIES = os.getenv("AWS_S3_PROXIES") +AWS_S3_TRANSFER_CONFIG = os.getenv("AWS_S3_TRANSFER_CONFIG") +AWS_S3_CUSTOM_DOMAIN = os.getenv("AWS_S3_CUSTOM_DOMAIN") +AWS_CLOUDFRONT_KEY = os.getenv("AWS_CLOUDFRONT_KEY") +AWS_CLOUDFRONT_KEY_ID = os.getenv("AWS_CLOUDFRONT_KEY_ID") +AWS_S3_SIGNATURE_VERSION = os.getenv("AWS_S3_SIGNATURE_VERSION") +AWS_S3_CLIENT_CONFIG = os.getenv("AWS_S3_CLIENT_CONFIG") diff --git a/codeforlife/types.py b/codeforlife/types.py index 68524a19..6d2f9b1f 100644 --- a/codeforlife/types.py +++ b/codeforlife/types.py @@ -7,6 +7,8 @@ import typing as t +Env = t.Literal["local", "development", "staging", "production"] + Args = t.Tuple[t.Any, ...] KwArgs = t.Dict[str, t.Any] diff --git a/codeforlife/urls/patterns.py b/codeforlife/urls/patterns.py index 2ab36c4b..f7ac58c2 100644 --- a/codeforlife/urls/patterns.py +++ b/codeforlife/urls/patterns.py @@ -6,12 +6,14 @@ import typing as t from django.contrib import admin -from django.http import HttpResponse -from django.urls import URLPattern, URLResolver, include, path, re_path -from rest_framework import status +from django.urls import URLPattern, URLResolver, include, path -from ..settings import SERVICE_IS_ROOT, SERVICE_NAME -from ..views import CsrfCookieView, HealthCheckView, LogoutView +from ..views import ( + CsrfCookieView, + HealthCheckView, + LogoutView, + session_expired_view, +) UrlPatterns = t.List[t.Union[URLResolver, URLPattern]] @@ -33,73 +35,41 @@ def get_urlpatterns( """ urlpatterns: UrlPatterns = [ + path( + "health-check/", + health_check_view.as_view(), + name="health-check", + ), path( "admin/", admin.site.urls, name="admin", ), path( - "api/csrf/cookie/", + "csrf/cookie/", CsrfCookieView.as_view(), name="get-csrf-cookie", ), path( - "api/session/logout/", + "session/logout/", LogoutView.as_view(), name="logout", ), - # Django's default behavior with the @login_required decorator is to - # redirect users to the login template found in setting LOGIN_URL. - # Because we're using a React frontend, we want to return a - # 401-Unauthorized whenever a user's session-cookie expires so we can - # redirect them to the login page. Therefore, all login redirects will - # direct to this view which will return the desired 401. path( - "api/session/expired/", - lambda request: HttpResponse( - status=status.HTTP_401_UNAUTHORIZED, - ), + "session/expired/", + session_expired_view, name="session-expired", ), - path( - "api/", - include(api_url_patterns), - name="api", - ), + *api_url_patterns, ] if include_user_urls: urlpatterns.append( path( - "api/", + "", include("codeforlife.user.urls"), name="user", ) ) - health_check_path = path( - "health-check/", - health_check_view.as_view(), - name="health-check", - ) - - if SERVICE_IS_ROOT: - urlpatterns.append(health_check_path) - return urlpatterns - - return [ - health_check_path, - path( - f"{SERVICE_NAME}/", - include(urlpatterns), - name="service", - ), - re_path( - rf"^(?!{SERVICE_NAME}/).*", - lambda request: HttpResponse( - f'The base route is "{SERVICE_NAME}/".', - status=status.HTTP_404_NOT_FOUND, - ), - name="service-not-found", - ), - ] + return urlpatterns diff --git a/codeforlife/views/__init__.py b/codeforlife/views/__init__.py index 3df02f92..bfd4acbe 100644 --- a/codeforlife/views/__init__.py +++ b/codeforlife/views/__init__.py @@ -5,7 +5,8 @@ from .api import APIView, BaseAPIView from .base_login import BaseLoginView -from .common import CsrfCookieView, LogoutView +from .csrf import CsrfCookieView from .decorators import action, cron_job from .health_check import HealthCheckView from .model import BaseModelViewSet, ModelViewSet +from .session import LogoutView, session_expired_view diff --git a/codeforlife/views/common.py b/codeforlife/views/csrf.py similarity index 69% rename from codeforlife/views/common.py rename to codeforlife/views/csrf.py index ac84de68..818f7bb9 100644 --- a/codeforlife/views/common.py +++ b/codeforlife/views/csrf.py @@ -3,8 +3,6 @@ Created on 12/04/2024 at 16:51:36(+01:00). """ -from django.contrib.auth.views import LogoutView as _LogoutView -from django.http import JsonResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie from rest_framework.request import Request @@ -26,10 +24,3 @@ def get(self, request: Request): Return a response which Django will auto-insert a CSRF cookie into. """ return Response() - - -class LogoutView(_LogoutView): - """Override Django's logout view to always return a JSON response.""" - - def render_to_response(self, context, **response_kwargs): - return JsonResponse({}) diff --git a/codeforlife/views/session.py b/codeforlife/views/session.py new file mode 100644 index 00000000..1364f14d --- /dev/null +++ b/codeforlife/views/session.py @@ -0,0 +1,30 @@ +""" +© Ocado Group +Created on 06/12/2024 at 11:55:49(+00:00). + +Session views. +""" + +from django.contrib.auth.views import LogoutView as _LogoutView +from django.http import HttpRequest, HttpResponse, JsonResponse +from rest_framework import status + + +class LogoutView(_LogoutView): + """Override Django's logout view to always return a JSON response.""" + + def render_to_response(self, context, **response_kwargs): + return JsonResponse({}) + + +def session_expired_view(request: HttpRequest): + """ + Django's default behavior with the @login_required decorator is to redirect + users to the login template found in setting LOGIN_URL. Because we're using + a React frontend, we want to return a 401-Unauthorized whenever a user's + session-cookie expires so we can redirect them to the login page. Therefore, + all login redirects will direct to this view which will return the desired + 401. + """ + + return HttpResponse(status=status.HTTP_401_UNAUTHORIZED)