Merge branch 'staging' into 170505212-uwsgi-logs
This commit is contained in:
commit
2254e0dd01
@ -3,7 +3,7 @@
|
||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2019-12-18T22:26:52Z",
|
||||
"generated_at": "2020-01-09T16:55:07Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"base64_limit": 4.5,
|
||||
@ -25,15 +25,6 @@
|
||||
}
|
||||
],
|
||||
"results": {
|
||||
"Pipfile.lock": [
|
||||
{
|
||||
"hashed_secret": "526c14b5cd73155d72784fec39907f9b7d5ddcdd",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 4,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
],
|
||||
"README.md": [
|
||||
{
|
||||
"hashed_secret": "d141ce86b0584abb29ee7c24af9afb1e3d871f04",
|
||||
@ -111,15 +102,6 @@
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
"ssl/certificate-authority/ca.key": [
|
||||
{
|
||||
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 1,
|
||||
"type": "Private Key"
|
||||
}
|
||||
],
|
||||
"ssl/client-certs/atat.mil.key": [
|
||||
{
|
||||
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
|
||||
|
5
Pipfile
5
Pipfile
@ -8,7 +8,9 @@ webassets = "*"
|
||||
Unipath = "*"
|
||||
pendulum = "*"
|
||||
redis = "*"
|
||||
sqlalchemy = ">=1.3.0"
|
||||
sqlalchemy = ">=1.3.12"
|
||||
sqlalchemy-json = "*"
|
||||
pydantic = "*"
|
||||
alembic = "*"
|
||||
"psycopg2-binary" = "*"
|
||||
flask = "*"
|
||||
@ -30,6 +32,7 @@ msrestazure = "*"
|
||||
azure-mgmt-authorization = "*"
|
||||
azure-mgmt-managementgroups = "*"
|
||||
azure-mgmt-resource = "*"
|
||||
transitions = "*"
|
||||
|
||||
[dev-packages]
|
||||
bandit = "*"
|
||||
|
380
Pipfile.lock
generated
380
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c203c47b00f413fd40056ef6d2d8e51b37ad3ff5f7693db5eb170b7f8fd43234"
|
||||
"sha256": "63b8f9d203f306a6f0ff20514b024909aa7e64917e1befcc9ea79931b5b4bd34"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -39,10 +39,10 @@
|
||||
},
|
||||
"azure-common": {
|
||||
"hashes": [
|
||||
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
|
||||
"sha256:99ef36e74b6395329aada288764ce80504da16ecc8206cb9a72f55fb02e8b484"
|
||||
"sha256:184ad6a05a3089dfdc1ce07c1cbfa489bbc45b5f6f56e848cac0851e6443da21",
|
||||
"sha256:3d64e9ab995300f42abd5bc0ef02f02bab661321e394d4dbacb4382ea1fb2f72"
|
||||
],
|
||||
"version": "==1.1.23"
|
||||
"version": "==1.1.24"
|
||||
},
|
||||
"azure-graphrbac": {
|
||||
"hashes": [
|
||||
@ -218,10 +218,11 @@
|
||||
},
|
||||
"flask-assets": {
|
||||
"hashes": [
|
||||
"sha256:6031527b89fb3509d1581d932affa5a79dd348cfffb58d0aef99a43461d47847"
|
||||
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
|
||||
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.12"
|
||||
"version": "==2.0"
|
||||
},
|
||||
"flask-session": {
|
||||
"hashes": [
|
||||
@ -256,11 +257,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"isodate": {
|
||||
"hashes": [
|
||||
@ -339,10 +340,10 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.0.2"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"msrest": {
|
||||
"hashes": [
|
||||
@ -422,6 +423,26 @@
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:176885123dfdd8f7ab6e7ba1b66d4197de75ba830bb44d921af88b3d977b8aa5",
|
||||
"sha256:2b32a5f14558c36e39aeefda0c550bfc0f47fc32b4ce16d80dc4df2b33838ed8",
|
||||
"sha256:2eab7d548b0e530bf65bee7855ad8164c2f6a889975d5e9c4eefd1e7c98245dc",
|
||||
"sha256:479ca8dc7cc41418751bf10302ee0a1b1f8eedb2de6c4f4c0f3cf8372b204f9a",
|
||||
"sha256:59235324dd7dc5363a654cd14271ea8631f1a43de5d4fc29c782318fcc498002",
|
||||
"sha256:87673d1de790c8d5282153cab0b09271be77c49aabcedf3ac5ab1a1fd4dcbac0",
|
||||
"sha256:8a8e089aec18c26561e09ee6daf15a3cc06df05bdc67de60a8684535ef54562f",
|
||||
"sha256:b60f2b3b0e0dd74f1800a57d1bbd597839d16faf267e45fa4a5407b15d311085",
|
||||
"sha256:c0da48978382c83f9488c6bbe4350e065ea5c83e85ca5cfb8fa14ac11de3c296",
|
||||
"sha256:cbe284bd5ad67333d49ecc0dc27fa52c25b4c2fe72802a5c060b5f922db58bef",
|
||||
"sha256:d03df07b7611004140b0fef91548878c2b5f48c520a8cb76d11d20e9887a495e",
|
||||
"sha256:d4bb6a75abc2f04f6993124f1ed4221724c9dc3bd9df5cb54132e0b68775d375",
|
||||
"sha256:dacb79144bb3fdb57cf9435e1bd16c35586bc44256215cfaa33bf21565d926ae",
|
||||
"sha256:dd9359db7644317898816f6142f378aa48848dcc5cf14a481236235fde11a148"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3"
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
|
||||
@ -468,20 +489,20 @@
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
||||
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
||||
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
||||
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
||||
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
||||
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
||||
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
||||
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
||||
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
||||
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2"
|
||||
"version": "==5.3"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
@ -520,6 +541,27 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.3.12"
|
||||
},
|
||||
"sqlalchemy-json": {
|
||||
"hashes": [
|
||||
"sha256:d17952e771eecd9023c0f683d2a6aaa27ce1a6dbf57b0fe2bf4d5aef4c5dad1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.2.3"
|
||||
},
|
||||
"sqlalchemy-utils": {
|
||||
"hashes": [
|
||||
"sha256:4e637c88bf3ac5f99b7d72342092a1f636bea1287b2e3e17d441b0413771f86e"
|
||||
],
|
||||
"version": "==0.36.1"
|
||||
},
|
||||
"transitions": {
|
||||
"hashes": [
|
||||
"sha256:011afaefa1244177cad3d960d836c0c4a201403252371bd4c555cf8c17ce7d3c",
|
||||
"sha256:5566c9d32e438ee9eb1f046e3ac1a0b2689f32807b47859210162084d4c84ab7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.2"
|
||||
},
|
||||
"unipath": {
|
||||
"hashes": [
|
||||
"sha256:09839adcc72e8a24d4f76d63656f30b5a1f721fc40c9bcd79d8c67bdd8b47dae",
|
||||
@ -544,10 +586,11 @@
|
||||
},
|
||||
"webassets": {
|
||||
"hashes": [
|
||||
"sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db"
|
||||
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
|
||||
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.12.1"
|
||||
"version": "==2.0"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
@ -566,10 +609,10 @@
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==1.0.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -633,12 +676,12 @@
|
||||
},
|
||||
"beautifulsoup4": {
|
||||
"hashes": [
|
||||
"sha256:5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169",
|
||||
"sha256:6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931",
|
||||
"sha256:dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57"
|
||||
"sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a",
|
||||
"sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887",
|
||||
"sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.8.1"
|
||||
"version": "==4.8.2"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
@ -685,39 +728,39 @@
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:0cd13a6e98c37b510a2d34c8281d5e1a226aaf9b65b7d770ef03c63169965351",
|
||||
"sha256:1a4b6b6a2a3a6612e6361130c2cc3dc4378d8c221752b96167ccbad94b47f3cd",
|
||||
"sha256:2ee55e6dba516ddf6f484aa83ccabbb0adf45a18892204c23486938d12258cde",
|
||||
"sha256:3be5338a2eb4ef03c57f20917e1d12a1fd10e3853fed060b6d6b677cb3745898",
|
||||
"sha256:44b783b02db03c4777d8cf71bae19eadc171a6f2a96777d916b2c30a1eb3d070",
|
||||
"sha256:475bf7c4252af0a56e1abba9606f1e54127cdf122063095c75ab04f6f99cf45e",
|
||||
"sha256:47c81ee687eafc2f1db7f03fbe99aab81330565ebc62fb3b61edfc2216a550c8",
|
||||
"sha256:4a7f8e72b18f2aca288ff02255ce32cc830bc04d993efbc87abf6beddc9e56c0",
|
||||
"sha256:50197163a22fd17f79086e087a787883b3ec9280a509807daf158dfc2a7ded02",
|
||||
"sha256:56b13000acf891f700f5067512b804d1ec8c301d627486c678b903859d07f798",
|
||||
"sha256:79388ae29c896299b3567965dbcd93255f175c17c6c7bca38614d12718c47466",
|
||||
"sha256:79fd5d3d62238c4f583b75d48d53cdae759fe04d4fb18fe8b371d88ad2b6f8be",
|
||||
"sha256:7fe3e2fde2bf1d7ce25ebcd2d3de3650b8d60d9a73ce6dcef36e20191291613d",
|
||||
"sha256:81042a24f67b96e4287774014fa27220d8a4d91af1043389e4d73892efc89ac6",
|
||||
"sha256:81326f1095c53111f8afc95da281e1414185f4a538609a77ca50bdfa39a6c207",
|
||||
"sha256:8873dc0d8f42142ea9f20c27bbdc485190fff93823c6795be661703369e5877d",
|
||||
"sha256:88d2cbcb0a112f47eef71eb95460b6995da18e6f8ca50c264585abc2c473154b",
|
||||
"sha256:91f2491aeab9599956c45a77c5666d323efdec790bfe23fcceafcd91105d585a",
|
||||
"sha256:979daa8655ae5a51e8e7a24e7d34e250ae8309fd9719490df92cbb2fe2b0422b",
|
||||
"sha256:9c871b006c878a890c6e44a5b2f3c6291335324b298c904dc0402ee92ee1f0be",
|
||||
"sha256:a6d092545e5af53e960465f652e00efbf5357adad177b2630d63978d85e46a72",
|
||||
"sha256:b5ed7837b923d1d71c4f587ae1539ccd96bfd6be9788f507dbe94dab5febbb5d",
|
||||
"sha256:ba259f68250f16d2444cbbfaddaa0bb20e1560a4fdaad50bece25c199e6af864",
|
||||
"sha256:be1d89614c6b6c36d7578496dc8625123bda2ff44f224cf8b1c45b810ee7383f",
|
||||
"sha256:c1b030a79749aa8d1f1486885040114ee56933b15ccfc90049ba266e4aa2139f",
|
||||
"sha256:c95bb147fab76f2ecde332d972d8f4138b8f2daee6c466af4ff3b4f29bd4c19e",
|
||||
"sha256:d52c1c2d7e856cecc05aa0526453cb14574f821b7f413cc279b9514750d795c1",
|
||||
"sha256:d609a6d564ad3d327e9509846c2c47f170456344521462b469e5cb39e48ba31c",
|
||||
"sha256:e1bad043c12fb58e8c7d92b3d7f2f49977dcb80a08a6d1e7a5114a11bf819fca",
|
||||
"sha256:e5a675f6829c53c87d79117a8eb656cc4a5f8918185a32fc93ba09778e90f6db",
|
||||
"sha256:fec32646b98baf4a22fdceb08703965bd16dea09051fbeb31a04b5b6e72b846c"
|
||||
"sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
|
||||
"sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
|
||||
"sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
|
||||
"sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
|
||||
"sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
|
||||
"sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
|
||||
"sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
|
||||
"sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
|
||||
"sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
|
||||
"sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
|
||||
"sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
|
||||
"sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
|
||||
"sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
|
||||
"sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
|
||||
"sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
|
||||
"sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
|
||||
"sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
|
||||
"sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
|
||||
"sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
|
||||
"sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
|
||||
"sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
|
||||
"sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
|
||||
"sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
|
||||
"sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
|
||||
"sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
|
||||
"sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
|
||||
"sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
|
||||
"sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
|
||||
"sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
|
||||
"sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
|
||||
"sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
|
||||
],
|
||||
"version": "==5.0"
|
||||
"version": "==5.0.3"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
@ -750,10 +793,10 @@
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11",
|
||||
"sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"
|
||||
"sha256:047d4d1791bfb3756264da670d99df13d799bb36e7d88774b1585a82d05dbaec",
|
||||
"sha256:1b1a58961683b30c574520d0c739c4443e0ef6a185c04382e8cc888273dbebed"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
@ -794,11 +837,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"ipdb": {
|
||||
"hashes": [
|
||||
@ -809,11 +852,11 @@
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:190a279bd3d4fc585a611e9358a88f1048cc57fd688254a86f9461889ee152a6",
|
||||
"sha256:762d79a62b6aa96b04971e920543f558dfbeedc0468b899303c080c8068d4ac2"
|
||||
"sha256:0f4bcf18293fb666df8511feec0403bdb7e061a5842ea6e88a3177b0ceb34ead",
|
||||
"sha256:387686dd7fc9caf29d2fddcf3116c4b07a11d9025701d220c589a430b0171d8a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.10.2"
|
||||
"version": "==7.11.1"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
@ -838,10 +881,10 @@
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
"sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27",
|
||||
"sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"
|
||||
"sha256:1349c1e8c107095a55386628bb3b2a79422f3a2cab8381e34ce19909e0cf5064",
|
||||
"sha256:e909527104a903606dd63bea6e8e888833f0ef087057829b89a18364a856f807"
|
||||
],
|
||||
"version": "==0.15.1"
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
@ -918,29 +961,30 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
|
||||
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
|
||||
],
|
||||
"version": "==8.0.2"
|
||||
"version": "==8.1.0"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:0308c35fd16c96a81b8dfc4d09ec63b8fa607cfec087acf5aafb44c2c45197de",
|
||||
"sha256:39f7be2f89668d21b2bbab45ce5aa15e69bf8d6f3b46f9e1cc1a88e4fcc84f3d",
|
||||
"sha256:4223f576813c79a10d0fd14192c86f1b85e3bd235c93792f22ed811a20b5ee4e",
|
||||
"sha256:4c8f812a2fbefa96185933fbe05aa035e9cf791cf3a23bbdb6a219c80b60e0b1",
|
||||
"sha256:4ea9ee847ea5bb38ea275441f3aea7eeba1b96187a3f968ee359d33d9dcc0eda",
|
||||
"sha256:573c68df69f0e399fa57866a0b72989acf0a56c4008eee59c789c2ca5ea9df03",
|
||||
"sha256:588c0e38466306aa7dbe6522ceacf37dde8b13cfa5edde90be2ce382f078875f",
|
||||
"sha256:6d1bd2e675823a19e6bf72149540ab9851bfe698b796aea698fb926ab2bedd02",
|
||||
"sha256:aa8e3bd1540dd5c39ef580ec2146a9c99c45f7c62af890095fec9e87b5ca19fb",
|
||||
"sha256:b978ba1ea90d0abe2fc720ec9a41824b7d3a1304569bd58c9038d8d61dc4dfdb",
|
||||
"sha256:c85c5367c2e8247e06cc0aba84e3633e90f48e8a0677bc51b351e138b5ff80b1",
|
||||
"sha256:ce69577b424058bfa177df27213869f37c1e964c3e1ebd3b3d54f1d10b234c4d",
|
||||
"sha256:ec6eaf98a57624d96d9916352a5bad2d73959f6358fabf43838f7d1a4d2f8389"
|
||||
"sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a",
|
||||
"sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7",
|
||||
"sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2",
|
||||
"sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474",
|
||||
"sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0",
|
||||
"sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217",
|
||||
"sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749",
|
||||
"sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6",
|
||||
"sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf",
|
||||
"sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36",
|
||||
"sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b",
|
||||
"sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72",
|
||||
"sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1",
|
||||
"sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.760"
|
||||
"version": "==0.761"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@ -958,9 +1002,10 @@
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c"
|
||||
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
|
||||
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"pathtools": {
|
||||
"hashes": [
|
||||
@ -1013,10 +1058,10 @@
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
|
||||
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
|
||||
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
||||
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
||||
],
|
||||
"version": "==1.8.0"
|
||||
"version": "==1.8.1"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
@ -1066,11 +1111,11 @@
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
"sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d",
|
||||
"sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"
|
||||
"sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f",
|
||||
"sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.0"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"pytest-watch": {
|
||||
"hashes": [
|
||||
@ -1088,46 +1133,46 @@
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
||||
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
||||
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
||||
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
||||
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
||||
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
||||
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
||||
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
||||
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
||||
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
||||
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2"
|
||||
"version": "==5.3"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42",
|
||||
"sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2",
|
||||
"sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2",
|
||||
"sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49",
|
||||
"sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb",
|
||||
"sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15",
|
||||
"sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b",
|
||||
"sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434",
|
||||
"sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83",
|
||||
"sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91",
|
||||
"sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0",
|
||||
"sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e",
|
||||
"sha256:7c5e2efcf079c35ff266c3f3a6708834f88f9fd04a3c16b855e036b2b7b1b543",
|
||||
"sha256:8355eaa64724a0fdb010a1654b77cb3e375dc08b7f592cc4a1c05ac606aa481c",
|
||||
"sha256:999a885f7f5194464238ad5d74b05982acee54002f3aa775d8e0e8c5fb74c06c",
|
||||
"sha256:9fd2f4813eaa3e421e82819d38e5b634d900faff7ae5a80cd89ccff407175e69",
|
||||
"sha256:a2e1e53df7dd27943da2b512895125b33fb20f81862c9fed7b3bab2a1de684d1",
|
||||
"sha256:ab43bc0836820b7900dfffc025b996784aec26ec87dc1df4f95a40398760223f",
|
||||
"sha256:ba449b56fa419fb19bf2a2438adbd2433f27087a6fe115917eaf9cfca684d5b6",
|
||||
"sha256:d3f632cefad2cf247bd845794002585e3772288bfcb0dbac59fdecd32cd38b67",
|
||||
"sha256:d51311496061863caae2cfe120cf1ef37900019b86c89c2d75f0918e0b4b8bf3"
|
||||
"sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525",
|
||||
"sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b",
|
||||
"sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576",
|
||||
"sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5",
|
||||
"sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0",
|
||||
"sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35",
|
||||
"sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003",
|
||||
"sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d",
|
||||
"sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161",
|
||||
"sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26",
|
||||
"sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9",
|
||||
"sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1",
|
||||
"sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146",
|
||||
"sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f",
|
||||
"sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149",
|
||||
"sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351",
|
||||
"sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461",
|
||||
"sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b",
|
||||
"sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242",
|
||||
"sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c",
|
||||
"sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77"
|
||||
],
|
||||
"version": "==2019.12.19"
|
||||
"version": "==2020.1.8"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@ -1139,12 +1184,12 @@
|
||||
},
|
||||
"rope": {
|
||||
"hashes": [
|
||||
"sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969",
|
||||
"sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf",
|
||||
"sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"
|
||||
"sha256:52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203",
|
||||
"sha256:ae1fa2fd56f64f4cc9be46493ce54bed0dd12dee03980c61a4393d89d84029ad",
|
||||
"sha256:d2830142c2e046f5fc26a022fe680675b6f48f81c7fc1f03a950706e746e9dfe"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.14.0"
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"selenium": {
|
||||
"hashes": [
|
||||
@ -1205,29 +1250,30 @@
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
|
||||
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
|
||||
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
|
||||
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
|
||||
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
|
||||
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
|
||||
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
|
||||
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
|
||||
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
|
||||
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
|
||||
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
|
||||
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
|
||||
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
|
||||
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
|
||||
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
|
||||
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
|
||||
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
|
||||
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
|
||||
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
|
||||
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
|
||||
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
|
||||
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
|
||||
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
|
||||
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
|
||||
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
|
||||
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
|
||||
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
|
||||
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
|
||||
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
|
||||
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
|
||||
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
|
||||
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
|
||||
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
|
||||
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
|
||||
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
|
||||
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
|
||||
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
|
||||
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
|
||||
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
|
||||
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
|
||||
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
|
||||
],
|
||||
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.4.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
@ -1252,10 +1298,10 @@
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
||||
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
||||
],
|
||||
"version": "==0.1.7"
|
||||
"version": "==0.1.8"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
@ -1273,10 +1319,10 @@
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
"version": "==1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
70
PortfolioProvision.md
Normal file
70
PortfolioProvision.md
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
Each CSP will have a set of "stages" that are required to be completed before the provisioning process can be considered complete.
|
||||
|
||||
Azure Stages:
|
||||
tenant,
|
||||
billing profile,
|
||||
admin subscription
|
||||
etc.
|
||||
|
||||
`atst.models.mixins.state_machines` module contains:
|
||||
|
||||
python Enum classes that define the stages for a CSP
|
||||
|
||||
class AzureStages(Enum):
|
||||
TENANT = "tenant"
|
||||
BILLING_PROFILE = "billing profile"
|
||||
ADMIN_SUBSCRIPTION = "admin subscription"
|
||||
|
||||
there are two types of python dataclass subclasses defined in `atst.models.portoflio_state_machine` module.
|
||||
|
||||
one holds the data that is submitted to the CSP
|
||||
|
||||
@dataclass
|
||||
class TenantCSPPayload():
|
||||
user_id: str
|
||||
password: str
|
||||
etc.
|
||||
|
||||
the other holds the results of the call to the CSP
|
||||
@dataclass
|
||||
class TenantCSPResult():
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
user_object_id: str
|
||||
etc.
|
||||
|
||||
A Finite State Machine `atst.models.portoflio_state_machine.PortfolioStateMachine` is created for each provisioning process and tied to an instance of Portfolio class.
|
||||
|
||||
Aach time the FSM is created/accessed it will generate a list of States and Transitions between the states.
|
||||
|
||||
There is a set of "system" states such as UNSTARTED, STARTING, STARTED, COMPLETED, FAILED etc
|
||||
|
||||
There is a set of CSP specific states generated for each "stage" in the FSM.
|
||||
TENANT_IN_PROGRESS
|
||||
TENANT_IN_COMPLETED
|
||||
TENANT_IN_FAILED
|
||||
BILLING_PROFILE_IN_PROGRESS
|
||||
BILLING_PROFILE_IN_COMPLETED
|
||||
BILLING_PROFILE_IN_FAILED
|
||||
etc.
|
||||
|
||||
There is a set of callbacks defined that are triggered as the process transitions between stages.
|
||||
|
||||
callback `PortfolioStateMachine.after_in_progress_callback`
|
||||
The CSP api call is made as the process transitions into IN_PROGESS state for each state.
|
||||
|
||||
callback `PortfolioStateMachine.is_csp_data_valid`
|
||||
validates the collected data.
|
||||
|
||||
A transition into the next state can be triggered using PortfolioStateMachine.trigger_next_transition`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
47
README.md
47
README.md
@ -362,50 +362,3 @@ fi
|
||||
|
||||
Also note that if the line number of a previously whitelisted secret changes, the whitelist file, `.secrets.baseline`, will be updated and needs to be committed.
|
||||
|
||||
## Local Kubernetes Setup
|
||||
|
||||
A modified version of the Kubernetes cluster can be deployed locally for
|
||||
testing and development purposes.
|
||||
|
||||
It is strongly recommended that you backup your local K8s config (usually
|
||||
`~/.kube/config`) before launching Minikube for the first time.
|
||||
|
||||
Before beginning:
|
||||
|
||||
- install the [Docker CLI](https://docs.docker.com/v17.12/install/)
|
||||
- install [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/)
|
||||
(this will also require installing a Hypervisor, such as VirtualBox)
|
||||
|
||||
### Setup
|
||||
|
||||
Run
|
||||
|
||||
```
|
||||
script/minikube_setup
|
||||
```
|
||||
|
||||
Once the script exits successfully, run
|
||||
|
||||
```
|
||||
minikube service list
|
||||
```
|
||||
|
||||
### Access the site
|
||||
|
||||
One of the two URLs given for the `atat-auth` service will load an HTTP version
|
||||
of the application.
|
||||
|
||||
For HTTP basic auth, the username and password are both `minikube`.
|
||||
|
||||
### Differences from the main config
|
||||
|
||||
As of the time of writing, this setup does not include the following:
|
||||
|
||||
- SSL/TLS or the complete DoD PKI
|
||||
- the cronjob for syncing CRLs and the peristent storage
|
||||
- production configuration
|
||||
|
||||
In order for the application to run, the K8s config for Minikube includes an
|
||||
additional deployment resource called `datastores`. This includes Postgres
|
||||
and Redis containers. It also includes hard-coded versions of the K8s secrets
|
||||
used in the regular clusters.
|
||||
|
113
alembic/versions/59973fa17ded_portfolio_state_machine_table.py
Normal file
113
alembic/versions/59973fa17ded_portfolio_state_machine_table.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""portfolio state machine table.
|
||||
|
||||
Revision ID: 59973fa17ded
|
||||
Revises: 828d8c188dce
|
||||
Create Date: 2020-01-08 10:37:32.924245
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
import sqlalchemy_json
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "59973fa17ded" # pragma: allowlist secret
|
||||
down_revision = "828d8c188dce" # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"portfolio_job_failures",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("task_id", sa.String(), nullable=False),
|
||||
sa.Column("portfolio_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["portfolio_id"], ["portfolios.id"],),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"portfolio_state_machines",
|
||||
sa.Column(
|
||||
"time_created",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"time_updated",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"deleted", sa.Boolean(), server_default=sa.text("false"), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"id",
|
||||
postgresql.UUID(as_uuid=True),
|
||||
server_default=sa.text("uuid_generate_v4()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("portfolio_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column(
|
||||
"state",
|
||||
sa.Enum(
|
||||
"UNSTARTED",
|
||||
"STARTING",
|
||||
"STARTED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"TENANT_CREATED",
|
||||
"TENANT_IN_PROGRESS",
|
||||
"TENANT_FAILED",
|
||||
"BILLING_PROFILE_CREATED",
|
||||
"BILLING_PROFILE_IN_PROGRESS",
|
||||
"BILLING_PROFILE_FAILED",
|
||||
"ADMIN_SUBSCRIPTION_CREATED",
|
||||
"ADMIN_SUBSCRIPTION_IN_PROGRESS",
|
||||
"ADMIN_SUBSCRIPTION_FAILED",
|
||||
name="fsmstates",
|
||||
native_enum=False,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["portfolio_id"], ["portfolios.id"],),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.add_column("portfolios", sa.Column("app_migration", sa.String(), nullable=True))
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("complexity", sa.ARRAY(sa.String()), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("complexity_other", sa.String(), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"portfolios",
|
||||
sa.Column("csp_data", sqlalchemy_json.NestedMutableJson(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("dev_team", sa.ARRAY(sa.String()), nullable=True)
|
||||
)
|
||||
op.add_column("portfolios", sa.Column("dev_team_other", sa.String(), nullable=True))
|
||||
op.add_column("portfolios", sa.Column("native_apps", sa.String(), nullable=True))
|
||||
op.add_column(
|
||||
"portfolios", sa.Column("team_experience", sa.String(), nullable=True)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("portfolios", "team_experience")
|
||||
op.drop_column("portfolios", "native_apps")
|
||||
op.drop_column("portfolios", "dev_team_other")
|
||||
op.drop_column("portfolios", "dev_team")
|
||||
op.drop_column("portfolios", "csp_data")
|
||||
op.drop_column("portfolios", "complexity_other")
|
||||
op.drop_column("portfolios", "complexity")
|
||||
op.drop_column("portfolios", "app_migration")
|
||||
op.drop_table("portfolio_state_machines")
|
||||
op.drop_table("portfolio_job_failures")
|
||||
# ### end Alembic commands ###
|
28
alembic/versions/5d7198d34b91_remove_users_provisional.py
Normal file
28
alembic/versions/5d7198d34b91_remove_users_provisional.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""remove users.provisional
|
||||
|
||||
Revision ID: 5d7198d34b91
|
||||
Revises: 02ac8bdcf16f
|
||||
Create Date: 2020-01-09 08:42:34.512191
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5d7198d34b91' # pragma: allowlist secret
|
||||
down_revision = '02ac8bdcf16f' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'provisional')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('provisional', sa.BOOLEAN(), autoincrement=False, nullable=True))
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,58 @@
|
||||
"""update environment_roles enum list
|
||||
|
||||
Revision ID: 828d8c188dce
|
||||
Revises: 5d7198d34b91
|
||||
Create Date: 2020-01-08 16:08:03.879881
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '828d8c188dce' # pragma: allowlist secret
|
||||
down_revision = '5d7198d34b91' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE environment_roles
|
||||
SET role = NULL
|
||||
"""
|
||||
)
|
||||
|
||||
op.alter_column(
|
||||
"environment_roles",
|
||||
"role",
|
||||
type_=sa.Enum(
|
||||
"ADMIN",
|
||||
"BILLING_READ",
|
||||
"CONTRIBUTOR",
|
||||
name="role",
|
||||
native_enum=False,
|
||||
),
|
||||
existing_type=sa.VARCHAR(),
|
||||
nullable=True,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"environment_roles",
|
||||
"status",
|
||||
type_=sa.VARCHAR(),
|
||||
existing_type=sa.Enum(
|
||||
"ADMIN",
|
||||
"BILLING_READ",
|
||||
"CONTRIBUTOR",
|
||||
name="status",
|
||||
native_enum=False,
|
||||
),
|
||||
)
|
||||
# ### end Alembic commands ###
|
@ -159,6 +159,7 @@ def map_config(config):
|
||||
"ENV": config["default"]["ENVIRONMENT"],
|
||||
"BROKER_URL": config["default"]["REDIS_URI"],
|
||||
"DEBUG": config["default"].getboolean("DEBUG"),
|
||||
"DEBUG_MAILER": config["default"].getboolean("DEBUG_MAILER"),
|
||||
"SQLALCHEMY_ECHO": config["default"].getboolean("SQLALCHEMY_ECHO"),
|
||||
"PORT": int(config["default"]["PORT"]),
|
||||
"SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"],
|
||||
@ -289,7 +290,7 @@ def make_crl_validator(app):
|
||||
|
||||
|
||||
def make_mailer(app):
|
||||
if app.config["DEBUG"]:
|
||||
if app.config["DEBUG"] or app.config["DEBUG_MAILER"]:
|
||||
mailer_connection = mailer.RedisConnection(app.redis)
|
||||
else:
|
||||
mailer_connection = mailer.SMTPConnection(
|
||||
|
@ -1,4 +1,4 @@
|
||||
from flask import g, redirect, url_for, session, request
|
||||
from flask import g, redirect, url_for, session, request, current_app as app
|
||||
|
||||
from atst.domain.users import Users
|
||||
|
||||
@ -59,8 +59,10 @@ def get_last_login():
|
||||
|
||||
def logout():
|
||||
if session.get("user_id"): # pragma: no branch
|
||||
dod_id = g.current_user.dod_id
|
||||
del session["user_id"]
|
||||
del session["last_login"]
|
||||
app.logger.info(f"user with EDIPI {dod_id} has logged out")
|
||||
|
||||
|
||||
def _unprotected_route(request):
|
||||
|
@ -1,3 +1,5 @@
|
||||
import importlib
|
||||
|
||||
from .cloud import MockCloudProvider
|
||||
from .file_uploads import AzureUploader, MockUploader
|
||||
from .reports import MockReportingProvider
|
||||
@ -29,3 +31,22 @@ def make_csp_provider(app, csp=None):
|
||||
app.csp = MockCSP(app, test_mode=True)
|
||||
else:
|
||||
app.csp = MockCSP(app)
|
||||
|
||||
|
||||
def _stage_to_classname(stage):
|
||||
return "".join(
|
||||
map(lambda word: word.capitalize(), stage.replace("_", " ").split(" "))
|
||||
)
|
||||
|
||||
|
||||
def get_stage_csp_class(stage, class_type):
|
||||
"""
|
||||
given a stage name and class_type return the class
|
||||
class_type is either 'payload' or 'result'
|
||||
|
||||
"""
|
||||
cls_name = "".join([_stage_to_classname(stage), "CSP", class_type.capitalize()])
|
||||
try:
|
||||
return getattr(importlib.import_module("atst.domain.csp.cloud"), cls_name)
|
||||
except AttributeError:
|
||||
print("could not import CSP Result class <%s>" % cls_name)
|
||||
|
@ -1,12 +1,12 @@
|
||||
from typing import Dict
|
||||
import re
|
||||
from typing import Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from atst.models.user import User
|
||||
from atst.models.application import Application
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from .policy import AzurePolicyManager
|
||||
|
||||
|
||||
class GeneralCSPException(Exception):
|
||||
@ -142,6 +142,97 @@ class BaselineProvisionException(GeneralCSPException):
|
||||
)
|
||||
|
||||
|
||||
class BaseCSPPayload(BaseModel):
|
||||
# {"username": "mock-cloud", "pass": "shh"}
|
||||
creds: Dict
|
||||
|
||||
|
||||
class TenantCSPPayload(BaseCSPPayload):
|
||||
user_id: str
|
||||
password: str
|
||||
domain_name: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
country_code: str
|
||||
password_recovery_email_address: str
|
||||
|
||||
|
||||
class TenantCSPResult(BaseModel):
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
user_object_id: str
|
||||
|
||||
|
||||
class BillingProfileAddress(BaseModel):
|
||||
address: Dict
|
||||
"""
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"""
|
||||
|
||||
|
||||
class BillingProfileCLINBudget(BaseModel):
|
||||
clinBudget: Dict
|
||||
"""
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class BillingProfileCSPPayload(
|
||||
BaseCSPPayload, BillingProfileAddress, BillingProfileCLINBudget
|
||||
):
|
||||
displayName: str
|
||||
poNumber: str
|
||||
invoiceEmailOptIn: str
|
||||
|
||||
"""
|
||||
{
|
||||
"displayName": "string",
|
||||
"poNumber": "string",
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"invoiceEmailOptIn": true,
|
||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class CloudProviderInterface:
|
||||
def root_creds(self) -> Dict:
|
||||
raise NotImplementedError()
|
||||
@ -325,6 +416,68 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
return {"id": self._id(), "credentials": self._auth_credentials}
|
||||
|
||||
def create_tenant(self, payload):
|
||||
"""
|
||||
payload is an instance of TenantCSPPayload data class
|
||||
"""
|
||||
|
||||
self._authorize(payload.creds)
|
||||
|
||||
self._delay(1, 5)
|
||||
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
# return tenant id, tenant owner id and tenant owner object id from:
|
||||
response = {"tenantId": "string", "userId": "string", "objectId": "string"}
|
||||
return {
|
||||
"tenant_id": response["tenantId"],
|
||||
"user_id": response["userId"],
|
||||
"user_object_id": response["objectId"],
|
||||
}
|
||||
|
||||
def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
|
||||
# call billing profile creation endpoint, specifying owner
|
||||
# Payload:
|
||||
"""
|
||||
{
|
||||
"displayName": "string",
|
||||
"poNumber": "string",
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"invoiceEmailOptIn": true,
|
||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
# response will be mostly the same as the body, but we only really care about the id
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
response = {"id": "string"}
|
||||
return {"billing_profile_id": response["id"]}
|
||||
|
||||
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
|
||||
self._authorize(auth_credentials)
|
||||
|
||||
@ -401,18 +554,15 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
|
||||
|
||||
class AzureSDKProvider(object):
|
||||
def __init__(self):
|
||||
from azure.mgmt import subscription, authorization, managementgroups
|
||||
from azure.mgmt.resource import policy
|
||||
from azure.mgmt import subscription, authorization
|
||||
import azure.graphrbac as graphrbac
|
||||
import azure.common.credentials as credentials
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
|
||||
self.subscription = subscription
|
||||
self.authorization = authorization
|
||||
self.managementgroups = managementgroups
|
||||
self.graphrbac = graphrbac
|
||||
self.credentials = credentials
|
||||
self.policy = policy
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
|
||||
@ -430,28 +580,45 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
else:
|
||||
self.sdk = azure_sdk_provider
|
||||
|
||||
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||
|
||||
def create_environment(
|
||||
self, auth_credentials: Dict, user: User, environment: Environment
|
||||
):
|
||||
# since this operation would only occur within a tenant, should we source the tenant
|
||||
# via lookup from environment once we've created the portfolio csp data schema
|
||||
# something like this:
|
||||
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
|
||||
# though we'd probably source the whole credentials for these calls from the portfolio csp
|
||||
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
|
||||
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
|
||||
credentials = self._get_credential_obj(self._root_creds)
|
||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
||||
management_group_id = "?" # management group id chained from environment
|
||||
parent_id = "?" # from environment.application
|
||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||
|
||||
management_group = self._create_management_group(
|
||||
credentials, management_group_id, display_name, parent_id,
|
||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
||||
|
||||
billing_profile_id = "?" # something chained from environment?
|
||||
sku_id = AZURE_SKU_ID
|
||||
# we want to set AT-AT as an owner here
|
||||
# we could potentially associate subscriptions with "management groups" per DOD component
|
||||
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
||||
display_name,
|
||||
billing_profile_id,
|
||||
sku_id,
|
||||
# owner=<AdPrincipal: for AT-AT user>
|
||||
)
|
||||
|
||||
return management_group
|
||||
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
||||
# TOs filed for the environment
|
||||
billing_account_name = "?"
|
||||
invoice_section_name = "?"
|
||||
# We may also want to create billing sections in the enrollment account
|
||||
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
||||
billing_account_name, invoice_section_name, body
|
||||
)
|
||||
|
||||
# the resulting object from this process is a link to the new subscription
|
||||
# not a subscription model, so we'll have to unpack the ID
|
||||
new_sub = sub_creation_operation.result()
|
||||
|
||||
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
||||
if subscription_id:
|
||||
return subscription_id
|
||||
else:
|
||||
# troublesome error, subscription should exist at this point
|
||||
# but we just don't have a valid ID
|
||||
pass
|
||||
|
||||
def create_atat_admin_user(
|
||||
self, auth_credentials: Dict, csp_environment_id: str
|
||||
@ -490,126 +657,135 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"role_name": role_assignment_id,
|
||||
}
|
||||
|
||||
def _create_application(self, auth_credentials: Dict, application: Application):
|
||||
management_group_name = str(uuid4()) # can be anything, not just uuid
|
||||
display_name = application.name # Does this need to be unique?
|
||||
credentials = self._get_credential_obj(auth_credentials)
|
||||
parent_id = "?" # application.portfolio.csp_details.management_group_id
|
||||
def create_tenant(self, payload):
|
||||
# auth as SP that is allowed to create tenant? (tenant creation sp creds)
|
||||
# create tenant with owner details (populated from portfolio point of contact, pw is generated)
|
||||
|
||||
return self._create_management_group(
|
||||
credentials, management_group_name, display_name, parent_id,
|
||||
# return tenant id, tenant owner id and tenant owner object id from:
|
||||
response = {"tenantId": "string", "userId": "string", "objectId": "string"}
|
||||
return self._ok(
|
||||
{
|
||||
"tenant_id": response["tenantId"],
|
||||
"user_id": response["userId"],
|
||||
"user_object_id": response["objectId"],
|
||||
}
|
||||
)
|
||||
|
||||
def _create_management_group(
|
||||
self, credentials, management_group_id, display_name, parent_id=None,
|
||||
):
|
||||
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
|
||||
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
|
||||
id=parent_id
|
||||
)
|
||||
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
|
||||
parent=create_parent_grp_info
|
||||
)
|
||||
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
|
||||
name=management_group_id,
|
||||
display_name=display_name,
|
||||
details=create_mgmt_grp_details,
|
||||
)
|
||||
create_request = mgmgt_group_client.management_groups.create_or_update(
|
||||
management_group_id, mgmt_grp_create
|
||||
)
|
||||
def create_billing_owner(self, creds, tenant_admin_details):
|
||||
# authenticate as tenant_admin
|
||||
# create billing owner identity
|
||||
|
||||
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
|
||||
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
|
||||
return create_request.result()
|
||||
# TODO: Lookup response format
|
||||
# Managed service identity?
|
||||
response = {"id": "string"}
|
||||
return self._ok({"billing_owner_id": response["id"]})
|
||||
|
||||
def _create_subscription(
|
||||
self,
|
||||
credentials,
|
||||
display_name,
|
||||
billing_profile_id,
|
||||
sku_id,
|
||||
management_group_id,
|
||||
billing_account_name,
|
||||
invoice_section_name,
|
||||
):
|
||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||
|
||||
billing_profile_id = "?" # where do we source this?
|
||||
sku_id = AZURE_SKU_ID
|
||||
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
||||
# TOs filed for the environment
|
||||
billing_account_name = "?" # from TO?
|
||||
invoice_section_name = "?" # from TO?
|
||||
|
||||
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
||||
display_name=display_name,
|
||||
billing_profile_id=billing_profile_id,
|
||||
sku_id=sku_id,
|
||||
management_group_id=management_group_id,
|
||||
)
|
||||
|
||||
# We may also want to create billing sections in the enrollment account
|
||||
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
||||
billing_account_name, invoice_section_name, body
|
||||
)
|
||||
|
||||
# the resulting object from this process is a link to the new subscription
|
||||
# not a subscription model, so we'll have to unpack the ID
|
||||
new_sub = sub_creation_operation.result()
|
||||
|
||||
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
||||
if subscription_id:
|
||||
return subscription_id
|
||||
else:
|
||||
# troublesome error, subscription should exist at this point
|
||||
# but we just don't have a valid ID
|
||||
pass
|
||||
|
||||
AZURE_MANAGEMENT_API = "https://management.azure.com"
|
||||
|
||||
def _create_policy_definition(
|
||||
self, credentials, subscription_id, management_group_id, properties,
|
||||
):
|
||||
def assign_billing_owner(self, creds, billing_owner_id, tenant_id):
|
||||
# TODO: Do we source role definition ID from config, api or self-defined?
|
||||
# TODO: If from api,
|
||||
"""
|
||||
Requires credentials that have AZURE_MANAGEMENT_API
|
||||
specified as the resource. The Service Principal
|
||||
specified in the credentials must have the "Resource
|
||||
Policy Contributor" role assigned with a scope at least
|
||||
as high as the management group specified by
|
||||
management_group_id.
|
||||
|
||||
Arguments:
|
||||
credentials -- ServicePrincipalCredentials
|
||||
subscription_id -- str, ID of the subscription (just the UUID, not the path)
|
||||
management_group_id -- str, ID of the management group (just the UUID, not the path)
|
||||
properties -- dictionary, the "properties" section of a valid Azure policy definition document
|
||||
|
||||
Returns:
|
||||
azure.mgmt.resource.policy.[api version].models.PolicyDefinition: the PolicyDefinition object provided to Azure
|
||||
|
||||
Raises:
|
||||
TBD
|
||||
{
|
||||
"principalId": "string",
|
||||
"principalTenantId": "string",
|
||||
"billingRoleDefinitionId": "string"
|
||||
}
|
||||
"""
|
||||
# TODO: which subscription would this be?
|
||||
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
|
||||
|
||||
definition = client.policy_definitions.models.PolicyDefinition(
|
||||
policy_type=properties.get("policyType"),
|
||||
mode=properties.get("mode"),
|
||||
display_name=properties.get("displayName"),
|
||||
description=properties.get("description"),
|
||||
policy_rule=properties.get("policyRule"),
|
||||
parameters=properties.get("parameters"),
|
||||
return self.ok()
|
||||
|
||||
def create_billing_profile(self, creds, tenant_admin_details, billing_owner_id):
|
||||
# call billing profile creation endpoint, specifying owner
|
||||
# Payload:
|
||||
"""
|
||||
{
|
||||
"displayName": "string",
|
||||
"poNumber": "string",
|
||||
"address": {
|
||||
"firstName": "string",
|
||||
"lastName": "string",
|
||||
"companyName": "string",
|
||||
"addressLine1": "string",
|
||||
"addressLine2": "string",
|
||||
"addressLine3": "string",
|
||||
"city": "string",
|
||||
"region": "string",
|
||||
"country": "string",
|
||||
"postalCode": "string"
|
||||
},
|
||||
"invoiceEmailOptIn": true,
|
||||
Note: These last 2 are also the body for adding/updating new TOs/clins
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# response will be mostly the same as the body, but we only really care about the id
|
||||
response = {"id": "string"}
|
||||
return self._ok({"billing_profile_id": response["id"]})
|
||||
|
||||
def report_clin(self, creds, clin_id, clin_amount, clin_start, clin_end, clin_to):
|
||||
# should consumer be responsible for reporting each clin or
|
||||
# should this take a list and manage the sequential reporting?
|
||||
""" Payload
|
||||
{
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"skuId": "string"
|
||||
}
|
||||
],
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# we don't need any of the returned info for this
|
||||
return self._ok()
|
||||
|
||||
def create_remote_admin(self, creds, tenant_details):
|
||||
# create app/service principal within tenant, with name constructed from tenant details
|
||||
# assign principal global admin
|
||||
|
||||
# needs to call out to CLI with tenant owner username/password, prototyping for that underway
|
||||
|
||||
# return identifier and creds to consumer for storage
|
||||
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"}
|
||||
return self._ok(
|
||||
{
|
||||
"client_id": response["clientId"],
|
||||
"secret_key": response["secret_key"],
|
||||
"tenant_id": response["tenantId"],
|
||||
}
|
||||
)
|
||||
|
||||
name = properties.get("displayName")
|
||||
def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
|
||||
# use creds to update to force password recovery?
|
||||
# not sure what the endpoint/method for this is, yet
|
||||
|
||||
return client.policy_definitions.create_or_update_at_management_group(
|
||||
policy_definition_name=name,
|
||||
parameters=definition,
|
||||
management_group_id=management_group_id,
|
||||
)
|
||||
return self._ok()
|
||||
|
||||
def create_billing_alerts(self, TBD):
|
||||
# TODO: Add azure-mgmt-consumption for Budget and Notification entities/operations
|
||||
# TODO: Determine how to auth against that API using the SDK, doesn't seeem possible at the moment
|
||||
# TODO: billing alerts are registered as Notifications on Budget objects, which have start/end dates
|
||||
# TODO: determine what the keys in the Notifications dict are supposed to be
|
||||
# we may need to rotate budget objects when new TOs/CLINs are reported?
|
||||
|
||||
# we likely only want the budget ID, can be updated or replaced?
|
||||
response = {"id": "id"}
|
||||
return self._ok({"budget_id": response["id"]})
|
||||
|
||||
def _get_management_service_principal(self):
|
||||
# we really should be using graph.microsoft.com, but i'm getting
|
||||
@ -663,6 +839,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
return sub_id_match.group(1)
|
||||
|
||||
def _get_credential_obj(self, creds, resource=None):
|
||||
|
||||
return self.sdk.credentials.ServicePrincipalCredentials(
|
||||
client_id=creds.get("client_id"),
|
||||
secret=creds.get("secret_key"),
|
||||
@ -671,6 +848,27 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
cloud_environment=self.sdk.cloud,
|
||||
)
|
||||
|
||||
def _make_tenant_admin_cred_obj(self, username, password):
|
||||
return self.sdk.credentials.UserPassCredentials(username, password)
|
||||
|
||||
def _ok(self, body=None):
|
||||
return self._make_response("ok", body)
|
||||
|
||||
def _error(self, body=None):
|
||||
return self._make_response("error", body)
|
||||
|
||||
def _make_response(self, status, body=dict()):
|
||||
"""Create body for responses from API
|
||||
|
||||
Arguments:
|
||||
status {string} -- "ok" or "error"
|
||||
body {dict} -- dict containing details of response or error, if applicable
|
||||
|
||||
Returns:
|
||||
dict -- status of call with body containing details
|
||||
"""
|
||||
return {"status": status, "body": body}
|
||||
|
||||
@property
|
||||
def _root_creds(self):
|
||||
return {
|
||||
|
@ -2,4 +2,5 @@ from .portfolios import (
|
||||
Portfolios,
|
||||
PortfolioError,
|
||||
PortfolioDeletionApplicationsExistError,
|
||||
PortfolioStateMachines,
|
||||
)
|
||||
|
@ -1,11 +1,23 @@
|
||||
from sqlalchemy import or_
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from atst.database import db
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
from atst.domain.invitations import PortfolioInvitations
|
||||
from atst.models import Permissions, PortfolioRole, PortfolioRoleStatus
|
||||
|
||||
from .query import PortfoliosQuery
|
||||
from atst.domain.invitations import PortfolioInvitations
|
||||
from atst.models import (
|
||||
Portfolio,
|
||||
PortfolioStateMachine,
|
||||
FSMStates,
|
||||
Permissions,
|
||||
PortfolioRole,
|
||||
PortfolioRoleStatus,
|
||||
)
|
||||
|
||||
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
||||
from .scopes import ScopedPortfolio
|
||||
|
||||
|
||||
@ -17,7 +29,22 @@ class PortfolioDeletionApplicationsExistError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PortfolioStateMachines(object):
|
||||
@classmethod
|
||||
def create(cls, portfolio, **sm_attrs):
|
||||
sm_attrs.update({"portfolio": portfolio})
|
||||
sm = PortfolioStateMachinesQuery.create(**sm_attrs)
|
||||
return sm
|
||||
|
||||
|
||||
class Portfolios(object):
|
||||
@classmethod
|
||||
def get_or_create_state_machine(cls, portfolio):
|
||||
"""
|
||||
get or create Portfolio State Machine for a Portfolio
|
||||
"""
|
||||
return portfolio.state_machine or PortfolioStateMachines.create(portfolio)
|
||||
|
||||
@classmethod
|
||||
def create(cls, user, portfolio_attrs):
|
||||
portfolio = PortfoliosQuery.create(**portfolio_attrs)
|
||||
@ -75,10 +102,10 @@ class Portfolios(object):
|
||||
permission_sets = PortfolioRoles._permission_sets_for_names(
|
||||
member_data.get("permission_sets", [])
|
||||
)
|
||||
role = PortfolioRole(portfolio_id=portfolio.id, permission_sets=permission_sets)
|
||||
role = PortfolioRole(portfolio=portfolio, permission_sets=permission_sets)
|
||||
|
||||
invitation = PortfolioInvitations.create(
|
||||
inviter=inviter, role=role, member_data=member_data
|
||||
inviter=inviter, role=role, member_data=member_data["user_data"]
|
||||
)
|
||||
|
||||
PortfoliosQuery.add_and_commit(role)
|
||||
@ -111,3 +138,37 @@ class Portfolios(object):
|
||||
portfolio.description = new_data["description"]
|
||||
|
||||
PortfoliosQuery.add_and_commit(portfolio)
|
||||
|
||||
@classmethod
|
||||
def base_provision_query(cls):
|
||||
return db.session.query(Portfolio.id)
|
||||
|
||||
@classmethod
|
||||
def get_portfolios_pending_provisioning(cls) -> List[UUID]:
|
||||
"""
|
||||
Any portfolio with a corresponding State Machine that is either:
|
||||
not started yet,
|
||||
failed in creating a tenant
|
||||
failed
|
||||
"""
|
||||
|
||||
results = (
|
||||
cls.base_provision_query()
|
||||
.join(PortfolioStateMachine)
|
||||
.filter(
|
||||
or_(
|
||||
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
||||
PortfolioStateMachine.state == FSMStates.FAILED,
|
||||
PortfolioStateMachine.state == FSMStates.TENANT_FAILED,
|
||||
)
|
||||
)
|
||||
)
|
||||
return [id_ for id_, in results]
|
||||
|
||||
# db.session.query(PortfolioStateMachine).\
|
||||
# filter(
|
||||
# or_(
|
||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
||||
# )
|
||||
# ).all()
|
||||
|
@ -8,6 +8,13 @@ from atst.models.application_role import (
|
||||
Status as ApplicationRoleStatus,
|
||||
)
|
||||
from atst.models.application import Application
|
||||
from atst.models.portfolio_state_machine import PortfolioStateMachine
|
||||
|
||||
# from atst.models.application import Application
|
||||
|
||||
|
||||
class PortfolioStateMachinesQuery(Query):
|
||||
model = PortfolioStateMachine
|
||||
|
||||
|
||||
class PortfoliosQuery(Query):
|
||||
|
@ -117,12 +117,3 @@ class Users(object):
|
||||
user.last_session_id = session_id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def finalize(cls, user):
|
||||
user.provisional = False
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
return user
|
||||
|
@ -14,7 +14,7 @@ SERVICE_BRANCHES = [
|
||||
]
|
||||
|
||||
ENV_ROLE_NO_ACCESS = "No Access"
|
||||
ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [
|
||||
ENV_ROLES = [(role.name, role.value) for role in CSPRole] + [
|
||||
(ENV_ROLE_NO_ACCESS, ENV_ROLE_NO_ACCESS)
|
||||
]
|
||||
|
||||
|
@ -1,76 +1,59 @@
|
||||
from wtforms.validators import Required
|
||||
from wtforms.fields import StringField, FormField, FieldList, HiddenField
|
||||
from wtforms.fields import BooleanField, FormField
|
||||
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from .forms import BaseForm
|
||||
from .member import NewForm as BaseNewMemberForm
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.forms.fields import SelectField
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class PermissionsForm(BaseForm):
|
||||
member_name = StringField()
|
||||
member_id = HiddenField()
|
||||
perms_app_mgmt = SelectField(
|
||||
translate("forms.new_member.app_mgmt"),
|
||||
choices=[
|
||||
(
|
||||
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
translate("common.view"),
|
||||
),
|
||||
(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
translate("common.edit"),
|
||||
),
|
||||
],
|
||||
perms_app_mgmt = BooleanField(
|
||||
translate("forms.new_member.app_mgmt.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.app_mgmt.description"),
|
||||
)
|
||||
perms_funding = SelectField(
|
||||
translate("forms.new_member.funding"),
|
||||
choices=[
|
||||
(PermissionSets.VIEW_PORTFOLIO_FUNDING, translate("common.view")),
|
||||
(PermissionSets.EDIT_PORTFOLIO_FUNDING, translate("common.edit")),
|
||||
],
|
||||
perms_funding = BooleanField(
|
||||
translate("forms.new_member.funding.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.funding.description"),
|
||||
)
|
||||
perms_reporting = SelectField(
|
||||
translate("forms.new_member.reporting"),
|
||||
choices=[
|
||||
(PermissionSets.VIEW_PORTFOLIO_REPORTS, translate("common.view")),
|
||||
(PermissionSets.EDIT_PORTFOLIO_REPORTS, translate("common.edit")),
|
||||
],
|
||||
perms_reporting = BooleanField(
|
||||
translate("forms.new_member.reporting.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.reporting.description"),
|
||||
)
|
||||
perms_portfolio_mgmt = SelectField(
|
||||
translate("forms.new_member.portfolio_mgmt"),
|
||||
choices=[
|
||||
(PermissionSets.VIEW_PORTFOLIO_ADMIN, translate("common.view")),
|
||||
(PermissionSets.EDIT_PORTFOLIO_ADMIN, translate("common.edit")),
|
||||
],
|
||||
perms_portfolio_mgmt = BooleanField(
|
||||
translate("forms.new_member.portfolio_mgmt.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.portfolio_mgmt.description"),
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
_data = super().data
|
||||
_data["permission_sets"] = []
|
||||
for field in _data:
|
||||
if "perms" in field:
|
||||
_data["permission_sets"].append(_data[field])
|
||||
_data.pop("csrf_token", None)
|
||||
perm_sets = []
|
||||
|
||||
if _data["perms_app_mgmt"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
|
||||
|
||||
if _data["perms_funding"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_FUNDING)
|
||||
|
||||
if _data["perms_reporting"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||
|
||||
if _data["perms_portfolio_mgmt"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
|
||||
_data["permission_sets"] = perm_sets
|
||||
return _data
|
||||
|
||||
|
||||
class MembersPermissionsForm(BaseForm):
|
||||
members_permissions = FieldList(FormField(PermissionsForm))
|
||||
|
||||
|
||||
class NewForm(BaseForm):
|
||||
class NewForm(PermissionsForm):
|
||||
user_data = FormField(BaseNewMemberForm)
|
||||
permission_sets = FormField(PermissionsForm)
|
||||
|
||||
@property
|
||||
def update_data(self):
|
||||
return {
|
||||
"permission_sets": self.data.get("permission_sets").get("permission_sets"),
|
||||
**self.data.get("user_data"),
|
||||
}
|
||||
|
||||
|
||||
class AssignPPOCForm(PermissionsForm):
|
||||
|
@ -7,7 +7,7 @@ from wtforms.fields import (
|
||||
HiddenField,
|
||||
)
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import Required, Length, NumberRange, ValidationError
|
||||
from wtforms.validators import Required, Length, NumberRange, ValidationError, Regexp
|
||||
from flask_wtf import FlaskForm
|
||||
from numbers import Number
|
||||
|
||||
@ -15,6 +15,7 @@ from .data import JEDI_CLIN_TYPES
|
||||
from .fields import SelectField
|
||||
from .forms import BaseForm, remove_empty_string
|
||||
from atst.utils.localization import translate
|
||||
from .validators import REGEX_ALPHA_NUMERIC
|
||||
from flask import current_app as app
|
||||
|
||||
MAX_CLIN_AMOUNT = 1000000000
|
||||
@ -116,7 +117,10 @@ class AttachmentForm(BaseForm):
|
||||
filename = HiddenField(
|
||||
id="attachment_filename",
|
||||
validators=[
|
||||
Length(max=100, message=translate("forms.attachment.filename.length_error"))
|
||||
Length(
|
||||
max=100, message=translate("forms.attachment.filename.length_error")
|
||||
),
|
||||
Regexp(regex=REGEX_ALPHA_NUMERIC),
|
||||
],
|
||||
)
|
||||
object_name = HiddenField(
|
||||
@ -124,7 +128,8 @@ class AttachmentForm(BaseForm):
|
||||
validators=[
|
||||
Length(
|
||||
max=40, message=translate("forms.attachment.object_name.length_error")
|
||||
)
|
||||
),
|
||||
Regexp(regex=REGEX_ALPHA_NUMERIC),
|
||||
],
|
||||
)
|
||||
accept = ".pdf,application/pdf"
|
||||
|
@ -8,6 +8,9 @@ import pendulum
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
REGEX_ALPHA_NUMERIC = "^[A-Za-z0-9\-_ \.]*$"
|
||||
|
||||
|
||||
def DateRange(lower_bound=None, upper_bound=None, message=None):
|
||||
def _date_range(form, field):
|
||||
if field.data is None:
|
||||
|
33
atst/jobs.py
33
atst/jobs.py
@ -7,14 +7,27 @@ from atst.models import (
|
||||
EnvironmentJobFailure,
|
||||
EnvironmentRoleJobFailure,
|
||||
EnvironmentRole,
|
||||
PortfolioJobFailure,
|
||||
)
|
||||
from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.models.utils import claim_for_update
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class RecordPortfolioFailure(celery.Task):
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if "portfolio_id" in kwargs:
|
||||
failure = PortfolioJobFailure(
|
||||
portfolio_id=kwargs["portfolio_id"], task_id=task_id
|
||||
)
|
||||
db.session.add(failure)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class RecordEnvironmentFailure(celery.Task):
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if "environment_id" in kwargs:
|
||||
@ -125,6 +138,17 @@ def do_work(fn, task, csp, **kwargs):
|
||||
raise task.retry(exc=e)
|
||||
|
||||
|
||||
def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
fsm = Portfolios.get_or_create_state_machine(portfolio)
|
||||
fsm.trigger_next_transition()
|
||||
|
||||
|
||||
@celery.task(bind=True, base=RecordPortfolioFailure)
|
||||
def provision_portfolio(self, portfolio_id=None):
|
||||
do_work(do_provision_portfolio, self, app.csp.cloud, portfolio_id=portfolio_id)
|
||||
|
||||
|
||||
@celery.task(bind=True, base=RecordEnvironmentFailure)
|
||||
def create_environment(self, environment_id=None):
|
||||
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id)
|
||||
@ -144,6 +168,15 @@ def provision_user(self, environment_role_id=None):
|
||||
)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_provision_portfolio(self):
|
||||
"""
|
||||
Iterate over portfolios with a corresponding State Machine that have not completed.
|
||||
"""
|
||||
for portfolio_id in Portfolios.get_portfolios_pending_provisioning():
|
||||
provision_portfolio.delay(portfolio_id=portfolio_id)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_create_environment(self):
|
||||
for environment_id in Environments.get_environments_pending_creation(
|
||||
|
@ -7,11 +7,16 @@ from .audit_event import AuditEvent
|
||||
from .clin import CLIN, JEDICLINType
|
||||
from .environment import Environment
|
||||
from .environment_role import EnvironmentRole, CSPRole
|
||||
from .job_failure import EnvironmentJobFailure, EnvironmentRoleJobFailure
|
||||
from .job_failure import (
|
||||
EnvironmentJobFailure,
|
||||
EnvironmentRoleJobFailure,
|
||||
PortfolioJobFailure,
|
||||
)
|
||||
from .notification_recipient import NotificationRecipient
|
||||
from .permissions import Permissions
|
||||
from .permission_set import PermissionSet
|
||||
from .portfolio import Portfolio
|
||||
from .portfolio_state_machine import PortfolioStateMachine, FSMStates
|
||||
from .portfolio_invitation import PortfolioInvitation
|
||||
from .portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
|
||||
from .task_order import TaskOrder
|
||||
|
@ -9,10 +9,9 @@ import atst.models.types as types
|
||||
|
||||
|
||||
class CSPRole(Enum):
|
||||
BASIC_ACCESS = "Basic Access"
|
||||
NETWORK_ADMIN = "Network Admin"
|
||||
BUSINESS_READ = "Business Read-only"
|
||||
TECHNICAL_READ = "Technical Read-only"
|
||||
ADMIN = "Admin"
|
||||
BILLING_READ = "Billing Read-only"
|
||||
CONTRIBUTOR = "Contributor"
|
||||
|
||||
|
||||
class EnvironmentRole(
|
||||
@ -26,7 +25,7 @@ class EnvironmentRole(
|
||||
)
|
||||
environment = relationship("Environment")
|
||||
|
||||
role = Column(String())
|
||||
role = Column(SQLAEnum(CSPRole, native_enum=False), nullable=True)
|
||||
|
||||
application_role_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("application_roles.id"), nullable=False
|
||||
|
@ -14,3 +14,9 @@ class EnvironmentRoleJobFailure(Base, mixins.JobFailureMixin):
|
||||
__tablename__ = "environment_role_job_failures"
|
||||
|
||||
environment_role_id = Column(ForeignKey("environment_roles.id"), nullable=False)
|
||||
|
||||
|
||||
class PortfolioJobFailure(Base, mixins.JobFailureMixin):
|
||||
__tablename__ = "portfolio_job_failures"
|
||||
|
||||
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
|
||||
|
@ -4,3 +4,4 @@ from .permissions import PermissionsMixin
|
||||
from .deletable import DeletableMixin
|
||||
from .invites import InvitesMixin
|
||||
from .job_failure import JobFailureMixin
|
||||
from .state_machines import FSMMixin
|
||||
|
137
atst/models/mixins/state_machines.py
Normal file
137
atst/models/mixins/state_machines.py
Normal file
@ -0,0 +1,137 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class StageStates(Enum):
|
||||
CREATED = "created"
|
||||
IN_PROGRESS = "in progress"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class AzureStages(Enum):
|
||||
TENANT = "tenant"
|
||||
BILLING_PROFILE = "billing profile"
|
||||
ADMIN_SUBSCRIPTION = "admin subscription"
|
||||
|
||||
|
||||
def _build_csp_states(csp_stages):
|
||||
states = {
|
||||
"UNSTARTED": "unstarted",
|
||||
"STARTING": "starting",
|
||||
"STARTED": "started",
|
||||
"COMPLETED": "completed",
|
||||
"FAILED": "failed",
|
||||
}
|
||||
for csp_stage in csp_stages:
|
||||
for state in StageStates:
|
||||
states[csp_stage.name + "_" + state.name] = (
|
||||
csp_stage.value + " " + state.value
|
||||
)
|
||||
return states
|
||||
|
||||
|
||||
FSMStates = Enum("FSMStates", _build_csp_states(AzureStages))
|
||||
|
||||
|
||||
def _build_transitions(csp_stages):
|
||||
transitions = []
|
||||
states = []
|
||||
compose_state = lambda csp_stage, state: getattr(
|
||||
FSMStates, "_".join([csp_stage.name, state.name])
|
||||
)
|
||||
|
||||
for stage_i, csp_stage in enumerate(csp_stages):
|
||||
for state in StageStates:
|
||||
states.append(
|
||||
dict(
|
||||
name=compose_state(csp_stage, state),
|
||||
tags=[csp_stage.name, state.name],
|
||||
)
|
||||
)
|
||||
if state == StageStates.CREATED:
|
||||
if stage_i > 0:
|
||||
src = compose_state(
|
||||
list(csp_stages)[stage_i - 1], StageStates.CREATED
|
||||
)
|
||||
else:
|
||||
src = FSMStates.STARTED
|
||||
transitions.append(
|
||||
dict(
|
||||
trigger="create_" + csp_stage.name.lower(),
|
||||
source=src,
|
||||
dest=compose_state(csp_stage, StageStates.IN_PROGRESS),
|
||||
after="after_in_progress_callback",
|
||||
)
|
||||
)
|
||||
if state == StageStates.IN_PROGRESS:
|
||||
transitions.append(
|
||||
dict(
|
||||
trigger="finish_" + csp_stage.name.lower(),
|
||||
source=compose_state(csp_stage, state),
|
||||
dest=compose_state(csp_stage, StageStates.CREATED),
|
||||
conditions=["is_csp_data_valid"],
|
||||
)
|
||||
)
|
||||
if state == StageStates.FAILED:
|
||||
transitions.append(
|
||||
dict(
|
||||
trigger="fail_" + csp_stage.name.lower(),
|
||||
source=compose_state(csp_stage, StageStates.IN_PROGRESS),
|
||||
dest=compose_state(csp_stage, StageStates.FAILED),
|
||||
)
|
||||
)
|
||||
return states, transitions
|
||||
|
||||
|
||||
class FSMMixin:
|
||||
|
||||
system_states = [
|
||||
{"name": FSMStates.UNSTARTED.name, "tags": ["system"]},
|
||||
{"name": FSMStates.STARTING.name, "tags": ["system"]},
|
||||
{"name": FSMStates.STARTED.name, "tags": ["system"]},
|
||||
{"name": FSMStates.FAILED.name, "tags": ["system"]},
|
||||
{"name": FSMStates.COMPLETED.name, "tags": ["system"]},
|
||||
]
|
||||
|
||||
system_transitions = [
|
||||
{"trigger": "init", "source": FSMStates.UNSTARTED, "dest": FSMStates.STARTING},
|
||||
{"trigger": "start", "source": FSMStates.STARTING, "dest": FSMStates.STARTED},
|
||||
{"trigger": "reset", "source": "*", "dest": FSMStates.UNSTARTED},
|
||||
{"trigger": "fail", "source": "*", "dest": FSMStates.FAILED,},
|
||||
]
|
||||
|
||||
def prepare_init(self, event):
|
||||
pass
|
||||
|
||||
def before_init(self, event):
|
||||
pass
|
||||
|
||||
def after_init(self, event):
|
||||
pass
|
||||
|
||||
def prepare_start(self, event):
|
||||
pass
|
||||
|
||||
def before_start(self, event):
|
||||
pass
|
||||
|
||||
def after_start(self, event):
|
||||
pass
|
||||
|
||||
def prepare_reset(self, event):
|
||||
pass
|
||||
|
||||
def before_reset(self, event):
|
||||
pass
|
||||
|
||||
def after_reset(self, event):
|
||||
pass
|
||||
|
||||
def fail_stage(self, stage):
|
||||
fail_trigger = "fail" + stage
|
||||
if fail_trigger in self.machine.get_triggers(self.current_state.name):
|
||||
self.trigger(fail_trigger)
|
||||
|
||||
def finish_stage(self, stage):
|
||||
finish_trigger = "finish_" + stage
|
||||
if finish_trigger in self.machine.get_triggers(self.current_state.name):
|
||||
self.trigger(finish_trigger)
|
@ -11,6 +11,8 @@ from atst.domain.permission_sets import PermissionSets
|
||||
from atst.utils import first_or_none
|
||||
from atst.database import db
|
||||
|
||||
from sqlalchemy_json import NestedMutableJson
|
||||
|
||||
|
||||
class Portfolio(
|
||||
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin
|
||||
@ -19,16 +21,31 @@ class Portfolio(
|
||||
|
||||
id = types.Id()
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
defense_component = Column(
|
||||
ARRAY(String), nullable=False
|
||||
String, nullable=False
|
||||
) # Department of Defense Component
|
||||
|
||||
app_migration = Column(String) # App Migration
|
||||
complexity = Column(ARRAY(String)) # Application Complexity
|
||||
complexity_other = Column(String)
|
||||
description = Column(String)
|
||||
dev_team = Column(ARRAY(String)) # Development Team
|
||||
dev_team_other = Column(String)
|
||||
native_apps = Column(String) # Native Apps
|
||||
team_experience = Column(String) # Team Experience
|
||||
|
||||
csp_data = Column(NestedMutableJson, nullable=True)
|
||||
|
||||
applications = relationship(
|
||||
"Application",
|
||||
back_populates="portfolio",
|
||||
primaryjoin="and_(Application.portfolio_id == Portfolio.id, Application.deleted == False)",
|
||||
)
|
||||
|
||||
state_machine = relationship(
|
||||
"PortfolioStateMachine", uselist=False, back_populates="portfolio"
|
||||
)
|
||||
|
||||
roles = relationship("PortfolioRole")
|
||||
|
||||
task_orders = relationship("TaskOrder")
|
||||
@ -77,7 +94,7 @@ class Portfolio(
|
||||
"""
|
||||
Return the earliest period of performance start date and latest period
|
||||
of performance end date for all active task orders in a portfolio.
|
||||
@return: (datetime.date or None, datetime.date or None)
|
||||
@return: (datetime.date or None, datetime.date or None)
|
||||
"""
|
||||
start_dates = (
|
||||
task_order.start_date
|
||||
|
@ -12,17 +12,6 @@ from atst.utils import first_or_none
|
||||
from atst.models.mixins.auditable import record_permission_sets_updates
|
||||
|
||||
|
||||
MEMBER_STATUSES = {
|
||||
"active": "Active",
|
||||
"revoked": "Invite revoked",
|
||||
"expired": "Invite expired",
|
||||
"error": "Error on invite",
|
||||
"pending": "Pending",
|
||||
"unknown": "Unknown errors",
|
||||
"disabled": "Disabled",
|
||||
}
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "active"
|
||||
DISABLED = "disabled"
|
||||
@ -90,23 +79,23 @@ class PortfolioRole(
|
||||
@property
|
||||
def display_status(self):
|
||||
if self.status == Status.ACTIVE:
|
||||
return MEMBER_STATUSES["active"]
|
||||
return "active"
|
||||
elif self.status == Status.DISABLED:
|
||||
return MEMBER_STATUSES["disabled"]
|
||||
return "disabled"
|
||||
elif self.latest_invitation:
|
||||
if self.latest_invitation.is_revoked:
|
||||
return MEMBER_STATUSES["revoked"]
|
||||
return "invite_revoked"
|
||||
elif self.latest_invitation.is_rejected_wrong_user:
|
||||
return MEMBER_STATUSES["error"]
|
||||
return "invite_error"
|
||||
elif (
|
||||
self.latest_invitation.is_rejected_expired
|
||||
or self.latest_invitation.is_expired
|
||||
):
|
||||
return MEMBER_STATUSES["expired"]
|
||||
return "invite_expired"
|
||||
else:
|
||||
return MEMBER_STATUSES["pending"]
|
||||
return "invite_pending"
|
||||
else:
|
||||
return MEMBER_STATUSES["unknown"]
|
||||
return "unknown"
|
||||
|
||||
def has_permission_set(self, perm_set_name):
|
||||
return first_or_none(
|
||||
|
181
atst/models/portfolio_state_machine.py
Normal file
181
atst/models/portfolio_state_machine.py
Normal file
@ -0,0 +1,181 @@
|
||||
from sqlalchemy import Column, ForeignKey, Enum as SQLAEnum
|
||||
from sqlalchemy.orm import relationship, reconstructor
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from transitions import Machine
|
||||
from transitions.extensions.states import add_state_features, Tags
|
||||
|
||||
from flask import current_app as app
|
||||
|
||||
from atst.domain.csp.cloud import ConnectionException, UnknownServerException
|
||||
from atst.domain.csp import MockCSP, AzureCSP, get_stage_csp_class
|
||||
from atst.database import db
|
||||
from atst.models.types import Id
|
||||
from atst.models.base import Base
|
||||
import atst.models.mixins as mixins
|
||||
from atst.models.mixins.state_machines import FSMStates, AzureStages, _build_transitions
|
||||
|
||||
|
||||
@add_state_features(Tags)
|
||||
class StateMachineWithTags(Machine):
|
||||
pass
|
||||
|
||||
|
||||
class PortfolioStateMachine(
|
||||
Base,
|
||||
mixins.TimestampsMixin,
|
||||
mixins.AuditableMixin,
|
||||
mixins.DeletableMixin,
|
||||
mixins.FSMMixin,
|
||||
):
|
||||
__tablename__ = "portfolio_state_machines"
|
||||
|
||||
id = Id()
|
||||
|
||||
portfolio_id = Column(UUID(as_uuid=True), ForeignKey("portfolios.id"),)
|
||||
portfolio = relationship("Portfolio", back_populates="state_machine")
|
||||
|
||||
state = Column(
|
||||
SQLAEnum(FSMStates, native_enum=False, create_constraint=False),
|
||||
default=FSMStates.UNSTARTED,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def __init__(self, portfolio, csp=None, **kwargs):
|
||||
self.portfolio = portfolio
|
||||
self.attach_machine()
|
||||
|
||||
def after_state_change(self, event):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
@reconstructor
|
||||
def attach_machine(self):
|
||||
"""
|
||||
This is called as a result of a sqlalchemy query.
|
||||
Attach a machine depending on the current state.
|
||||
"""
|
||||
self.machine = StateMachineWithTags(
|
||||
model=self,
|
||||
send_event=True,
|
||||
initial=self.current_state if self.state else FSMStates.UNSTARTED,
|
||||
auto_transitions=False,
|
||||
after_state_change="after_state_change",
|
||||
)
|
||||
states, transitions = _build_transitions(AzureStages)
|
||||
self.machine.add_states(self.system_states + states)
|
||||
self.machine.add_transitions(self.system_transitions + transitions)
|
||||
|
||||
@property
|
||||
def current_state(self):
|
||||
if isinstance(self.state, str):
|
||||
return getattr(FSMStates, self.state)
|
||||
return self.state
|
||||
|
||||
def trigger_next_transition(self):
|
||||
state_obj = self.machine.get_state(self.state)
|
||||
|
||||
if state_obj.is_system:
|
||||
if self.current_state in (FSMStates.UNSTARTED, FSMStates.STARTING):
|
||||
# call the first trigger availabe for these two system states
|
||||
trigger_name = self.machine.get_triggers(self.current_state.name)[0]
|
||||
self.trigger(trigger_name)
|
||||
|
||||
elif self.current_state == FSMStates.STARTED:
|
||||
# get the first trigger that starts with 'create_'
|
||||
create_trigger = list(
|
||||
filter(
|
||||
lambda trigger: trigger.startswith("create_"),
|
||||
self.machine.get_triggers(FSMStates.STARTED.name),
|
||||
)
|
||||
)[0]
|
||||
self.trigger(create_trigger)
|
||||
|
||||
elif state_obj.is_IN_PROGRESS:
|
||||
pass
|
||||
|
||||
# elif state_obj.is_TENANT:
|
||||
# pass
|
||||
# elif state_obj.is_BILLING_PROFILE:
|
||||
# pass
|
||||
|
||||
# @with_payload
|
||||
def after_in_progress_callback(self, event):
|
||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||
if stage == "tenant":
|
||||
payload = dict( # nosec
|
||||
creds={"username": "mock-cloud", "pass": "shh"},
|
||||
user_id="123",
|
||||
password="123",
|
||||
domain_name="123",
|
||||
first_name="john",
|
||||
last_name="doe",
|
||||
country_code="US",
|
||||
password_recovery_email_address="password@email.com",
|
||||
)
|
||||
elif stage == "billing_profile":
|
||||
payload = dict(creds={"username": "mock-cloud", "pass": "shh"},)
|
||||
|
||||
payload_data_cls = get_stage_csp_class(stage, "payload")
|
||||
if not payload_data_cls:
|
||||
self.fail_stage(stage)
|
||||
try:
|
||||
payload_data = payload_data_cls(**payload)
|
||||
except PydanticValidationError as exc:
|
||||
print(exc.json())
|
||||
self.fail_stage(stage)
|
||||
|
||||
csp = event.kwargs.get("csp")
|
||||
if csp is not None:
|
||||
self.csp = AzureCSP(app).cloud
|
||||
else:
|
||||
self.csp = MockCSP(app).cloud
|
||||
|
||||
for attempt in range(5):
|
||||
try:
|
||||
response = getattr(self.csp, "create_" + stage)(payload_data)
|
||||
except (ConnectionException, UnknownServerException) as exc:
|
||||
print("caught exception. retry", attempt)
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
# failed all attempts
|
||||
self.fail_stage(stage)
|
||||
|
||||
if self.portfolio.csp_data is None:
|
||||
self.portfolio.csp_data = {}
|
||||
self.portfolio.csp_data[stage + "_data"] = response
|
||||
db.session.add(self.portfolio)
|
||||
db.session.commit()
|
||||
|
||||
self.finish_stage(stage)
|
||||
|
||||
def is_csp_data_valid(self, event):
|
||||
# check portfolio csp details json field for fields
|
||||
|
||||
if self.portfolio.csp_data is None or not isinstance(
|
||||
self.portfolio.csp_data, dict
|
||||
):
|
||||
return False
|
||||
|
||||
stage = self.current_state.name.split("_IN_PROGRESS")[0].lower()
|
||||
stage_data = self.portfolio.csp_data.get(stage + "_data")
|
||||
cls = get_stage_csp_class(stage, "result")
|
||||
if not cls:
|
||||
return False
|
||||
|
||||
try:
|
||||
cls(**stage_data)
|
||||
except PydanticValidationError as exc:
|
||||
print(exc.json())
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# print('failed condition', self.portfolio.csp_data)
|
||||
|
||||
@property
|
||||
def application_id(self):
|
||||
return None
|
@ -140,7 +140,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
@property
|
||||
def invoiced_funds(self):
|
||||
# TODO: implement this using reporting data from the CSP
|
||||
return self.total_obligated_funds * Decimal(0.75)
|
||||
if self.is_active:
|
||||
return self.total_obligated_funds * Decimal(0.75)
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def display_status(self):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import String, ForeignKey, Column, Date, Boolean, Table, TIMESTAMP
|
||||
from sqlalchemy import String, ForeignKey, Column, Date, Table, TIMESTAMP
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.event import listen
|
||||
@ -67,8 +67,6 @@ class User(
|
||||
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
last_session_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
provisional = Column(Boolean)
|
||||
|
||||
cloud_id = Column(String)
|
||||
|
||||
REQUIRED_FIELDS = [
|
||||
|
@ -7,6 +7,10 @@ celery = Celery(__name__)
|
||||
def update_celery(celery, app):
|
||||
celery.conf.update(app.config)
|
||||
celery.conf.CELERYBEAT_SCHEDULE = {
|
||||
"beat-dispatch_provision_portfolio": {
|
||||
"task": "atst.jobs.dispatch_provision_portfolio",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-dispatch_create_environment": {
|
||||
"task": "atst.jobs.dispatch_create_environment",
|
||||
"schedule": 60,
|
||||
|
@ -19,6 +19,7 @@ from werkzeug.exceptions import NotFound
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.authnid import AuthenticationContext
|
||||
from atst.domain.auth import logout as _logout
|
||||
from atst.domain.exceptions import UnauthenticatedError
|
||||
from atst.utils.flash import formatted_flash as flash
|
||||
|
||||
|
||||
@ -64,11 +65,15 @@ def catch_all(path):
|
||||
raise NotFound()
|
||||
|
||||
|
||||
def _client_s_dn():
|
||||
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
|
||||
|
||||
|
||||
def _make_authentication_context():
|
||||
return AuthenticationContext(
|
||||
crl_cache=app.crl_cache,
|
||||
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"),
|
||||
sdn=request.environ.get("HTTP_X_SSL_CLIENT_S_DN"),
|
||||
sdn=_client_s_dn(),
|
||||
cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT"),
|
||||
)
|
||||
|
||||
@ -89,19 +94,24 @@ def current_user_setup(user):
|
||||
session["user_id"] = user.id
|
||||
session["last_login"] = user.last_login
|
||||
app.session_limiter.on_login(user)
|
||||
app.logger.info(f"authentication succeeded for user with EDIPI {user.dod_id}")
|
||||
Users.update_last_login(user)
|
||||
|
||||
|
||||
@bp.route("/login-redirect")
|
||||
def login_redirect():
|
||||
auth_context = _make_authentication_context()
|
||||
auth_context.authenticate()
|
||||
user = auth_context.get_user()
|
||||
try:
|
||||
auth_context = _make_authentication_context()
|
||||
auth_context.authenticate()
|
||||
|
||||
if user.provisional:
|
||||
Users.finalize(user)
|
||||
user = auth_context.get_user()
|
||||
current_user_setup(user)
|
||||
except UnauthenticatedError as err:
|
||||
app.logger.info(
|
||||
f"authentication failed for subject distinguished name {_client_s_dn()}"
|
||||
)
|
||||
raise err
|
||||
|
||||
current_user_setup(user)
|
||||
return redirect(redirect_after_login_url())
|
||||
|
||||
|
||||
|
@ -78,7 +78,7 @@ def filter_env_roles_data(roles):
|
||||
{
|
||||
"environment_id": str(role.environment.id),
|
||||
"environment_name": role.environment.name,
|
||||
"role": role.role,
|
||||
"role": (role.role.value if role.role else "None"),
|
||||
}
|
||||
for role in roles
|
||||
],
|
||||
@ -99,8 +99,9 @@ def filter_env_roles_form_data(member, environments):
|
||||
|
||||
if len(env_roles_set) == 1:
|
||||
(env_role,) = env_roles_set
|
||||
env_data["role"] = env_role.role
|
||||
env_data["disabled"] = env_role.disabled
|
||||
if env_role.role:
|
||||
env_data["role"] = env_role.role.name
|
||||
|
||||
env_roles_form_data.append(env_data)
|
||||
|
||||
|
@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash
|
||||
from atst.domain.exceptions import UnauthorizedError
|
||||
|
||||
|
||||
def permission_str(member, edit_perm_set, view_perm_set):
|
||||
if member.has_permission_set(edit_perm_set):
|
||||
return edit_perm_set
|
||||
else:
|
||||
return view_perm_set
|
||||
|
||||
|
||||
def serialize_member_form_data(member):
|
||||
return {
|
||||
"member_name": member.full_name,
|
||||
"member_id": member.id,
|
||||
"perms_app_mgmt": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
def filter_perm_sets_data(member):
|
||||
perm_sets_data = {
|
||||
"perms_portfolio_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
),
|
||||
"perms_funding": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_FUNDING,
|
||||
PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
||||
"perms_app_mgmt": bool(
|
||||
member.has_permission_set(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||
)
|
||||
),
|
||||
"perms_reporting": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_REPORTS,
|
||||
PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
||||
"perms_funding": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
|
||||
),
|
||||
"perms_portfolio_mgmt": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_ADMIN,
|
||||
PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
||||
"perms_reporting": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||
),
|
||||
}
|
||||
|
||||
return perm_sets_data
|
||||
|
||||
def get_members_data(portfolio):
|
||||
members = sorted(
|
||||
[serialize_member_form_data(member) for member in portfolio.members],
|
||||
key=lambda member: member["member_name"],
|
||||
)
|
||||
for member in members:
|
||||
if member["member_id"] == portfolio.owner_role.id:
|
||||
ppoc = member
|
||||
members.remove(member)
|
||||
members.insert(0, ppoc)
|
||||
return members
|
||||
|
||||
def filter_members_data(members_list, portfolio):
|
||||
members_data = []
|
||||
for member in members_list:
|
||||
members_data.append(
|
||||
{
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": filter_perm_sets_data(member),
|
||||
"status": member.display_status,
|
||||
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
|
||||
# add in stuff here for forms
|
||||
}
|
||||
)
|
||||
|
||||
return sorted(members_data, key=lambda member: member["user_name"])
|
||||
|
||||
|
||||
def render_admin_page(portfolio, form=None):
|
||||
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
||||
members_data = get_members_data(portfolio)
|
||||
portfolio_form = PortfolioForm(obj=portfolio)
|
||||
member_perms_form = member_forms.MembersPermissionsForm(
|
||||
data={"members_permissions": members_data}
|
||||
)
|
||||
|
||||
member_list = portfolio.members
|
||||
assign_ppoc_form = member_forms.AssignPPOCForm()
|
||||
|
||||
for pf_role in portfolio.roles:
|
||||
if pf_role.user != portfolio.owner and pf_role.is_active:
|
||||
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)]
|
||||
@ -87,13 +75,12 @@ def render_admin_page(portfolio, form=None):
|
||||
"portfolios/admin.html",
|
||||
form=form,
|
||||
portfolio_form=portfolio_form,
|
||||
member_perms_form=member_perms_form,
|
||||
member_form=member_forms.NewForm(),
|
||||
members=filter_members_data(member_list, portfolio),
|
||||
new_manager_form=member_forms.NewForm(),
|
||||
assign_ppoc_form=assign_ppoc_form,
|
||||
portfolio=portfolio,
|
||||
audit_events=audit_events,
|
||||
user=g.current_user,
|
||||
ppoc_id=members_data[0].get("member_id"),
|
||||
current_member_id=current_member_id,
|
||||
applications_count=len(portfolio.applications),
|
||||
)
|
||||
@ -106,34 +93,6 @@ def admin(portfolio_id):
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/admin", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="view portfolio admin page")
|
||||
def edit_members(portfolio_id):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
member_perms_form = member_forms.MembersPermissionsForm(http_request.form)
|
||||
|
||||
if member_perms_form.validate():
|
||||
for subform in member_perms_form.members_permissions:
|
||||
member_id = subform.member_id.data
|
||||
member = PortfolioRoles.get_by_id(member_id)
|
||||
if member is not portfolio.owner_role:
|
||||
new_perm_set = subform.data["permission_sets"]
|
||||
PortfolioRoles.update(member, new_perm_set)
|
||||
|
||||
flash("update_portfolio_members", portfolio=portfolio)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
fragment="portfolio-members",
|
||||
_anchor="portfolio-members",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
||||
def update_ppoc(portfolio_id):
|
||||
|
@ -79,7 +79,7 @@ def invite_member(portfolio_id):
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
invite = Portfolios.invite(portfolio, g.current_user, form.update_data)
|
||||
invite = Portfolios.invite(portfolio, g.current_user, form.data)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
|
@ -1,214 +1,171 @@
|
||||
from flask import flash, render_template_string
|
||||
from flask import flash
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
MESSAGES = {
|
||||
"portfolio_deleted": {
|
||||
"title_template": "Portfolio has been deleted",
|
||||
"message_template": "Portfolio '{{portfolio_name}}' has been deleted",
|
||||
"category": "success",
|
||||
},
|
||||
"application_created": {
|
||||
"title_template": translate("flash.application.created.title"),
|
||||
"message_template": """
|
||||
{{ "flash.application.created.message" | translate({"application_name": application_name}) }}
|
||||
""",
|
||||
"title": "flash.application.created.title",
|
||||
"message": "flash.application.created.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_updated": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
{{ "flash.application.updated" | translate({"application_name": application_name}) }}
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"application_deleted": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
|
||||
<a href="#">{{ "common.undo" | translate }}</a>
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.application.updated",
|
||||
"category": "success",
|
||||
},
|
||||
"application_environments_name_error": {
|
||||
"title_template": "",
|
||||
"message_template": """{{ 'flash.application.env_name_error.message' | translate({ 'name': name }) }}""",
|
||||
"title": None,
|
||||
"message": "flash.application.env_name_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_environments_updated": {
|
||||
"title_template": "Application environments updated",
|
||||
"message_template": "Application environments have been updated",
|
||||
"title": "flash.environment.updated.title",
|
||||
"message": "flash.environment.updated.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_error": {
|
||||
"title_template": "Application invitation error",
|
||||
"message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_invite.error.title",
|
||||
"message": "flash.application_invite.error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_invite_resent": {
|
||||
"title_template": "Application invitation resent",
|
||||
"message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_invite.resent.title",
|
||||
"message": "flash.application_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_revoked": {
|
||||
"title_template": "Application invitation revoked",
|
||||
"message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_invite.revoked.title",
|
||||
"message": "flash.application_invite.revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_removed": {
|
||||
"title_template": "Team member removed from application",
|
||||
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_member.removed.title",
|
||||
"message": "flash.application_member.removed.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_update_error": {
|
||||
"title_template": "{{ user_name }} could not be updated",
|
||||
"message_template": "An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.",
|
||||
"title": "flash.application_member.update_error.title",
|
||||
"message": "flash.application_member.update_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_member_updated": {
|
||||
"title_template": "Team member updated",
|
||||
"message_template": "You have successfully updated the permissions for {{ user_name }}",
|
||||
"title": "flash.application_member.updated.title",
|
||||
"message": "flash.application_member.updated.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_name_error": {
|
||||
"title_template": "",
|
||||
"message_template": """{{ 'flash.application.name_error.message' | translate({ 'name': name }) }}""",
|
||||
"title": None,
|
||||
"message": "flash.application.name_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"ccpo_user_added": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": "You have successfully given {{ user_name }} CCPO permissions.",
|
||||
"title": "flash.success",
|
||||
"message": "flash.ccpo_user.added.message",
|
||||
"category": "success",
|
||||
},
|
||||
"ccpo_user_not_found": {
|
||||
"title_template": translate("ccpo.form.user_not_found_title"),
|
||||
"message_template": translate("ccpo.form.user_not_found_text"),
|
||||
"title": "ccpo.form.user_not_found_title",
|
||||
"message": "ccpo.form.user_not_found_text",
|
||||
"category": "info",
|
||||
},
|
||||
"ccpo_user_removed": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": "You have successfully removed {{ user_name }}'s CCPO permissions.",
|
||||
"title": "flash.success",
|
||||
"message": "flash.ccpo_user.removed.message",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_added": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
{{ "flash.environment_added" | translate({ "env_name": environment_name }) }}
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.environment_added",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_deleted": {
|
||||
"title_template": "{{ environment_name }} deleted",
|
||||
"message_template": 'The environment "{{ environment_name }}" has been deleted',
|
||||
"title": "flash.environment.deleted.title",
|
||||
"message": "flash.environment.deleted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"form_errors": {
|
||||
"title_template": "There were some errors",
|
||||
"message_template": "<p>Please see below.</p>",
|
||||
"title": "flash.form.errors.title",
|
||||
"message": "flash.form.errors.message",
|
||||
"category": "error",
|
||||
},
|
||||
"insufficient_funds": {
|
||||
"title_template": "Insufficient Funds",
|
||||
"message_template": "",
|
||||
"title": "flash.task_order.insufficient_funds.title",
|
||||
"message": "",
|
||||
"category": "warning",
|
||||
},
|
||||
"logged_out": {
|
||||
"title_template": translate("flash.logged_out"),
|
||||
"message_template": """
|
||||
You've been logged out.
|
||||
""",
|
||||
"title": "flash.logged_out.title",
|
||||
"message": "flash.logged_out.message",
|
||||
"category": "info",
|
||||
},
|
||||
"login_next": {
|
||||
"title_template": translate("flash.login_required_title"),
|
||||
"message_template": translate("flash.login_required_message"),
|
||||
"title": "flash.login_required_title",
|
||||
"message": "flash.login_required_message",
|
||||
"category": "warning",
|
||||
},
|
||||
"new_application_member": {
|
||||
"title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""",
|
||||
"message_template": """
|
||||
<p>{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}</p>
|
||||
""",
|
||||
"title": "flash.new_application_member.title",
|
||||
"message": "flash.new_application_member.message",
|
||||
"category": "success",
|
||||
},
|
||||
"new_portfolio_member": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
<p>{{ "flash.new_portfolio_member" | translate({ "user_name": user_name }) }}</p>
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.new_portfolio_member",
|
||||
"category": "success",
|
||||
},
|
||||
"portfolio_member_removed": {
|
||||
"title_template": translate("flash.deleted_member"),
|
||||
"message_template": """
|
||||
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
|
||||
""",
|
||||
"title": "flash.deleted_member",
|
||||
"message": "flash.delete_member_success",
|
||||
"category": "success",
|
||||
},
|
||||
"primary_point_of_contact_changed": {
|
||||
"title_template": translate("flash.new_ppoc_title"),
|
||||
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
|
||||
"title": "flash.new_ppoc_title",
|
||||
"message": "flash.new_ppoc_message",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation": {
|
||||
"title_template": "Invitation resent",
|
||||
"message_template": """
|
||||
<p>Successfully sent a new invitation to {{ user_name }}.</p>
|
||||
""",
|
||||
"title": "flash.portfolio_invite.resent.title",
|
||||
"message": "flash.portfolio_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"revoked_portfolio_access": {
|
||||
"title_template": "Removed portfolio access",
|
||||
"message_template": """
|
||||
<p>Portfolio access successfully removed from {{ member_name }}.</p>
|
||||
""",
|
||||
"title": "flash.portfolio_member.revoked.title",
|
||||
"message": "flash.portfolio_member.revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"session_expired": {
|
||||
"title_template": "Session Expired",
|
||||
"message_template": """
|
||||
Your session expired due to inactivity. Please log in again to continue.
|
||||
""",
|
||||
"title": "flash.session_expired.title",
|
||||
"message": "flash.session_expired.message",
|
||||
"category": "error",
|
||||
},
|
||||
"task_order_draft": {
|
||||
"title_template": translate("task_orders.form.draft_alert_title"),
|
||||
"message_template": translate("task_orders.form.draft_alert_message"),
|
||||
"title": "task_orders.form.draft_alert_title",
|
||||
"message": "task_orders.form.draft_alert_message",
|
||||
"category": "warning",
|
||||
},
|
||||
"task_order_number_error": {
|
||||
"title_template": "",
|
||||
"message_template": """{{ 'flash.task_order_number_error.message' | translate({ 'to_number': to_number }) }}""",
|
||||
"title": None,
|
||||
"message": "flash.task_order_number_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"task_order_submitted": {
|
||||
"title_template": "Your Task Order has been uploaded successfully.",
|
||||
"message_template": """
|
||||
Your task order form for {{ task_order.portfolio_name }} has been submitted.
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_members": {
|
||||
"title_template": "Success!",
|
||||
"message_template": """
|
||||
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
|
||||
""",
|
||||
"title": "flash.task_order.submitted.title",
|
||||
"message": "flash.task_order.submitted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"updated_application_team_settings": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
<p>{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}</p>
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.updated_application_team_settings",
|
||||
"category": "success",
|
||||
},
|
||||
"user_must_complete_profile": {
|
||||
"title_template": "You must complete your profile",
|
||||
"message_template": "<p>Before continuing, you must complete your profile</p>",
|
||||
"title": "flash.user.complete_profile.title",
|
||||
"message": "flash.user.complete_profile.message",
|
||||
"category": "info",
|
||||
},
|
||||
"user_updated": {
|
||||
"title_template": "User information updated.",
|
||||
"message_template": "",
|
||||
"title": "flash.user.updated.title",
|
||||
"message": None,
|
||||
"category": "success",
|
||||
},
|
||||
}
|
||||
@ -216,9 +173,11 @@ MESSAGES = {
|
||||
|
||||
def formatted_flash(message_name, **message_args):
|
||||
config = MESSAGES[message_name]
|
||||
title = render_template_string(config["title_template"], **message_args)
|
||||
message = render_template_string(config["message_template"], **message_args)
|
||||
actions = None
|
||||
if "actions" in config:
|
||||
actions = render_template_string(config["actions"], **message_args)
|
||||
|
||||
title = translate(config["title"], message_args) if config["title"] else None
|
||||
message = translate(config["message"], message_args) if config["message"] else None
|
||||
actions = (
|
||||
translate(config["actions"], message_args) if config.get("actions") else None
|
||||
)
|
||||
|
||||
flash({"title": title, "message": message, "actions": actions}, config["category"])
|
||||
|
@ -2,16 +2,22 @@ import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask import g, request, has_request_context
|
||||
from flask import g, request, has_request_context, session
|
||||
|
||||
|
||||
class RequestContextFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if has_request_context():
|
||||
if getattr(g, "current_user", None):
|
||||
record.user_id = str(g.current_user.id)
|
||||
record.dod_edipi = g.current_user.dod_id
|
||||
|
||||
user_id = session.get("user_id")
|
||||
if user_id:
|
||||
record.user_id = str(user_id)
|
||||
record.logged_in = True
|
||||
else:
|
||||
record.logged_in = False
|
||||
|
||||
if request.environ.get("HTTP_X_REQUEST_ID"):
|
||||
record.request_id = request.environ.get("HTTP_X_REQUEST_ID")
|
||||
|
||||
@ -30,6 +36,7 @@ class JsonFormatter(logging.Formatter):
|
||||
("request_id", lambda r: r.__dict__.get("request_id")),
|
||||
("user_id", lambda r: r.__dict__.get("user_id")),
|
||||
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
|
||||
("logged_in", lambda r: r.__dict__.get("logged_in")),
|
||||
("severity", lambda r: r.levelname),
|
||||
("tags", lambda r: r.__dict__.get("tags")),
|
||||
("audit_event", lambda r: r.__dict__.get("audit_event")),
|
||||
@ -44,7 +51,7 @@ class JsonFormatter(logging.Formatter):
|
||||
|
||||
for field, func in self._DEFAULT_RECORD_FIELDS:
|
||||
result = func(record)
|
||||
if result:
|
||||
if result is not None:
|
||||
message_dict[field] = result
|
||||
|
||||
if record.args:
|
||||
|
@ -15,6 +15,7 @@ CRL_FAIL_OPEN = false
|
||||
CRL_STORAGE_CONTAINER = crls
|
||||
CSP=mock
|
||||
DEBUG = true
|
||||
DEBUG_MAILER = false
|
||||
DISABLE_CRL_CHECK = false
|
||||
ENVIRONMENT = dev
|
||||
LIMIT_CONCURRENT_SESSIONS = false
|
||||
|
40
deploy/azure/autoscaling.yml
Normal file
40
deploy/azure/autoscaling.yml
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst
|
||||
namespace: atat
|
||||
spec:
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: atst
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: 60
|
||||
---
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst-worker
|
||||
namespace: atat
|
||||
spec:
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: atst-worker
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: 60
|
@ -15,7 +15,6 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
role: web
|
||||
replicas: 4
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
@ -49,6 +48,13 @@ spec:
|
||||
subPath: uwsgi.ini
|
||||
- name: flask-secret
|
||||
mountPath: "/config"
|
||||
resources:
|
||||
requests:
|
||||
memory: 200Mi
|
||||
cpu: 400m
|
||||
limits:
|
||||
memory: 200Mi
|
||||
cpu: 400m
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
@ -77,6 +83,13 @@ spec:
|
||||
mountPath: "/etc/nginx/snippets/"
|
||||
- name: nginx-secret
|
||||
mountPath: "/etc/ssl/"
|
||||
resources:
|
||||
requests:
|
||||
memory: 20Mi
|
||||
cpu: 10m
|
||||
limits:
|
||||
memory: 20Mi
|
||||
cpu: 10m
|
||||
volumes:
|
||||
- name: nginx-client-ca-bundle
|
||||
configMap:
|
||||
@ -155,7 +168,6 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
role: worker
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
@ -190,6 +202,13 @@ spec:
|
||||
subPath: pgsslrootcert.crt
|
||||
- name: flask-secret
|
||||
mountPath: "/config"
|
||||
resources:
|
||||
requests:
|
||||
memory: 280Mi
|
||||
cpu: 400m
|
||||
limits:
|
||||
memory: 280Mi
|
||||
cpu: 400m
|
||||
volumes:
|
||||
- name: pgsslrootcert
|
||||
configMap:
|
||||
@ -255,6 +274,13 @@ spec:
|
||||
subPath: pgsslrootcert.crt
|
||||
- name: flask-secret
|
||||
mountPath: "/config"
|
||||
resources:
|
||||
requests:
|
||||
memory: 80Mi
|
||||
cpu: 10m
|
||||
limits:
|
||||
memory: 80Mi
|
||||
cpu: 10m
|
||||
volumes:
|
||||
- name: pgsslrootcert
|
||||
configMap:
|
||||
|
@ -12,3 +12,4 @@ resources:
|
||||
- acme-challenges.yml
|
||||
- aadpodidentity.yml
|
||||
- nginx-snippets.yml
|
||||
- autoscaling.yml
|
||||
|
@ -1,35 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-config
|
||||
namespace: atat
|
||||
data:
|
||||
uwsgi-config: |-
|
||||
[uwsgi]
|
||||
callable = app
|
||||
module = app
|
||||
socket = /var/run/uwsgi/uwsgi.socket
|
||||
plugin = python3
|
||||
plugin = logfile
|
||||
virtualenv = /opt/atat/atst/.venv
|
||||
chmod-socket = 666
|
||||
|
||||
; logger config
|
||||
|
||||
; application logs: log without modifying
|
||||
logger = secondlogger stdio
|
||||
log-route = secondlogger atst
|
||||
log-encoder = format:secondlogger ${msg}
|
||||
|
||||
; default uWSGI messages (start, stop, etc.)
|
||||
logger = default stdio
|
||||
log-route = default ^((?!atst).)*$
|
||||
log-encoder = json:default {"timestamp":"${strftime:%%FT%%T}","source":"uwsgi","severity":"DEBUG","message":"${msg}"}
|
||||
log-encoder = nl
|
||||
|
||||
; uWSGI request logs
|
||||
logger-req = stdio
|
||||
log-format = request_id=%(var.HTTP_X_REQUEST_ID), pid=%(pid), remote_add=%(addr), request=%(method) %(uri), status=%(status), body_bytes_sent=%(rsize), referer=%(referer), user_agent=%(uagent), http_x_forwarded_for=%(var.HTTP_X_FORWARDED_FOR)
|
||||
log-req-encoder = json {"timestamp":"${strftime:%%FT%%T}","source":"req","severity":"INFO","message":"${msg}"}
|
||||
log-req-encoder = nl
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-envvars
|
||||
namespace: atat
|
||||
data:
|
||||
TZ: UTC
|
||||
FLASK_ENV: dev
|
||||
OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini
|
||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
||||
CRL_STORAGE_PROVIDER: CLOUDFILES
|
||||
LOG_JSON: "true"
|
||||
REDIS_URI: "redis://redis-svc:6379"
|
||||
PGHOST: postgres-svc
|
@ -1,73 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-nginx
|
||||
namespace: atat
|
||||
data:
|
||||
nginx-config: |-
|
||||
server {
|
||||
listen 8342;
|
||||
server_name aws.atat.code.mil;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
server {
|
||||
listen 8343;
|
||||
server_name auth-aws.atat.code.mil;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
server {
|
||||
server_name aws.atat.code.mil;
|
||||
# access_log /var/log/nginx/access.log json;
|
||||
listen 8442;
|
||||
location /login-redirect {
|
||||
return 301 https://auth-aws.atat.code.mil$request_uri;
|
||||
}
|
||||
location /login-dev {
|
||||
try_files $uri @appbasicauth;
|
||||
}
|
||||
location / {
|
||||
try_files $uri @app;
|
||||
}
|
||||
location @app {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||
uwsgi_param HTTP_X_REQUEST_ID $request_id;
|
||||
}
|
||||
location @appbasicauth {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||
auth_basic "Developer Access";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
uwsgi_param HTTP_X_REQUEST_ID $request_id;
|
||||
}
|
||||
}
|
||||
server {
|
||||
# access_log /var/log/nginx/access.log json;
|
||||
server_name auth-aws.atat.code.mil;
|
||||
listen 8443;
|
||||
listen [::]:8443 ipv6only=on;
|
||||
# Request and validate client certificate
|
||||
ssl_verify_client on;
|
||||
ssl_verify_depth 10;
|
||||
ssl_client_certificate /etc/ssl/client-ca-bundle.pem;
|
||||
# Guard against HTTPS -> HTTP downgrade
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
||||
location / {
|
||||
return 301 https://aws.atat.code.mil$request_uri;
|
||||
}
|
||||
location /login-redirect {
|
||||
try_files $uri @app;
|
||||
}
|
||||
location @app {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:///var/run/uwsgi/uwsgi.socket;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_VERIFY $ssl_client_verify;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_CERT $ssl_client_raw_cert;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_S_DN $ssl_client_s_dn;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_S_DN_LEGACY $ssl_client_s_dn_legacy;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_I_DN $ssl_client_i_dn;
|
||||
uwsgi_param HTTP_X_SSL_CLIENT_I_DN_LEGACY $ssl_client_i_dn_legacy;
|
||||
uwsgi_param HTTP_X_REQUEST_ID $request_id;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-worker-envvars
|
||||
namespace: atat
|
||||
data:
|
||||
TZ: UTC
|
||||
DISABLE_CRL_CHECK: "True"
|
||||
CRL_STORAGE_PROVIDER: CLOUDFILES
|
||||
REDIS_URI: "redis://redis-svc:6379"
|
||||
PGHOST: postgres-svc
|
@ -1,61 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: db-cache
|
||||
name: datastores
|
||||
namespace: atat
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: db-cache
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: db-cache
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 101
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:11-alpine
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
- name: redis
|
||||
image: redis:5.0-alpine
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres-svc
|
||||
namespace: atat
|
||||
spec:
|
||||
ports:
|
||||
- name: db-port
|
||||
protocol: "TCP"
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
selector:
|
||||
app: db-cache
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-svc
|
||||
namespace: atat
|
||||
spec:
|
||||
ports:
|
||||
- name: cache-port
|
||||
protocol: "TCP"
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: db-cache
|
@ -1,232 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst
|
||||
namespace: atat
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
role: web
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
role: web
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 101
|
||||
containers:
|
||||
- name: atst
|
||||
image: atat:latest
|
||||
imagePullPolicy: Never
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-envvars
|
||||
volumeMounts:
|
||||
- name: atst-config
|
||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||
subPath: atst-overrides.ini
|
||||
- name: nginx-client-ca-bundle
|
||||
mountPath: "/opt/atat/atst/ssl/server-certs/ca-chain.pem"
|
||||
subPath: client-ca-bundle.pem
|
||||
- name: uwsgi-socket-dir
|
||||
mountPath: "/var/run/uwsgi"
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8342
|
||||
name: main-upgrade
|
||||
- containerPort: 8442
|
||||
name: main
|
||||
- containerPort: 8343
|
||||
name: auth-upgrade
|
||||
- containerPort: 8443
|
||||
name: auth
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: "/etc/nginx/conf.d/atst.conf"
|
||||
subPath: atst.conf
|
||||
- name: uwsgi-socket-dir
|
||||
mountPath: "/var/run/uwsgi"
|
||||
- name: nginx-htpasswd
|
||||
mountPath: "/etc/nginx/.htpasswd"
|
||||
subPath: .htpasswd
|
||||
- name: nginx-client-ca-bundle
|
||||
mountPath: "/etc/ssl/"
|
||||
volumes:
|
||||
- name: atst-config
|
||||
secret:
|
||||
secretName: atst-config-ini
|
||||
items:
|
||||
- key: override.ini
|
||||
path: atst-overrides.ini
|
||||
mode: 0644
|
||||
- name: nginx-client-ca-bundle
|
||||
configMap:
|
||||
name: nginx-client-ca-bundle
|
||||
defaultMode: 0666
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: atst-nginx
|
||||
items:
|
||||
- key: nginx-config
|
||||
path: atst.conf
|
||||
- name: uwsgi-socket-dir
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: nginx-htpasswd
|
||||
secret:
|
||||
secretName: atst-nginx-htpasswd
|
||||
items:
|
||||
- key: htpasswd
|
||||
path: .htpasswd
|
||||
mode: 0640
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst-worker
|
||||
namespace: atat
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
role: worker
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
role: worker
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 101
|
||||
containers:
|
||||
- name: atst-worker
|
||||
image: atat:latest
|
||||
imagePullPolicy: Never
|
||||
args: [
|
||||
"/opt/atat/atst/.venv/bin/python",
|
||||
"/opt/atat/atst/.venv/bin/celery",
|
||||
"-A",
|
||||
"celery_worker.celery",
|
||||
"worker",
|
||||
"--loglevel=info"
|
||||
]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-envvars
|
||||
- configMapRef:
|
||||
name: atst-worker-envvars
|
||||
volumeMounts:
|
||||
- name: atst-config
|
||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||
subPath: atst-overrides.ini
|
||||
volumes:
|
||||
- name: atst-config
|
||||
secret:
|
||||
secretName: atst-config-ini
|
||||
items:
|
||||
- key: override.ini
|
||||
path: atst-overrides.ini
|
||||
mode: 0644
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst-beat
|
||||
namespace: atat
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
role: beat
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
role: beat
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 101
|
||||
containers:
|
||||
- name: atst-beat
|
||||
image: atat:latest
|
||||
imagePullPolicy: Never
|
||||
args: [
|
||||
"/opt/atat/atst/.venv/bin/python",
|
||||
"/opt/atat/atst/.venv/bin/celery",
|
||||
"-A",
|
||||
"celery_worker.celery",
|
||||
"beat",
|
||||
"--loglevel=info"
|
||||
]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-envvars
|
||||
- configMapRef:
|
||||
name: atst-worker-envvars
|
||||
volumeMounts:
|
||||
- name: atst-config
|
||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
||||
subPath: atst-overrides.ini
|
||||
volumes:
|
||||
- name: atst-config
|
||||
secret:
|
||||
secretName: atst-config-ini
|
||||
items:
|
||||
- key: override.ini
|
||||
path: atst-overrides.ini
|
||||
mode: 0644
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst-main
|
||||
namespace: atat
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8342
|
||||
name: http-main
|
||||
- port: 443
|
||||
targetPort: 8442
|
||||
name: https-main
|
||||
selector:
|
||||
role: web
|
||||
type: LoadBalancer
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
name: atst-auth
|
||||
namespace: atat
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8343
|
||||
name: http-auth
|
||||
- port: 443
|
||||
targetPort: 8443
|
||||
name: https-auth
|
||||
selector:
|
||||
role: web
|
||||
type: LoadBalancer
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -5,7 +5,6 @@ resources:
|
||||
- namespace.yml
|
||||
- reset-cron-job.yml
|
||||
patchesStrategicMerge:
|
||||
- replica_count.yml
|
||||
- ports.yml
|
||||
- envvars.yml
|
||||
- flex_vol.yml
|
||||
|
@ -1,14 +0,0 @@
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst
|
||||
spec:
|
||||
replicas: 2
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst-worker
|
||||
spec:
|
||||
replicas: 1
|
@ -5,7 +5,6 @@ resources:
|
||||
- namespace.yml
|
||||
- reset-cron-job.yml
|
||||
patchesStrategicMerge:
|
||||
- replica_count.yml
|
||||
- ports.yml
|
||||
- envvars.yml
|
||||
- flex_vol.yml
|
||||
|
@ -1,14 +0,0 @@
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst
|
||||
spec:
|
||||
replicas: 2
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst-worker
|
||||
spec:
|
||||
replicas: 1
|
0
docs/ATATArchitecture.md
Normal file
0
docs/ATATArchitecture.md
Normal file
32
docs/EdgeControls.md
Normal file
32
docs/EdgeControls.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Edge Control
|
||||
This document describes the expected connections and listening services.
|
||||
|
||||
## Transient Connections
|
||||
| Service | Direction | Ports | Protocol | Encrypted? | Ciphers |
|
||||
| --------|-----------|-------|----------|------------|--------------|
|
||||
| Azure Container Registry | Egress | 443 | HTTP | Yes | MSFT Managed |
|
||||
| DOD CRL Service | Egress | 443 | HTTP | Yes | DOD Managed |
|
||||
| Azure Storage | Egress | 443 | HTTP | Yes | MSFT Managed|
|
||||
| Redis | Egress | 6380 | HTTP | Yes | MSFT Managed|
|
||||
| Postgres | Egress | 5432 | HTTP | Yes | MSFT Managed|
|
||||
|
||||
# Listening Ports / Services
|
||||
| Service/App | Port | Protocol| Encrypted? | Accessible |
|
||||
|-------------|---------|---------|------------|--------|
|
||||
| ATAT App | 80, 443 | HTTP | Both | Load Balancer Only
|
||||
| ATAT Auth | 80, 443 | HTTP | Both | Load Balancer Only
|
||||
|
||||
# Host List
|
||||
## Dev
|
||||
| Service| Host |
|
||||
|--------|------|
|
||||
| Redis | cloudzero-dev-redis.redis.cache.windows.net |
|
||||
| Postgres| cloudzero-dev-sql.postgres.database.azure.com |
|
||||
| Docker Container Registry | cloudzerodevregistry.azurecr.io |
|
||||
|
||||
## Production
|
||||
| Service | Host |
|
||||
|---------|------|
|
||||
| Redis | |
|
||||
| Postgres| |
|
||||
| Docker Container Registry | |
|
@ -6,4 +6,6 @@ app = make_app(make_config())
|
||||
ctx = app.app_context()
|
||||
ctx.push()
|
||||
|
||||
print("\nWelcome to atst. This shell has all models in scope, and a SQLAlchemy session called db.")
|
||||
print(
|
||||
"\nWelcome to atst. This shell has all models in scope, and a SQLAlchemy session called db."
|
||||
)
|
||||
|
@ -70,7 +70,7 @@ describe('UploadInput Test', () => {
|
||||
})
|
||||
|
||||
const component = wrapper.find(uploadinput)
|
||||
const event = { target: { value: '', files: [{ name: '' }] } }
|
||||
const event = { target: { value: '', files: [{ name: 'sample.pdf' }] } }
|
||||
|
||||
component.setMethods({
|
||||
getUploader: async () => new MockUploader('token', 'objectName'),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { emitFieldChange } from '../lib/emitters'
|
||||
import escape from '../lib/escape'
|
||||
import optionsinput from './options_input'
|
||||
import textinput from './text_input'
|
||||
import clindollaramount from './clin_dollar_amount'
|
||||
@ -100,7 +99,7 @@ export default {
|
||||
computed: {
|
||||
clinTitle: function() {
|
||||
if (!!this.clinNumber) {
|
||||
return escape(`CLIN ${this.clinNumber}`)
|
||||
return `CLIN ${this.clinNumber}`
|
||||
} else {
|
||||
return `CLIN`
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { buildUploader } from '../lib/upload'
|
||||
import { emitFieldChange } from '../lib/emitters'
|
||||
import inputValidations from '../lib/input_validations'
|
||||
|
||||
export default {
|
||||
name: 'uploadinput',
|
||||
@ -28,6 +29,7 @@ export default {
|
||||
changed: false,
|
||||
uploadError: false,
|
||||
sizeError: false,
|
||||
filenameError: false,
|
||||
downloadLink: '',
|
||||
}
|
||||
},
|
||||
@ -50,6 +52,10 @@ export default {
|
||||
this.sizeError = true
|
||||
return
|
||||
}
|
||||
if (!this.validateFileName(file.name)) {
|
||||
this.filenameError = true
|
||||
return
|
||||
}
|
||||
|
||||
const uploader = await this.getUploader()
|
||||
const response = await uploader.upload(file)
|
||||
@ -71,6 +77,10 @@ export default {
|
||||
this.uploadError = true
|
||||
}
|
||||
},
|
||||
validateFileName: function(name) {
|
||||
const regex = inputValidations.restrictedFileName.match
|
||||
return regex.test(name)
|
||||
},
|
||||
removeAttachment: function(e) {
|
||||
e.preventDefault()
|
||||
this.attachment = null
|
||||
@ -118,7 +128,8 @@ export default {
|
||||
return (
|
||||
(!this.changed && this.initialErrors) ||
|
||||
this.uploadError ||
|
||||
this.sizeError
|
||||
this.sizeError ||
|
||||
this.filenameError
|
||||
)
|
||||
},
|
||||
valid: function() {
|
||||
|
@ -104,4 +104,11 @@ export default {
|
||||
unmask: ['(', ')', '-', ' '],
|
||||
validationError: 'Please enter a 10-digit phone number',
|
||||
},
|
||||
restrictedFileName: {
|
||||
mask: false,
|
||||
match: /^[A-Za-z0-9\-_ \.]+$/,
|
||||
unmask: [],
|
||||
validationError:
|
||||
'File names can only contain the characters A-Z, 0-9, space, hyphen, underscore, and period.',
|
||||
},
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ PASSWORD = os.getenv("ATAT_BA_PASSWORD", "")
|
||||
DISABLE_VERIFY = os.getenv("DISABLE_VERIFY", "true").lower() == "true"
|
||||
|
||||
# Alpha numerics for random entity names
|
||||
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" #pragma: allowlist secret
|
||||
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" # pragma: allowlist secret
|
||||
|
||||
NEW_PORTFOLIO_CHANCE = 10
|
||||
NEW_APPLICATION_CHANCE = 10
|
||||
@ -29,10 +29,6 @@ def logout(l):
|
||||
l.client.get("/logout")
|
||||
|
||||
|
||||
def get_index(l):
|
||||
l.client.get("/")
|
||||
|
||||
|
||||
def get_csrf_token(response):
|
||||
d = pq(response.text)
|
||||
return d("#csrf_token").val()
|
||||
@ -52,14 +48,9 @@ def extract_id(path):
|
||||
|
||||
|
||||
def get_portfolios(l):
|
||||
response = l.client.get("/portfolios")
|
||||
response = l.client.get("/home")
|
||||
d = pq(response.text)
|
||||
portfolio_links = [
|
||||
p.attr("href")
|
||||
for p in d(
|
||||
".global-panel-container .atat-table tbody tr td:first-child a"
|
||||
).items()
|
||||
]
|
||||
portfolio_links = [p.attr("href") for p in d(".sidenav__link").items()]
|
||||
force_new_portfolio = randrange(0, 100) < NEW_PORTFOLIO_CHANCE
|
||||
if len(portfolio_links) == 0 or force_new_portfolio:
|
||||
portfolio_links += [create_portfolio(l)]
|
||||
@ -73,7 +64,7 @@ def get_portfolio(l):
|
||||
d = pq(response.text)
|
||||
application_links = [
|
||||
p.attr("href")
|
||||
for p in d(".application-list .accordion__actions a:first-child").items()
|
||||
for p in d(".portfolio-applications .accordion__header-text a").items()
|
||||
]
|
||||
if len(application_links) > 0:
|
||||
portfolio_id = extract_id(portfolio_link)
|
||||
@ -161,18 +152,14 @@ class UserBehavior(TaskSequence):
|
||||
login(self)
|
||||
|
||||
@seq_task(1)
|
||||
def home(l):
|
||||
get_index(l)
|
||||
|
||||
@seq_task(2)
|
||||
def portfolios(l):
|
||||
get_portfolios(l)
|
||||
|
||||
@seq_task(3)
|
||||
@seq_task(2)
|
||||
def pick_a_portfolio(l):
|
||||
get_portfolio(l)
|
||||
|
||||
@seq_task(4)
|
||||
@seq_task(3)
|
||||
def pick_an_app(l):
|
||||
get_app(l)
|
||||
|
||||
@ -189,4 +176,3 @@ class WebsiteUser(HttpLocust):
|
||||
if __name__ == "__main__":
|
||||
# if run as the main file, will spin up a single locust
|
||||
WebsiteUser().run()
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# script/minikube_setup: Set up local AT-AT cluster on Minikube
|
||||
|
||||
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
|
||||
|
||||
output_divider "Start Minikube"
|
||||
minikube start
|
||||
|
||||
output_divider "Use Minikube Docker environment"
|
||||
eval $(minikube docker-env)
|
||||
|
||||
output_divider "Build AT-AT Docker image for Minikube registry"
|
||||
docker build . -t atat:latest
|
||||
|
||||
output_divider "Pull images for AT-AT cluster"
|
||||
docker pull redis:5.0-alpine
|
||||
docker pull postgres:11-alpine
|
||||
docker pull nginx:alpine
|
||||
|
||||
output_divider "Apply AT-AT Kubernetes config to Minikube cluster"
|
||||
kubectl --context=minikube create namespace atat
|
||||
kubectl --context=minikube apply -f deploy/minikube/
|
||||
|
||||
output_divider "Create database and apply migrations"
|
||||
# wait for the datastore deployment to become available
|
||||
kubectl --context=minikube -n atat wait --for=condition=Available deployment/datastores
|
||||
# postgres isn't necessarily running as soon as the pod is available, so wait a few
|
||||
sleep 3
|
||||
DB_POD=$(kubectl --context=minikube -n atat get pods -l app=db-cache -o custom-columns=NAME:.metadata.name --no-headers | sed -n 1p)
|
||||
ATST_POD=$(kubectl --context=minikube -n atat get pods -l app=atst -o custom-columns=NAME:.metadata.name --no-headers | sed -n 1p)
|
||||
kubectl --context=minikube -n atat exec -it $DB_POD -c postgres -- createdb -U postgres atat
|
||||
kubectl --context=minikube -n atat exec -it $ATST_POD -c atst -- .venv/bin/python .venv/bin/alembic upgrade head
|
@ -159,7 +159,7 @@ def get_users():
|
||||
|
||||
def add_members_to_portfolio(portfolio):
|
||||
for user_data in PORTFOLIO_USERS:
|
||||
invite = Portfolios.invite(portfolio, portfolio.owner, user_data)
|
||||
invite = Portfolios.invite(portfolio, portfolio.owner, {"user_data": user_data})
|
||||
profile = {
|
||||
k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"]
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEJDCCAwygAwIBAgIJAK4JGo3BBGhVMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
|
||||
BAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExFTATBgNVBAcTDFBoaWxhZGVs
|
||||
cGhpYTEMMAoGA1UEChMDRG9EMQwwCgYDVQQLEwNERFMxEDAOBgNVBAMTB0FUQVQg
|
||||
Q0EwHhcNMTgwNjAxMTk0NjIyWhcNMzgwNTI3MTk0NjIyWjBpMQswCQYDVQQGEwJV
|
||||
UzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWEx
|
||||
DDAKBgNVBAoTA0RvRDEMMAoGA1UECxMDRERTMRAwDgYDVQQDEwdBVEFUIENBMIIB
|
||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzYU7UbstArnnVliaC/TB6Vir
|
||||
kVWMnAEYMUZA1BKP8DZaNEKbzFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo
|
||||
13i5EwUUCSh2MdPfS8ZZt8DUIIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8
|
||||
VZkFEgvs8FCP0M4Ar6/gtJ24ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbH
|
||||
LkOM2gtp/pkYCCG0zqeU+0s3H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibk
|
||||
aI6sTTooXE5aSZkfkx0z6+fKM2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQID
|
||||
AQABo4HOMIHLMB0GA1UdDgQWBBSl7CUAWPbx8XqotKKKAufPh0wn4DCBmwYDVR0j
|
||||
BIGTMIGQgBSl7CUAWPbx8XqotKKKAufPh0wn4KFtpGswaTELMAkGA1UEBhMCVVMx
|
||||
FTATBgNVBAgTDFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlhMQww
|
||||
CgYDVQQKEwNEb0QxDDAKBgNVBAsTA0REUzEQMA4GA1UEAxMHQVRBVCBDQYIJAK4J
|
||||
Go3BBGhVMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABguwdFk42YP
|
||||
8U6Du5HQ6Is1jfc1KEOowdh0d2MCH8q0KNktqiu6kWzjH1gRjRwc07bAkAWqXPB6
|
||||
6gkRGYe/FRgi2Rn+Uo5UC5ahI4cXkE8OitCIEP3Br9fUw+vj/3Iiov0QZ6Hv81Kl
|
||||
ZTZhLiZbjAg5maL/vufnUp+n15qzm67APh3/2hcgO93UlE9o9vXohWy1lHs8u12o
|
||||
hPLxghSmGc9eKalEWEs61OrohpOtCHUEd1isq76WhaiXSwSUrBxgy89Z517A7ffC
|
||||
BjzLo5AVo6a9ou+ONVeZk8qw6YR6X9J7axy8YuTWt+Z82WFvOF0ubkqjm72d001M
|
||||
7R9zCOQ3O+g=
|
||||
-----END CERTIFICATE-----
|
@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAzYU7UbstArnnVliaC/TB6VirkVWMnAEYMUZA1BKP8DZaNEKb
|
||||
zFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo13i5EwUUCSh2MdPfS8ZZt8DU
|
||||
IIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8VZkFEgvs8FCP0M4Ar6/gtJ24
|
||||
ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbHLkOM2gtp/pkYCCG0zqeU+0s3
|
||||
H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibkaI6sTTooXE5aSZkfkx0z6+fK
|
||||
M2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQIDAQABAoIBAHR4EInc3UEyQVu5
|
||||
knM8Hbgzu+b86FZweFlUSuDkNBYZdz0ukkRUHvb+x3c9SRBLnL8CDv+AhqPWgo6M
|
||||
tIr6Aofkb4vMqnWQ5y3ZdEIApAa5PZbY/F4AGFql3wdO8H8CJ7ojBCTOSDiVYTnk
|
||||
1Lcjy9okshyAP1Ne1sPJo/bdB56HtXs+wqok1NntIQwiXjjD9xUuc1EZk0J4M97L
|
||||
vBUjUGNX942UjtRiey5zwhRp3bTPasTduHcA01NaIbOVYlRFwc2W+cflz0l6ml2p
|
||||
14TNEEvIMMMCNKnlPrpGI23n0psAvE4nbuxZQGVYAFvXrWn+Gyvz0Yag2EoMUCEs
|
||||
ziLED9ECgYEA6IByu+xqIuIAhj/PwIIxV4+lkuV4TXIlfAFLR4JuokOVfbRsmu2e
|
||||
9EfeOUD9LfQ4KsG5mu4Abpja0k/VKRKRGRjV6Oe2C6VK942HFP6Kpn0hgIuomZkD
|
||||
eVv8naDezZjAvVace38zjRWB2GXTpapwBAgf/YflPPsDZ8bi/weqZCMCgYEA4kqx
|
||||
Ka489Rr7+cSXpMeS5lLufhlaE5OVQc5HVFREDAI5vXU8BM2sLiHTC/BHjis2JvLm
|
||||
aRJ0UsxUoIUURl2KjTbx3zns4HDVkzBrSpoDXWxBjAo0oEg7JVc+6+qEqbDHHS1L
|
||||
/UJ6mlUegsE42MkFWG3YJQuHxyLZqPXIwNAyhZMCgYEA5cxnGnSt5rJoAEi7xzMn
|
||||
H7s71Hf3stw6TlldFV3GiZyw+aDFo09vR1RtQTuJwczbYu88yvOn+6gax7neHo1a
|
||||
WmrgqiWzGcmS0iDRPZ/kXG/bGBlxV/cTpvSTNx0UejMbdUhQvANaaXyzbLYgPWK6
|
||||
+lEphUW2/tG+aOj73UOvVu8CgYA5L8sJz4CUKJeZDTeNauoSzs56i4mZ/OfxU2Hv
|
||||
S8ROjJlu6ZubUya6Gc4t7DEJGp56xVO5JfLDoeOZFUiEZ8tF2KbTVN4p8hnnMotK
|
||||
tRU4nM0LyOB3yQk5bIz4LbIM+CG5m+LiQ9Sb//rP7GijUFnLeSbwZbOQfZwn+MUd
|
||||
BQBfhQKBgQDmuX8tJdPkjE133IhQhZHbHHt6AEQA3aXkFdvPvbYD9VbGTZ8wnpFO
|
||||
VJrDDWnIKAgO2FerIX9oq+H9a5fggYtTMeAX1cOA6b9SnLmFjt0utxrQKxf7p5I+
|
||||
n+EsmcAWfb+KRQwoB0L/mE9Ool14AeJ15kHyNIrCrMPv0J4zoC0Jdg==
|
||||
-----END RSA PRIVATE KEY-----
|
@ -1 +0,0 @@
|
||||
F4D74F1607DD3C83
|
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Generate the root (GIVE IT A PASSWORD IF YOU'RE NOT AUTOMATING SIGNING!):
|
||||
echo 'MAKING CA'
|
||||
openssl genrsa -out certificate-authority/ca.key 2048
|
||||
openssl req -new -x509 -days 7300 -key certificate-authority/ca.key -sha256 -extensions v3_ca -out certificate-authority/ca.crt
|
||||
|
||||
# Generate the domain key:
|
||||
openssl genrsa -out server-certs/dev.cac.atat.codes.key 2048
|
||||
|
||||
echo 'MAKING CSR'
|
||||
# Generate the certificate signing request
|
||||
openssl req -nodes -sha256 -new -key server-certs/dev.cac.atat.codes.key -out server-certs/dev.cac.atat.codes.csr -reqexts SAN -config <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend"))
|
||||
|
||||
# Sign the request with your root key
|
||||
openssl x509 -sha256 -req -in server-certs/dev.cac.atat.codes.csr -CA certificate-authority/ca.crt -CAkey certificate-authority/ca.key -CAcreateserial -out server-certs/dev.cac.atat.codes.crt -days 7300 -extfile <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend")) -extensions SAN
|
||||
|
||||
# Check your homework:
|
||||
openssl verify -CAfile certificate-authority/ca.crt server-certs/dev.cac.atat.codes.crt
|
@ -38,6 +38,7 @@
|
||||
@import "components/dod_login_notice.scss";
|
||||
@import "components/sticky_cta.scss";
|
||||
@import "components/error_page.scss";
|
||||
@import "components/member_form.scss";
|
||||
|
||||
@import "sections/login";
|
||||
@import "sections/home";
|
||||
|
@ -4,6 +4,20 @@
|
||||
background-color: $color-gray-lightest;
|
||||
margin-top: $gap * 5;
|
||||
|
||||
&--white {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
&--centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__message {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-left: -$gap * 3;
|
||||
margin-right: -$gap * 3;
|
||||
|
61
styles/components/_member_form.scss
Normal file
61
styles/components/_member_form.scss
Normal file
@ -0,0 +1,61 @@
|
||||
.member-form {
|
||||
text-align: left;
|
||||
|
||||
input[type="checkbox"] + label::before {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
text-align: left;
|
||||
|
||||
.usa-input__choices label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
padding: $gap * 2;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
|
||||
&.checked {
|
||||
border: 1px solid $color-blue;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
p.usa-input__help {
|
||||
margin-bottom: 0;
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.usa-input {
|
||||
width: 45rem;
|
||||
|
||||
input,
|
||||
label,
|
||||
.usa-input__message {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
label .icon-validation {
|
||||
left: unset;
|
||||
right: -$gap * 4;
|
||||
}
|
||||
|
||||
&--validation--phoneExt {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#modal--add-app-mem,
|
||||
#modal--add-portfolio-manager {
|
||||
.modal__body {
|
||||
min-width: 75rem;
|
||||
}
|
||||
}
|
@ -5,13 +5,6 @@
|
||||
}
|
||||
|
||||
margin-left: 2 * $gap;
|
||||
|
||||
.line {
|
||||
box-sizing: border-box;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
border: 1px solid $color-gray-lightest;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-header {
|
||||
@ -40,36 +33,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__budget {
|
||||
font-size: $small-font-size;
|
||||
align-items: center;
|
||||
|
||||
.icon-tooltip {
|
||||
margin-left: -$gap / 2;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--dollars {
|
||||
font-size: $h2-font-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--amount {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&--cents {
|
||||
font-size: 2rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-left: -0.7rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
justify-content: center;
|
||||
font-size: $small-font-size;
|
||||
@ -109,22 +72,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-left {
|
||||
width: 12.5rem;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
margin-left: -0.4rem;
|
||||
}
|
||||
|
||||
.unfunded {
|
||||
color: $color-red;
|
||||
.icon {
|
||||
@include icon-color($color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin subheading {
|
||||
@ -138,6 +85,10 @@
|
||||
.portfolio-content {
|
||||
margin: (4 * $gap) $gap 0 $gap;
|
||||
|
||||
.panel {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
a.add-new-button {
|
||||
display: inherit;
|
||||
margin-left: auto;
|
||||
@ -157,44 +108,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
input.usa-button.usa-button-primary {
|
||||
width: 9rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
select {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.members-table-ppoc {
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
float: right;
|
||||
margin: 5px 0px;
|
||||
padding: 0px 24px;
|
||||
background-image: none;
|
||||
-ms-word-break: normal;
|
||||
word-break: normal;
|
||||
padding-right: 3rem;
|
||||
padding-left: 1.2rem;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
box-shadow: none;
|
||||
color: $color-gray;
|
||||
}
|
||||
}
|
||||
|
||||
a.modal-link.icon-link {
|
||||
float: right;
|
||||
|
||||
|
@ -33,7 +33,7 @@ $title-font-size: 5.2rem;
|
||||
$h1-font-size: 4rem;
|
||||
$h2-font-size: 3rem;
|
||||
$h3-font-size: 2.3rem;
|
||||
$h4-font-size: 1.7rem;
|
||||
$h4-font-size: 1.9rem;
|
||||
$h5-font-size: 1.5rem;
|
||||
$h6-font-size: 1.3rem;
|
||||
$base-line-height: 1.5;
|
||||
@ -44,6 +44,7 @@ $font-sans: "Source Sans Pro", sans-serif;
|
||||
$font-serif: "Merriweather", serif;
|
||||
|
||||
$font-normal: 400;
|
||||
$font-semibold: 600;
|
||||
$font-bold: 700;
|
||||
|
||||
// Color
|
||||
|
@ -12,6 +12,7 @@
|
||||
.usa-button,
|
||||
a {
|
||||
margin: 0 0 0 $gap;
|
||||
cursor: pointer;
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin: 0 0 0 ($gap * 2);
|
||||
|
@ -21,24 +21,8 @@ table.atat-table {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--shrink {
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
&--expand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--hide-small {
|
||||
display: none;
|
||||
|
||||
@include media($medium-screen) {
|
||||
display: table-cell;
|
||||
}
|
||||
&--third {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,28 +67,6 @@ table.atat-table {
|
||||
|
||||
@include panel-margin;
|
||||
|
||||
&__header {
|
||||
@include panel-base;
|
||||
@include panel-theme-default;
|
||||
|
||||
border-top: none;
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $gap * 2;
|
||||
|
||||
&__title {
|
||||
@include h4;
|
||||
|
||||
font-size: $lead-font-size;
|
||||
justify-content: space-between;
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -23,66 +23,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#modal--add-app-mem,
|
||||
.form-content--app-mem {
|
||||
text-align: left;
|
||||
|
||||
.modal__body {
|
||||
min-width: 75rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label::before {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
text-align: left;
|
||||
|
||||
.usa-input__choices label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
padding: $gap * 2;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
|
||||
&.checked {
|
||||
border: 1px solid $color-blue;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
p.usa-input__help {
|
||||
margin-bottom: 0;
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.application-member__user-info {
|
||||
.usa-input {
|
||||
width: 45rem;
|
||||
|
||||
input,
|
||||
label,
|
||||
.usa-input__message {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
label .icon-validation {
|
||||
left: unset;
|
||||
right: -$gap * 4;
|
||||
}
|
||||
|
||||
&--validation--phoneExt {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.environment-roles {
|
||||
padding: 0 ($gap * 3) ($gap * 3);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
.task-order {
|
||||
margin-top: $gap * 4;
|
||||
margin-bottom: $footer-height;
|
||||
width: 900px;
|
||||
|
||||
&__amount {
|
||||
|
@ -118,7 +118,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro InfoFields(member_form) %}
|
||||
<div class="application-member__user-info">
|
||||
<div class="user-info">
|
||||
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
|
||||
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
|
||||
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
||||
|
@ -1,12 +1,10 @@
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/label.html" import Label %}
|
||||
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
||||
{% import "components/member_form.html" as member_form %}
|
||||
{% import "applications/fragments/member_form_fields.html" as member_fields %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
{% from "components/save_button.html" import SaveButton %}
|
||||
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
||||
|
||||
{% macro MemberManagementTemplate(
|
||||
application,
|
||||
@ -25,7 +23,7 @@
|
||||
|
||||
<div class="panel">
|
||||
{% if not application.members %}
|
||||
<div class='empty-state panel__content'>
|
||||
<div class='empty-state empty-state--centered empty-state--white panel__content'>
|
||||
<p class='empty-state__message'>
|
||||
{{ ("portfolios.applications.members.blank_slate" | translate) }}
|
||||
</p>
|
||||
@ -179,8 +177,19 @@
|
||||
form=new_member_form,
|
||||
form_action=url_for(action_new, application_id=application.id),
|
||||
steps=[
|
||||
member_steps.MemberStepOne(new_member_form),
|
||||
member_steps.MemberStepTwo(new_member_form, application)
|
||||
member_form.BasicStep(
|
||||
title="portfolios.applications.members.form.add_member"|translate,
|
||||
form=member_fields.InfoFields(new_member_form.user_data),
|
||||
next_button_text="portfolios.applications.members.form.next_button"|translate,
|
||||
previous=False,
|
||||
modal=new_member_modal_name,
|
||||
),
|
||||
member_form.SubmitStep(
|
||||
name=new_member_modal_name,
|
||||
form=member_fields.PermsFields(form=new_member_form, new=True),
|
||||
submit_text="portfolios.applications.members.form.add_member"|translate,
|
||||
modal=new_member_modal_name,
|
||||
)
|
||||
],
|
||||
) }}
|
||||
{% endif %}
|
||||
|
@ -1,50 +0,0 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% import "applications/fragments/member_form_fields.html" as member_fields %}
|
||||
|
||||
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
|
||||
<hr class="full-width">
|
||||
{% if title %} <h1>{{ title }}</h1> {% endif %}
|
||||
|
||||
{{ caller() }}
|
||||
|
||||
<div class='action-group'>
|
||||
{{ next_button }}
|
||||
{% if previous %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="previous()"
|
||||
class='action-group__action usa-button usa-button-secondary'
|
||||
value='{{ "common.previous" | translate }}'>
|
||||
{% endif %}
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro MemberStepOne(member_form) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="next()"
|
||||
v-bind:disabled="!canSave"
|
||||
class='action-group__action usa-button'
|
||||
value='{{ "portfolios.applications.members.form.next_button" | translate }}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberFormTemplate(title="portfolios.applications.members.form.add_member"|translate, next_button=next_button, previous=False) %}
|
||||
{{ member_fields.InfoFields(member_form.user_data) }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
{% macro MemberStepTwo(member_form, application) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type="submit"
|
||||
class='action-group__action usa-button'
|
||||
form="add-app-mem"
|
||||
v-bind:disabled="!canSave"
|
||||
value='{{ "portfolios.applications.members.form.add_member" | translate}}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberFormTemplate(next_button=next_button) %}
|
||||
{{ member_fields.PermsFields(form=member_form, new=True) }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
@ -25,7 +25,7 @@
|
||||
|
||||
<div class='usa-alert-body'>
|
||||
{% if vue_template %}
|
||||
<h3 class='usa-alert-heading' v-html='title'></h3>
|
||||
<h3 class='usa-alert-heading' v-text='title'></h3>
|
||||
{% elif title %}
|
||||
<h3 class='usa-alert-heading'>{{ title | safe }}</h3>
|
||||
{% endif %}
|
||||
|
@ -57,7 +57,7 @@
|
||||
<span class='usa-input__message'>{{ "forms.task_order.clin_funding_errors.obligated_amount_error" | translate }}</span>
|
||||
</template>
|
||||
<template v-else-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class='usa-input__message'></span>
|
||||
|
@ -23,7 +23,7 @@
|
||||
inline-template>
|
||||
<div class="clin-card" v-if="showClin">
|
||||
<div class="card__title">
|
||||
<span class="h4" v-html='clinTitle'></span>
|
||||
<span class="h4" v-text='clinTitle'></span>
|
||||
<button
|
||||
v-if='clinIndex > 0'
|
||||
class="icon-link icon-link__remove-clin"
|
||||
@ -68,7 +68,7 @@
|
||||
<input type='hidden' v-bind:value='rawValue' :name='name' />
|
||||
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class='usa-input__message'></span>
|
||||
@ -119,7 +119,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="h5 clin-card__title">Percent Obligated</div>
|
||||
<p id="percent-obligated" v-html='percentObligated'></p>
|
||||
<p id="percent-obligated" v-text='percentObligated'></p>
|
||||
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
@ -140,7 +140,7 @@
|
||||
<div class='modal__dialog' role='dialog' aria-modal='true'>
|
||||
<div class='modal__body'>
|
||||
<div class="task-order__modal-cancel">
|
||||
<h1 v-html='"{{ 'task_orders.form.clin_remove_text' | translate }}" + clinTitle + "?"'></h1>
|
||||
<h1 v-text='"{{ 'task_orders.form.clin_remove_text' | translate }}" + clinTitle + "?"'></h1>
|
||||
<div class="task-order__modal-cancel_buttons">
|
||||
<button
|
||||
v-on:click='closeModal(removeModalId)'
|
||||
|
@ -9,11 +9,15 @@
|
||||
"text": "changes pending",
|
||||
"color": "default",
|
||||
},
|
||||
"ppoc": {"text": "primary point of contact"}
|
||||
} %}
|
||||
|
||||
{% if type -%}
|
||||
{% if type in label_info.keys() -%}
|
||||
<span class='label label--{{ label_info[type]["color"] }} {{ classes }}'>
|
||||
{{ Icon(label_info[type]["icon"]) }} {{ label_info[type]["text"] }}
|
||||
{% if label_info[type]["icon"] %}
|
||||
{{ Icon(label_info[type]["icon"]) }}
|
||||
{% endif %}
|
||||
{{ label_info[type]["text"] }}
|
||||
</span>
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
65
templates/components/member_form.html
Normal file
65
templates/components/member_form.html
Normal file
@ -0,0 +1,65 @@
|
||||
<!-- Layout macro -->
|
||||
{% macro MemberForm(title=None, next_button=None, previous=True, modal=modal) %}
|
||||
<div class="member-form">
|
||||
<hr class="full-width">
|
||||
{% if title %} <h2>{{ title }}</h2> {% endif %}
|
||||
|
||||
{{ caller() }}
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
{{ next_button }}
|
||||
{% if previous %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="previous()"
|
||||
class='action-group__action usa-button usa-button-secondary'
|
||||
value='{{ "common.previous" | translate }}'>
|
||||
{% endif %}
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ modal }}')">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Step macros to use with MultiStepModalForm -->
|
||||
{% macro BasicStep(
|
||||
title=None,
|
||||
form=form,
|
||||
next_button_text=next_button_text,
|
||||
previous=True,
|
||||
modal=modal
|
||||
) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="next()"
|
||||
v-bind:disabled="!canSave"
|
||||
class='action-group__action usa-button'
|
||||
value='{{ next_button_text }}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
|
||||
{{ form }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro SubmitStep(
|
||||
name=name,
|
||||
title=None,
|
||||
form=form,
|
||||
submit_text=submit_text,
|
||||
previous=True,
|
||||
modal=modal
|
||||
) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type="submit"
|
||||
class='action-group__action usa-button'
|
||||
form="{{ name }}"
|
||||
v-bind:disabled="!canSave"
|
||||
value='{{ submit_text }}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
|
||||
{{ form }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
@ -54,7 +54,7 @@
|
||||
|
||||
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
|
||||
</fieldset>
|
||||
|
@ -48,7 +48,7 @@
|
||||
{{ field(disabled=disabled) }}
|
||||
|
||||
<template v-if='showError'>
|
||||
<span class='usa-input__message' v-html='validationError'></span>
|
||||
<span class='usa-input__message' v-text='validationError'></span>
|
||||
</template>
|
||||
|
||||
</fieldset>
|
||||
|
@ -107,7 +107,7 @@
|
||||
/>
|
||||
|
||||
{% if show_validation %}
|
||||
<span v-if='showError' class='usa-input__message' v-html='validationError'></span>
|
||||
<span v-if='showError' class='usa-input__message' v-text='validationError'></span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div>
|
||||
<div v-show="valid" class="uploaded-file">
|
||||
{{ Icon("ok") }}
|
||||
<a class="uploaded-file__name" v-html="baseName" v-bind:href="downloadLink"></a>
|
||||
<a class="uploaded-file__name" v-text="baseName" v-bind:href="downloadLink"></a>
|
||||
<a href="#" class="uploaded-file__remove" v-on:click="removeAttachment">Remove</a>
|
||||
</div>
|
||||
<div v-show="valid === false" v-bind:class='{ "usa-input": true, "usa-input--error": showErrors }'>
|
||||
@ -49,6 +49,9 @@
|
||||
<template v-if="sizeError">
|
||||
<span class="usa-input__message">{{ "forms.task_order.size_error" | translate }}</span>
|
||||
</template>
|
||||
<template v-if="filenameError">
|
||||
<span class="usa-input__message">{{ "forms.task_order.filename_error" | translate }}</span>
|
||||
</template>
|
||||
{% for error, error_messages in field.errors.items() %}
|
||||
<span class="usa-input__message">{{error_messages[0]}}</span>
|
||||
{% endfor %}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% extends "portfolios/base.html" %}
|
||||
|
||||
{% from "components/label.html" import Label %}
|
||||
{% from "components/pagination.html" import Pagination %}
|
||||
{% from 'components/save_button.html' import SaveButton %}
|
||||
{% from 'components/sticky_cta.html' import StickyCTA %}
|
||||
@ -9,8 +10,8 @@
|
||||
|
||||
{{ StickyCTA(text="Settings") }}
|
||||
|
||||
<div v-cloak class="portfolio-admin portfolio-content">
|
||||
|
||||
<div v-cloak class="portfolio-admin">
|
||||
{% include "fragments/flash.html" %}
|
||||
<!-- max width of this section is 460px -->
|
||||
<section class="form-container__half">
|
||||
<h3>Portfolio name and component</h3>
|
||||
|
@ -1,79 +0,0 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
|
||||
{% macro SimpleOptionsInput(field) %}
|
||||
<div class="usa-input">
|
||||
<fieldset data-ally-disabled="true" class="usa-input__choices">
|
||||
<legend>
|
||||
<div class="usa-input__title-inline">
|
||||
{{ field.label | striptags}}
|
||||
</div>
|
||||
</legend>
|
||||
{{ field() }}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% set step_one %}
|
||||
<hr class="full-width">
|
||||
<h1>Invite new portfolio member</h1>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(member_form.user_data.first_name, validation='requiredField', optional=False) }}
|
||||
</div>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(member_form.user_data.last_name, validation='requiredField', optional=False) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(member_form.user_data.email, validation='email', optional=False) }}
|
||||
</div>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(member_form.user_data.phone_number, validation='usPhone') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--half'>
|
||||
{{ TextInput(member_form.user_data.dod_id, validation='dodId', optional=False) }}
|
||||
</div>
|
||||
<div class='form-col form-col--half'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="next()"
|
||||
v-bind:disabled="!canSave"
|
||||
class='action-group__action usa-button usa-button-primary'
|
||||
value='Next'>
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">Cancel</a>
|
||||
</div>
|
||||
{% endset %}
|
||||
{% set step_two %}
|
||||
<hr class="full-width">
|
||||
<h1>Assign member permissions</h1>
|
||||
<a class='icon-link'>
|
||||
{{ Icon('info') }}
|
||||
{{ "portfolios.admin.permissions_info" | translate }}
|
||||
</a>
|
||||
{{ SimpleOptionsInput(member_form.permission_sets.perms_app_mgmt) }}
|
||||
{{ SimpleOptionsInput(member_form.permission_sets.perms_funding) }}
|
||||
{{ SimpleOptionsInput(member_form.permission_sets.perms_reporting) }}
|
||||
{{ SimpleOptionsInput(member_form.permission_sets.perms_portfolio_mgmt) }}
|
||||
<div class='action-group'>
|
||||
<input
|
||||
type="submit"
|
||||
class='action-group__action usa-button usa-button-primary'
|
||||
form="add-port-mem"
|
||||
value='Invite member'>
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">Cancel</a>
|
||||
</div>
|
||||
{% endset %}
|
||||
{{ MultiStepModalForm(
|
||||
'add-port-mem',
|
||||
member_form,
|
||||
url_for("portfolios.invite_member", portfolio_id=portfolio.id),
|
||||
[step_one, step_two],
|
||||
) }}
|
37
templates/portfolios/fragments/member_form_fields.html
Normal file
37
templates/portfolios/fragments/member_form_fields.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/phone_input.html" import PhoneInput %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% macro PermsFields(form, member_role_id=None) %}
|
||||
<h2>Set Portfolio Permissions</h2>
|
||||
<div class="portfolio-perms">
|
||||
{% if new %}
|
||||
{% set app_mgmt = form.perms_app_mgmt.name %}
|
||||
{% set funding = form.perms_funding.name %}
|
||||
{% set reporting = form.perms_reporting.name %}
|
||||
{% set portfolio_mgmt = form.perms_portfolio_mgmt.name %}
|
||||
{% else %}
|
||||
{% set app_mgmt = "perms_app_mgmt-{}".format(member_role_id) %}
|
||||
{% set funding = "perms_funding-{}".format(member_role_id) %}
|
||||
{% set reporting = "perms_reporting-{}".format(member_role_id) %}
|
||||
{% set portfolio_mgmt = "perms_portfolio_mgmt-{}".format(member_role_id) %}
|
||||
{% endif %}
|
||||
|
||||
{{ CheckboxInput(form.perms_app_mgmt, classes="input__inline-fields", key=app_mgmt, id=app_mgmt, optional=True) }}
|
||||
{{ CheckboxInput(form.perms_funding, classes="input__inline-fields", key=funding, id=funding, optional=True) }}
|
||||
{{ CheckboxInput(form.perms_reporting, classes="input__inline-fields", key=reporting, id=reporting, optional=True) }}
|
||||
{{ CheckboxInput(form.perms_portfolio_mgmt, classes="input__inline-fields", key=portfolio_mgmt, id=portfolio_mgmt, optional=True) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro InfoFields(member_form) %}
|
||||
<div class="user-info">
|
||||
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
|
||||
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
|
||||
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
||||
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}}
|
||||
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }}
|
||||
<a href="#">How do I find the DoD ID?</a>
|
||||
</div>
|
||||
{% endmacro %}
|
@ -1,38 +0,0 @@
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/options_input.html" import OptionsInput %}
|
||||
|
||||
{% for subform in member_perms_form.members_permissions %}
|
||||
{% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, subform.member_id.data) %}
|
||||
{% set ppoc = subform.member_id.data == ppoc_id %}
|
||||
{% set archive_button_class = 'button-danger-outline' %}
|
||||
|
||||
<tr {% if ppoc %}class="members-table-ppoc"{% endif %}>
|
||||
<td class='name'>{{ subform.member_name.data }}
|
||||
<div>
|
||||
{% if ppoc %}
|
||||
{% set archive_button_class = 'usa-button-disabled' %}
|
||||
<span class='you'>PPoC</span>
|
||||
{% endif %}
|
||||
{% if subform.member_id.data == current_member_id %}
|
||||
{% set archive_button_class = 'usa-button-disabled' %}
|
||||
<span class='you'>(<span class='green'>you</span>)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>{{ OptionsInput(subform.perms_app_mgmt, label=False, disabled=ppoc) }}</td>
|
||||
<td>{{ OptionsInput(subform.perms_funding, label=False, disabled=ppoc) }}</td>
|
||||
<td>{{ OptionsInput(subform.perms_reporting, label=False, disabled=ppoc) }}</td>
|
||||
<td>{{ OptionsInput(subform.perms_portfolio_mgmt, label=False, disabled=ppoc) }}</td>
|
||||
|
||||
<td>
|
||||
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ archive_button_class }}'>
|
||||
{{ "portfolios.members.archive_button" | translate }}
|
||||
</a>
|
||||
{% if not ppoc %}
|
||||
{{ subform.member_id() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
@ -1,26 +0,0 @@
|
||||
{% for subform in member_perms_form.members_permissions %}
|
||||
{% set ppoc = subform.member_id.data == ppoc_id %}
|
||||
{% set heading_perms = [subform.perms_app_mgmt, subform.perms_funding, subform.perms_reporting, subform.perms_portfolio_mgmt] %}
|
||||
|
||||
<tr>
|
||||
<td class='name'>{{ subform.member_name.data }}
|
||||
<div>
|
||||
{% if ppoc %}
|
||||
<span class='you'>PPoC</span>
|
||||
{% endif %}
|
||||
{% if subform.member_id.data == current_member_id %}
|
||||
<span class='you'>(<span class='green'>you</span>)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{% for access in heading_perms %}
|
||||
{% if dict(access.choices).get(access.data) == ('portfolios.members.permissions.edit_access' | translate) %}
|
||||
<td class='green'>{{ 'portfolios.members.permissions.edit_access' | translate }}</td>
|
||||
{% else %}
|
||||
<td>{{ 'common.view' | translate }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
@ -1,105 +1,74 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from 'components/save_button.html' import SaveButton %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% import "components/member_form.html" as member_form %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
{% from 'components/save_button.html' import SaveButton %}
|
||||
{% import "portfolios/fragments/member_form_fields.html" as member_form_fields %}
|
||||
|
||||
<section class="member-list" id="portfolio-members">
|
||||
<div class='responsive-table-wrapper panel accordion-table'>
|
||||
{% if g.matchesPath("portfolio-members") %}
|
||||
{% include "fragments/flash.html" %}
|
||||
{% endif %}
|
||||
<base-form inline-template>
|
||||
<form method='POST' id="member-perms" action='{{ url_for("portfolios.edit_members", portfolio_id=portfolio.id) }}' autocomplete="off" enctype="multipart/form-data">
|
||||
{{ member_perms_form.csrf_token }}
|
||||
<h3>Portfolio Managers</h3>
|
||||
<div class="panel">
|
||||
<section class="member-list">
|
||||
<div class="responsive-table-wrapper">
|
||||
<table class="atat-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-cell--third">Name</th>
|
||||
<th>Portfolio Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in members -%}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ member.user_name }}{% if member.role_id == current_member_id %} (You){% endif %}</strong>
|
||||
<br>
|
||||
{% if member.ppoc %}
|
||||
{{ Label(type="ppoc", classes='label--below label--purple')}}
|
||||
{% endif %}
|
||||
{{ Label(type=member.status, classes='label--below')}}
|
||||
</td>
|
||||
<td>
|
||||
{% for perm, value in member.permission_sets.items() -%}
|
||||
<div>
|
||||
{% if value -%}
|
||||
{{ ("portfolios.admin.members.{}.{}".format(perm, value)) | translate }}
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%-endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class='application-list-item'>
|
||||
<header>
|
||||
<div class='responsive-table-wrapper__header'>
|
||||
<div class='responsive-table-wrapper__title'>
|
||||
<div class='h3'>{{ "portfolios.admin.portfolio_members_title" | translate }}</div>
|
||||
<div class='subheading'>
|
||||
{{ "portfolios.admin.portfolio_members_subheading" | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class='icon-link'>
|
||||
{{ Icon('info') }}
|
||||
{{ "portfolios.admin.settings_info" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if not portfolio.members %}
|
||||
<p>{{ "portfolios.admin.no_members" | translate }}</p>
|
||||
{% else %}
|
||||
<table class="atat-table">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ "portfolios.members.permissions.name" | translate }}</td>
|
||||
<td>{{ "portfolios.members.permissions.app_mgmt" | translate }}</td>
|
||||
<td>{{ "portfolios.members.permissions.funding" | translate }}</td>
|
||||
<td>{{ "portfolios.members.permissions.reporting" | translate }}</td>
|
||||
<td>{{ "portfolios.members.permissions.portfolio_mgmt" | translate }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
|
||||
{% include "portfolios/fragments/members_edit.html" %}
|
||||
{% elif user_can(permissions.VIEW_PORTFOLIO_USERS) %}
|
||||
{% include "portfolios/fragments/members_view.html" %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="panel__footer">
|
||||
<div class="action-group save">
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
|
||||
{{ SaveButton(text=('common.save' | translate), element="input", form="member-perms") }}
|
||||
{% endif %}
|
||||
|
||||
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
|
||||
<a class="icon-link modal-link" v-on:click="openModal('add-port-mem')">
|
||||
{{ "portfolios.admin.add_new_member" | translate }}
|
||||
{{ Icon("plus") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</base-form>
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_USERS) %}
|
||||
{% for subform in member_perms_form.members_permissions %}
|
||||
{% set modal_id = "portfolio_id_{}_user_id_{}".format(portfolio.id, subform.member_id.data) %}
|
||||
{% call Modal(name=modal_id, dismissable=False) %}
|
||||
<h1>{{ "portfolios.admin.alert_header" | translate }}</h1>
|
||||
<hr>
|
||||
{{
|
||||
Alert(
|
||||
title="portfolios.admin.alert_title" | translate,
|
||||
message="portfolios.admin.alert_message" | translate,
|
||||
level="warning"
|
||||
)
|
||||
}}
|
||||
<div class="action-group">
|
||||
<form method="POST" action="{{ url_for('portfolios.remove_member', portfolio_id=portfolio.id, portfolio_role_id=subform.member_id.data)}}">
|
||||
{{ member_perms_form.csrf_token }}
|
||||
<button class="usa-button usa-button-danger">
|
||||
{{ "portfolios.members.archive_button" | translate }}
|
||||
</button>
|
||||
</form>
|
||||
<a v-on:click="closeModal('{{ modal_id }}')" class="action-group__action icon-link icon-link--default">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if user_can(permissions.CREATE_PORTFOLIO_USERS) %}
|
||||
{% include "portfolios/fragments/add_new_portfolio_member.html" %}
|
||||
{% set new_manager_modal = "add-portfolio-manager" %}
|
||||
<a class="usa-button usa-button-secondary add-new-button" v-on:click="openModal('{{ new_manager_modal }}')">
|
||||
Add Portfolio Manager
|
||||
</a>
|
||||
|
||||
{{ MultiStepModalForm(
|
||||
name=new_manager_modal,
|
||||
form=new_manager_form,
|
||||
form_action=url_for("portfolios.invite_member", portfolio_id=portfolio.id),
|
||||
steps=[
|
||||
member_form.BasicStep(
|
||||
title="Add Manager",
|
||||
form=member_form_fields.InfoFields(new_manager_form.user_data),
|
||||
next_button_text="Next: Permissions",
|
||||
previous=False,
|
||||
modal=new_manager_modal_name,
|
||||
),
|
||||
member_form.SubmitStep(
|
||||
name=new_manager_modal,
|
||||
form=member_form_fields.PermsFields(new_manager_form),
|
||||
submit_text="Add Mananger",
|
||||
modal=new_manager_modal_name,
|
||||
)
|
||||
],
|
||||
) }}
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
@ -37,19 +37,19 @@
|
||||
<tr>
|
||||
<td>
|
||||
<button v-on:click='toggle($event, applicationIndex)' class='icon-link icon-link--large'>
|
||||
<span v-html='application.name'></span>
|
||||
<span v-text='application.name'></span>
|
||||
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
|
||||
<template v-else>{{ Icon('caret_up') }}</template>
|
||||
</button>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application.this_month || 0)'></span>
|
||||
<span v-text='formatDollars(application.this_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application.last_month || 0)'></span>
|
||||
<span v-text='formatDollars(application.last_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(application.total || 0)'></span>
|
||||
<span v-text='formatDollars(application.total || 0)'></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
@ -58,16 +58,16 @@
|
||||
v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']"
|
||||
>
|
||||
<td>
|
||||
<span class="reporting-spend-table__env-row-label" v-html='environment.name'></span>
|
||||
<span class="reporting-spend-table__env-row-label" v-text='environment.name'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment.this_month || 0)'></span>
|
||||
<span v-text='formatDollars(environment.this_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment.last_month || 0)'></span>
|
||||
<span v-text='formatDollars(environment.last_month || 0)'></span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(environment.total || 0)'></span>
|
||||
<span v-text='formatDollars(environment.total || 0)'></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% macro TOFormStepHeader(description, title=None, to_number=None) %}
|
||||
{% macro TOFormStepHeader(description=None, title=None, to_number=None) %}
|
||||
<div class="task-order__header">
|
||||
{% if title -%}
|
||||
<div class="h2">
|
||||
@ -10,8 +10,10 @@
|
||||
<strong>Task Order Number:</strong> {{ to_number }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{{ description }}
|
||||
</p>
|
||||
{% if description %}
|
||||
<p>
|
||||
{{ description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
@ -23,14 +23,9 @@
|
||||
<span class="summary-item__header-text">Total Task Order value</span>
|
||||
{{ Tooltip(("task_orders.review.tooltip.total_value" | translate), title="", classes="summary-item__header-icon") }}
|
||||
</h4>
|
||||
{% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %}
|
||||
{% if earliest_pop_start_date and latest_pop_end_date %}
|
||||
<p class="summary-item__value--large">
|
||||
{{ contract_amount | dollars }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="summary-item__value--large"> - </p>
|
||||
{% endif %}
|
||||
<p class="summary-item__value--large">
|
||||
{{ contract_amount | dollars }}
|
||||
</p>
|
||||
</div>
|
||||
<div class='col col--grow summary-item'>
|
||||
<h4 class="summary-item__header">
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends "task_orders/builder_base.html" %}
|
||||
|
||||
{% from "task_orders/fragments/task_order_view.html" import TaskOrderView %}
|
||||
{% from 'task_orders/form_header.html' import TOFormStepHeader %}
|
||||
|
||||
{% set action = url_for('task_orders.form_step_five_confirm_signature', task_order_id=task_order_id) %}
|
||||
{% set previous_button_link = url_for("task_orders.form_step_three_add_clins", task_order_id=task_order_id) %}
|
||||
@ -16,5 +17,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block to_builder_form_field %}
|
||||
{{ TOFormStepHeader(to_number=task_order.number) }}
|
||||
{{ TaskOrderView(task_order, portfolio, builder_mode=True) }}
|
||||
{% endblock %}
|
||||
|
@ -51,6 +51,19 @@ For Ubuntu 19.10
|
||||
snap install powershell --classic
|
||||
```
|
||||
|
||||
# Preview Features
|
||||
To create all the resources we need for this environment we'll need to enable some _Preview_ features.
|
||||
|
||||
This registers the specific feature for _SystemAssigned_ principals
|
||||
```
|
||||
az feature register --namespace Microsoft.ContainerService --name MSIPreview
|
||||
```
|
||||
|
||||
To apply the registration, run the following
|
||||
```
|
||||
az provider register -n Microsoft.ContainerService
|
||||
```
|
||||
|
||||
# Running Terraform
|
||||
First, you'll need to log in to Azure. With the Azure CLI installed, you can run the following.
|
||||
|
||||
@ -76,6 +89,50 @@ terraform apply
|
||||
|
||||
Check the output for errors. Sometimes the syntax is valid, but some of the configuration may be wrong and only rejected by the Azure API at run time. If this is the case, fix your mistake, and re-run.
|
||||
|
||||
# After running TF (Manual Steps)
|
||||
|
||||
## VM Scale Set
|
||||
After running terraform, we need to make a manual change to the VM Scale Set that is used in the kubernetes. Terraform has a bug that is not applying this as of `v1.40` of the `azurerm` provider.
|
||||
|
||||
In order to get the `SystemAssigned` identity to be set, it needs to be set manually in the console.
|
||||
|
||||
Navigate to the VM Scale Set for the k8s cluster you're managing (in the console).
|
||||
|
||||

|
||||
_Just click the `Status` to `On`_
|
||||
|
||||
## KeyVault Policy
|
||||
There is a bug (missing feature really) in the `azurerm` terraform provider which exposes the wrong `object_id/principal_id` in the `azurerm_kubernetes_cluster` output. The `id` that it exposes is the `object_id` of the cluster itself, and _not_ the Virtual Machine Scale Set SystemAssigned identity. This needs to be updated manually after running terraform for the first time.
|
||||
|
||||
To update, just edit the `keyvault.tf`. Set the `principal_id` to the `object_id` of the Virtual Machine Scale set. This can be found in the Azure portal, or via cli.
|
||||
|
||||
```
|
||||
az vmss list
|
||||
```
|
||||
In that list, find the scale set for the k8s cluster you're working on. You'll want the value of `principal_id`.
|
||||
|
||||
|
||||
The error looks like the following
|
||||
```
|
||||
Warning FailedMount 8s (x6 over 25s) kubelet, aks-default-54410534-vmss000001 MountVolume.SetUp failed for volume "flask-secret" : mount command failed, status: Failure, reason: /etc/kubernetes/volumeplugins/azure~kv/azurekeyvault-flex
|
||||
volume failed, Access denied. Caller was not found on any access policy. r nCaller: appid=e6651156-7127-432d-9617-4425177c48f1;oid=f9bcbe58-8b73-4957-aee2-133dc3e58063;numgroups=0;iss=https://sts.windows.net/b5ab0e1e-09f8-4258-afb7-fb17654bc5
|
||||
b3/ r nVault: cloudzero-dev-keyvault;location=eastus2 InnerError={code:AccessDenied}
|
||||
```
|
||||
|
||||
Final configuration will look like this.
|
||||
**keyvault.tf**
|
||||
```
|
||||
module "keyvault" {
|
||||
source = "../../modules/keyvault"
|
||||
name = var.name
|
||||
region = var.region
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
tenant_id = var.tenant_id
|
||||
principal_id = "f9bcbe58-8b73-4957-aee2-133dc3e58063"
|
||||
}
|
||||
```
|
||||
|
||||
# Shutting down and environment
|
||||
To shutdown and remove an environment completely as to not incur any costs you would need to run a `terraform destroy`.
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user