From 0e0898c3cfcade3e9117b3ae3c3c57d695d9f6ee Mon Sep 17 00:00:00 2001 From: Jens L Date: Sat, 22 Aug 2020 00:42:15 +0200 Subject: [PATCH] Flow exporting/importing (#187) * stages/*: Add SerializerModel as base model, implement serializer property * flows: add initial flow exporter and importer * policies/*: implement .serializer for all policies * root: fix missing dacite requirement --- Pipfile | 1 + Pipfile.lock | 97 ++++++++++++-- passbook/flows/models.py | 20 ++- passbook/flows/tests/test_transfer.py | 119 ++++++++++++++++ passbook/flows/transfer/__init__.py | 0 passbook/flows/transfer/common.py | 59 ++++++++ passbook/flows/transfer/exporter.py | 78 +++++++++++ passbook/flows/transfer/importer.py | 134 +++++++++++++++++++ passbook/lib/models.py | 13 ++ passbook/policies/dummy/models.py | 7 + passbook/policies/expiry/models.py | 7 + passbook/policies/expression/models.py | 7 + passbook/policies/group_membership/models.py | 9 ++ passbook/policies/hibp/models.py | 7 + passbook/policies/models.py | 12 +- passbook/policies/password/models.py | 7 + passbook/policies/reputation/models.py | 7 + passbook/stages/captcha/models.py | 7 + passbook/stages/consent/models.py | 7 + passbook/stages/dummy/models.py | 7 + passbook/stages/email/models.py | 7 + passbook/stages/identification/models.py | 7 + passbook/stages/invitation/models.py | 7 + passbook/stages/otp_static/models.py | 7 + passbook/stages/otp_time/models.py | 7 + passbook/stages/otp_validate/models.py | 7 + passbook/stages/password/models.py | 7 + passbook/stages/prompt/models.py | 18 ++- passbook/stages/user_delete/models.py | 7 + passbook/stages/user_login/models.py | 7 + passbook/stages/user_logout/models.py | 7 + passbook/stages/user_write/models.py | 7 + swagger.yaml | 2 - 33 files changed, 681 insertions(+), 21 deletions(-) create mode 100644 passbook/flows/tests/test_transfer.py create mode 100644 passbook/flows/transfer/__init__.py create mode 100644 passbook/flows/transfer/common.py create mode 100644 passbook/flows/transfer/exporter.py create mode 100644 passbook/flows/transfer/importer.py diff --git a/Pipfile b/Pipfile index ac753aa4b..be3fe4fdd 100644 --- a/Pipfile +++ b/Pipfile @@ -38,6 +38,7 @@ signxml = "*" structlog = "*" swagger-spec-validator = "*" urllib3 = {extras = ["secure"],version = "*"} +dacite = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index b159b74d3..687bad5cd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "616f5d355c42881b7ea70d4623bf885cff043d4c58913287960923df49c09909" + "sha256": "8f099b73d5993a0693261bf3d2b0e696d4f4d7ddd69a10d3db8ffe59a8ebd805" }, "pipfile-spec": 6, "requires": { @@ -21,6 +21,7 @@ "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.6.1" }, "asgiref": { @@ -28,6 +29,7 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], + "markers": "python_version >= '3.5'", "version": "==3.2.10" }, "attrs": { @@ -35,6 +37,7 @@ "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.1.0" }, "billiard": { @@ -46,6 +49,7 @@ }, "boto3": { "hashes": [ + "sha256:0d9cbeb5c8ca67650cc963c77e2e3b3ab5dffeeee16e03d61d740755f8fc7c44", "sha256:df73edf3bd6f191870212e04ae9a8bc6245fd6749f464e9fb950392a8d15bd8c" ], "index": "pypi", @@ -151,6 +155,14 @@ ], "version": "==2.9.2" }, + "dacite": { + "hashes": [ + "sha256:764c96e0304cb189628686689a163a6a3a8ce7bf3465f0a2d882a8b42f88108f", + "sha256:f7f269647ede90f8702728eb7dcb972051511c81b853a93c962fbd31f1753b9f" + ], + "index": "pypi", + "version": "==1.5.1" + }, "defusedxml": { "hashes": [ "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", @@ -257,6 +269,7 @@ "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" ], + "markers": "python_version >= '3.5'", "version": "==3.11.1" }, "djangorestframework-guardian": { @@ -273,6 +286,7 @@ "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.15.2" }, "drf-yasg": { @@ -302,6 +316,7 @@ "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, "idna": { @@ -316,6 +331,7 @@ "sha256:88b101b2668a1d81d6d72d4c2018e53bc6c7fc544c987849da1c7f77545c3bc9", "sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924" ], + "markers": "python_version >= '3.5'", "version": "==0.5.0" }, "itypes": { @@ -330,6 +346,7 @@ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.2" }, "jmespath": { @@ -337,6 +354,7 @@ "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.0" }, "jsonschema": { @@ -351,12 +369,16 @@ "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.6.11" }, "ldap3": { "hashes": [ + "sha256:b399c39e80b6459e349b33fbe9787c1bcbf86de05994d41806a05c06f3e7574d", + "sha256:bdaf568cd30fc0006c8bb4f5e6014554afeb0c4bbea1677de9706e278a4057e7", + "sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f", "sha256:59d1adcd5ead263387039e2a37d7cd772a2006b1cdb3ecfcbaab5192a601c515", - "sha256:df27407f4991f25bd669b5bb1bc8cb9ddf44a3e713ff6b3afeb3b3c26502f88f" + "sha256:7abbb3e5f4522114e0230ec175b60ae968b938d1f8a7d8bce7789f78d871fb9f" ], "index": "pypi", "version": "==2.8" @@ -434,6 +456,7 @@ "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "oauthlib": { @@ -441,6 +464,7 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "packaging": { @@ -496,15 +520,37 @@ }, "pyasn1": { "hashes": [ + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576" ], "version": "==0.4.8" }, "pyasn1-modules": { "hashes": [ + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", + "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", + "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", + "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" ], "version": "==0.2.8" }, @@ -513,6 +559,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pycryptodome": { @@ -584,6 +631,7 @@ "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46", "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.9.8" }, "pyjwkest": { @@ -605,6 +653,7 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pyrsistent": { @@ -618,6 +667,7 @@ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "pytz": { @@ -675,6 +725,7 @@ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.5.3" }, "requests": { @@ -682,12 +733,14 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requests-oauthlib": { "hashes": [ - "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc", + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d" ], "index": "pypi", "version": "==1.3.0" @@ -721,7 +774,7 @@ "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad", "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e" ], - "markers": "platform_python_implementation == 'CPython' and python_version < '3.9'", + "markers": "python_version < '3.9' and platform_python_implementation == 'CPython'", "version": "==0.2.0" }, "s3transfer": { @@ -760,6 +813,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sqlparse": { @@ -767,6 +821,7 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "structlog": { @@ -790,6 +845,7 @@ "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.1" }, "urllib3": { @@ -801,7 +857,6 @@ "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "index": "pypi", - "markers": null, "version": "==1.25.10" }, "vine": { @@ -809,6 +864,7 @@ "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.0" } }, @@ -825,6 +881,7 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], + "markers": "python_version >= '3.5'", "version": "==3.2.10" }, "astroid": { @@ -832,6 +889,7 @@ "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" ], + "markers": "python_version >= '3.5'", "version": "==2.4.1" }, "attrs": { @@ -839,6 +897,7 @@ "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.1.0" }, "autopep8": { @@ -869,6 +928,7 @@ "sha256:477f0e18a0d58e50bb3dbc9af7fcda464fd0ebfc7a6151d8888602d7153171a0", "sha256:cd4f3a231305e405ed8944d8ff35bd742d9bc740ad62f483bd0ca21ce7131984" ], + "markers": "python_version >= '3.5'", "version": "==1.0.0" }, "bumpversion": { @@ -898,6 +958,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -966,11 +1027,11 @@ }, "docker": { "hashes": [ - "sha256:431a268f2caf85aa30613f9642da274c62f6ee8bae7d70d968e01529f7d6af93", - "sha256:ba118607b0ba6bfc1b236ec32019a355c47b5d012d01d976467d4692ef443929" + "sha256:13966471e8bc23b36bfb3a6fb4ab75043a5ef1dac86516274777576bed3b9828", + "sha256:bad94b8dd001a8a4af19ce4becc17f41b09f228173ffe6a4e0355389eef142f2" ], "index": "pypi", - "version": "==4.3.0" + "version": "==4.3.1" }, "dodgy": { "hashes": [ @@ -984,6 +1045,7 @@ "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.8.3" }, "flake8-polyfill": { @@ -998,6 +1060,7 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], + "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { @@ -1005,6 +1068,7 @@ "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" ], + "markers": "python_version >= '3.4'", "version": "==3.1.7" }, "idna": { @@ -1019,6 +1083,7 @@ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.21" }, "lazy-object-proxy": { @@ -1045,6 +1110,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -1087,6 +1153,7 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -1094,6 +1161,7 @@ "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" ], + "markers": "python_version >= '3.5'", "version": "==5.0.2" }, "pyflakes": { @@ -1101,6 +1169,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pylint": { @@ -1193,6 +1262,7 @@ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.24.0" }, "requirements-detector": { @@ -1220,6 +1290,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -1227,6 +1298,7 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "snowballstemmer": { @@ -1241,6 +1313,7 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "stevedore": { @@ -1248,6 +1321,7 @@ "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5", "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633" ], + "markers": "python_version >= '3.6'", "version": "==3.2.0" }, "toml": { @@ -1300,7 +1374,6 @@ "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "index": "pypi", - "markers": null, "version": "==1.25.10" }, "websocket-client": { diff --git a/passbook/flows/models.py b/passbook/flows/models.py index a4da0fb53..1792710da 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -7,9 +7,11 @@ from django.forms import ModelForm from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from model_utils.managers import InheritanceManager +from rest_framework.serializers import BaseSerializer from structlog import get_logger from passbook.core.types import UIUserSettings +from passbook.lib.models import SerializerModel from passbook.policies.models import PolicyBindingModel if TYPE_CHECKING: @@ -38,7 +40,7 @@ class FlowDesignation(models.TextChoices): STAGE_SETUP = "stage_setup" -class Stage(models.Model): +class Stage(SerializerModel): """Stage is an instance of a component used in a flow. This can verify the user, enroll the user or offer a way of recovery""" @@ -81,7 +83,7 @@ def in_memory_stage(view: Type["StageView"]) -> Stage: return stage -class Flow(PolicyBindingModel): +class Flow(SerializerModel, PolicyBindingModel): """Flow describes how a series of Stages should be executed to authenticate/enroll/recover a user. Additionally, policies can be applied, to specify which users have access to this flow.""" @@ -95,6 +97,12 @@ class Flow(PolicyBindingModel): stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True) + @property + def serializer(self) -> BaseSerializer: + from passbook.flows.api import FlowSerializer + + return FlowSerializer + @staticmethod def with_policy(request: HttpRequest, **flow_filter) -> Optional["Flow"]: """Get a Flow by `**flow_filter` and check if the request from `request` can access it.""" @@ -128,7 +136,7 @@ class Flow(PolicyBindingModel): verbose_name_plural = _("Flows") -class FlowStageBinding(PolicyBindingModel): +class FlowStageBinding(SerializerModel, PolicyBindingModel): """Relationship between Flow and Stage. Order is required and unique for each flow-stage Binding. Additionally, policies can be specified, which determine if this Binding applies to the current user""" @@ -149,6 +157,12 @@ class FlowStageBinding(PolicyBindingModel): objects = InheritanceManager() + @property + def serializer(self) -> BaseSerializer: + from passbook.flows.api import FlowStageBindingSerializer + + return FlowStageBindingSerializer + def __str__(self) -> str: return f"Flow Binding {self.target} -> {self.stage}" diff --git a/passbook/flows/tests/test_transfer.py b/passbook/flows/tests/test_transfer.py new file mode 100644 index 000000000..6913fef0a --- /dev/null +++ b/passbook/flows/tests/test_transfer.py @@ -0,0 +1,119 @@ +"""Test flow transfer""" +from json import dumps + +from django.test import TransactionTestCase + +from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding +from passbook.flows.transfer.common import DataclassEncoder +from passbook.flows.transfer.exporter import FlowExporter +from passbook.flows.transfer.importer import FlowImporter +from passbook.policies.expression.models import ExpressionPolicy +from passbook.policies.models import PolicyBinding +from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage +from passbook.stages.user_login.models import UserLoginStage + + +class TestFlowTransfer(TransactionTestCase): + """Test flow transfer""" + + def test_bundle_invalid_format(self): + """Test bundle with invalid format""" + importer = FlowImporter('{"version": 3}') + self.assertFalse(importer.validate()) + importer = FlowImporter( + '{"version": 1,"entries":[{"identifier":"","attrs":{},"model": "passbook_core.User"}]}' + ) + self.assertFalse(importer.validate()) + + def test_export_validate_import(self): + """Test export and validate it""" + login_stage = UserLoginStage.objects.create(name="default-authentication-login") + + flow = Flow.objects.create( + slug="test", + designation=FlowDesignation.AUTHENTICATION, + name="Welcome to passbook!", + ) + FlowStageBinding.objects.update_or_create( + target=flow, stage=login_stage, order=0, + ) + + exporter = FlowExporter(flow) + export = exporter.export() + self.assertEqual(len(export.entries), 3) + export_json = dumps(export, cls=DataclassEncoder) + importer = FlowImporter(export_json) + self.assertTrue(importer.validate()) + flow.delete() + login_stage.delete() + self.assertTrue(importer.apply()) + + self.assertTrue(Flow.objects.filter(slug="test").exists()) + + def test_export_validate_import_policies(self): + """Test export and validate it""" + flow_policy = ExpressionPolicy.objects.create( + name="default-source-authentication-if-sso", expression="return True", + ) + flow = Flow.objects.create( + slug="default-source-authentication", + designation=FlowDesignation.AUTHENTICATION, + name="Welcome to passbook!", + ) + PolicyBinding.objects.create(policy=flow_policy, target=flow, order=0) + + user_login = UserLoginStage.objects.create( + name="default-source-authentication-login" + ) + FlowStageBinding.objects.create(target=flow, stage=user_login, order=0) + + exporter = FlowExporter(flow) + export = exporter.export() + export_json = dumps(export, cls=DataclassEncoder) + importer = FlowImporter(export_json) + self.assertTrue(importer.validate()) + self.assertTrue(importer.apply()) + + def test_export_validate_import_prompt(self): + """Test export and validate it""" + # First stage fields + username_prompt = Prompt.objects.create( + field_key="username", label="Username", order=0, type=FieldTypes.TEXT + ) + password = Prompt.objects.create( + field_key="password", label="Password", order=1, type=FieldTypes.PASSWORD + ) + password_repeat = Prompt.objects.create( + field_key="password_repeat", + label="Password (repeat)", + order=2, + type=FieldTypes.PASSWORD, + ) + # Stages + first_stage = PromptStage.objects.create(name="prompt-stage-first") + first_stage.fields.set([username_prompt, password, password_repeat]) + first_stage.save() + + # Password checking policy + password_policy = ExpressionPolicy.objects.create( + name="policy-enrollment-password-equals", + expression="return request.context['password'] == request.context['password_repeat']", + ) + PolicyBinding.objects.create( + target=first_stage, policy=password_policy, order=0 + ) + + flow = Flow.objects.create( + name="default-enrollment-flow", + slug="default-enrollment-flow", + designation=FlowDesignation.ENROLLMENT, + ) + + FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0) + + exporter = FlowExporter(flow) + export = exporter.export() + export_json = dumps(export, cls=DataclassEncoder) + importer = FlowImporter(export_json) + self.assertTrue(importer.validate()) + self.assertTrue(importer.apply()) diff --git a/passbook/flows/transfer/__init__.py b/passbook/flows/transfer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/flows/transfer/common.py b/passbook/flows/transfer/common.py new file mode 100644 index 000000000..01b9ae57c --- /dev/null +++ b/passbook/flows/transfer/common.py @@ -0,0 +1,59 @@ +"""transfer common classes""" +from dataclasses import asdict, dataclass, field, is_dataclass +from json.encoder import JSONEncoder +from typing import Any, Dict, List +from uuid import UUID + +from passbook.lib.models import SerializerModel +from passbook.lib.sentry import SentryIgnoredException + + +def get_attrs(obj: SerializerModel) -> Dict[str, Any]: + """Get object's attributes via their serializer, and covert it to a normal dict""" + data = dict(obj.serializer(obj).data) + if "policies" in data: + data.pop("policies") + if "stages" in data: + data.pop("stages") + return data + + +@dataclass +class FlowBundleEntry: + """Single entry of a bundle""" + + identifier: str + model: str + attrs: Dict[str, Any] + + @staticmethod + def from_model(model: SerializerModel) -> "FlowBundleEntry": + """Convert a SerializerModel instance to a Bundle Entry""" + return FlowBundleEntry( + identifier=model.pk, + model=f"{model._meta.app_label}.{model._meta.model_name}", + attrs=get_attrs(model), + ) + + +@dataclass +class FlowBundle: + """Dataclass used for a full export""" + + version: int = field(default=1) + entries: List[FlowBundleEntry] = field(default_factory=list) + + +class DataclassEncoder(JSONEncoder): + """Convert FlowBundleEntry to json""" + + def default(self, o): + if is_dataclass(o): + return asdict(o) + if isinstance(o, UUID): + return str(o) + return super().default(o) + + +class EntryInvalidError(SentryIgnoredException): + """Error raised when an entry is invalid""" diff --git a/passbook/flows/transfer/exporter.py b/passbook/flows/transfer/exporter.py new file mode 100644 index 000000000..9ccd23e1b --- /dev/null +++ b/passbook/flows/transfer/exporter.py @@ -0,0 +1,78 @@ +"""Flow exporter""" +from json import dumps +from typing import Iterator + +from passbook.flows.models import Flow, FlowStageBinding, Stage +from passbook.flows.transfer.common import DataclassEncoder, FlowBundle, FlowBundleEntry +from passbook.policies.models import Policy, PolicyBinding, PolicyBindingModel +from passbook.stages.prompt.models import PromptStage + + +class FlowExporter: + """Export flow with attached stages into json""" + + flow: Flow + with_policies: bool + with_stage_prompts: bool + + def __init__(self, flow: Flow): + self.flow = flow + self.with_policies = True + self.with_stage_prompts = True + + def walk_stages(self) -> Iterator[FlowBundleEntry]: + """Convert all stages attached to self.flow into FlowBundleEntry objects""" + stages = ( + Stage.objects.filter(flow=self.flow).select_related().select_subclasses() + ) + for stage in stages: + if isinstance(stage, PromptStage): + pass + yield FlowBundleEntry.from_model(stage) + + def walk_stage_bindings(self) -> Iterator[FlowBundleEntry]: + """Convert all bindings attached to self.flow into FlowBundleEntry objects""" + bindings = FlowStageBinding.objects.filter(target=self.flow).select_related() + for binding in bindings: + yield FlowBundleEntry.from_model(binding) + + def walk_policies(self) -> Iterator[FlowBundleEntry]: + """Walk over all policies and their respective bindings""" + pbm_uuids = [self.flow.pbm_uuid] + for stage_subclass in Stage.__subclasses__(): + if issubclass(stage_subclass, PolicyBindingModel): + pbm_uuids += stage_subclass.objects.filter(flow=self.flow).values_list( + "pbm_uuid", flat=True + ) + pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list( + "pbm_uuid", flat=True + ) + policies = Policy.objects.filter(bindings__in=pbm_uuids).select_related() + for policy in policies: + yield FlowBundleEntry.from_model(policy) + bindings = PolicyBinding.objects.filter(target__in=pbm_uuids).select_related() + for binding in bindings: + yield FlowBundleEntry.from_model(binding) + + def walk_stage_prompts(self) -> Iterator[FlowBundleEntry]: + """Walk over all prompts associated with any PromptStages""" + prompt_stages = PromptStage.objects.filter(flow=self.flow) + for stage in prompt_stages: + for prompt in stage.fields.all(): + yield FlowBundleEntry.from_model(prompt) + + def export(self) -> FlowBundle: + """Create a list of all objects including the flow""" + bundle = FlowBundle() + bundle.entries.append(FlowBundleEntry.from_model(self.flow)) + if self.with_stage_prompts: + bundle.entries.extend(self.walk_stage_prompts()) + bundle.entries.extend(self.walk_stages()) + bundle.entries.extend(self.walk_stage_bindings()) + if self.with_policies: + bundle.entries.extend(self.walk_policies()) + return bundle + + def export_to_string(self) -> str: + """Call export and convert it to json""" + return dumps(self.export(), cls=DataclassEncoder) diff --git a/passbook/flows/transfer/importer.py b/passbook/flows/transfer/importer.py new file mode 100644 index 000000000..e8d4f74e0 --- /dev/null +++ b/passbook/flows/transfer/importer.py @@ -0,0 +1,134 @@ +"""Flow importer""" +from json import loads +from typing import Type + +from dacite import from_dict +from dacite.exceptions import DaciteError +from django.apps import apps +from django.db import transaction +from django.db.models import Model +from rest_framework.serializers import BaseSerializer, Serializer +from structlog import BoundLogger, get_logger + +from passbook.flows.models import Flow, FlowStageBinding, Stage +from passbook.flows.transfer.common import ( + EntryInvalidError, + FlowBundle, + FlowBundleEntry, +) +from passbook.lib.models import SerializerModel +from passbook.policies.models import Policy, PolicyBinding, PolicyBindingModel +from passbook.stages.prompt.models import Prompt + +ALLOWED_MODELS = (Flow, FlowStageBinding, Stage, Policy, PolicyBinding, Prompt) + + +class FlowImporter: + """Import Flow from json""" + + __import: FlowBundle + + logger: BoundLogger + + def __init__(self, json_input: str): + self.logger = get_logger() + import_dict = loads(json_input) + try: + self.__import = from_dict(FlowBundle, import_dict) + except DaciteError as exc: + raise EntryInvalidError from exc + + def validate(self) -> bool: + """Validate loaded flow export, ensure all models are allowed + and serializers have no errors""" + if self.__import.version != 1: + self.logger.warning("Invalid bundle version") + return False + for entry in self.__import.entries: + try: + self._validate_single(entry) + except EntryInvalidError as exc: + self.logger.warning(exc) + return False + return True + + def __get_pk_filed(self, model_class: Type[Model]) -> str: + fields = model_class._meta.get_fields() + pks = [] + for field in fields: + # Ignore base PK from pbm as that isn't the same pk we exported + if field.model in [PolicyBindingModel]: + continue + # Ignore primary keys with _ptr suffix as those are surrogate and not what we exported + if field.name.endswith("_ptr"): + continue + if hasattr(field, "primary_key"): + if field.primary_key: + pks.append(field.name) + if len(pks) > 1: + self.logger.debug( + "Found more than one fields with primary_key=True, using pk", pks=pks + ) + return "pk" + return pks[0] + + def _validate_single(self, entry: FlowBundleEntry) -> BaseSerializer: + """Validate a single entry""" + model_app_label, model_name = entry.model.split(".") + model: SerializerModel = apps.get_model(model_app_label, model_name) + if not isinstance(model(), ALLOWED_MODELS): + raise EntryInvalidError(f"Model {model} not allowed") + + # If we try to validate without referencing a possible instance + # we'll get a duplicate error, hence we load the model here and return + # the full serializer for later usage + existing_models = model.objects.filter(pk=entry.identifier) + serializer_kwargs = {"data": entry.attrs} + if existing_models.exists(): + self.logger.debug( + "initialise serializer with instance", instance=existing_models.first() + ) + serializer_kwargs["instance"] = existing_models.first() + else: + self.logger.debug("initialise new instance", pk=entry.identifier) + + serializer: Serializer = model().serializer(**serializer_kwargs) + is_valid = serializer.is_valid() + if not is_valid: + raise EntryInvalidError(f"Serializer errors {serializer.errors}") + if not existing_models.exists(): + # only insert the PK if we're creating a new model, otherwise we get + # an integrity error + model_pk = self.__get_pk_filed(model) + serializer.validated_data[model_pk] = entry.identifier + return serializer + + def apply(self) -> bool: + """Apply (create/update) flow json, in database transaction""" + transaction.set_autocommit(False) + successful = self._apply_models() + if not successful: + self.logger.debug("Reverting changes due to error") + transaction.rollback() + transaction.set_autocommit(True) + return False + self.logger.debug("Committing changes") + transaction.commit() + transaction.set_autocommit(True) + return True + + def _apply_models(self) -> bool: + """Apply (create/update) flow json""" + for entry in self.__import.entries: + model_app_label, model_name = entry.model.split(".") + model: SerializerModel = apps.get_model(model_app_label, model_name) + # Validate each single entry + try: + serializer = self._validate_single(entry) + except EntryInvalidError as exc: + self.logger.error("entry not valid", entry=entry, error=exc) + return False + + model = serializer.save() + self.logger.debug("updated model", model=model, pk=model.pk) + return True diff --git a/passbook/lib/models.py b/passbook/lib/models.py index 0966dd8ac..80fbcd3c3 100644 --- a/passbook/lib/models.py +++ b/passbook/lib/models.py @@ -1,6 +1,19 @@ """Generic models""" from django.db import models from model_utils.managers import InheritanceManager +from rest_framework.serializers import BaseSerializer + + +class SerializerModel(models.Model): + """Base Abstract Model which has a serializer""" + + @property + def serializer(self) -> BaseSerializer: + """Get serializer for this model""" + raise NotImplementedError + + class Meta: + abstract = True class CreatedUpdatedModel(models.Model): diff --git a/passbook/policies/dummy/models.py b/passbook/policies/dummy/models.py index 497f7a0fb..75dcc109a 100644 --- a/passbook/policies/dummy/models.py +++ b/passbook/policies/dummy/models.py @@ -6,6 +6,7 @@ from typing import Type from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import BaseSerializer from structlog import get_logger from passbook.policies.models import Policy @@ -24,6 +25,12 @@ class DummyPolicy(Policy): wait_min = models.IntegerField(default=5) wait_max = models.IntegerField(default=30) + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.dummy.api import DummyPolicySerializer + + return DummyPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.dummy.forms import DummyPolicyForm diff --git a/passbook/policies/expiry/models.py b/passbook/policies/expiry/models.py index f476c4dc0..96fed48aa 100644 --- a/passbook/policies/expiry/models.py +++ b/passbook/policies/expiry/models.py @@ -6,6 +6,7 @@ from django.db import models from django.forms import ModelForm from django.utils.timezone import now from django.utils.translation import gettext as _ +from rest_framework.serializers import BaseSerializer from structlog import get_logger from passbook.policies.models import Policy @@ -21,6 +22,12 @@ class PasswordExpiryPolicy(Policy): deny_only = models.BooleanField(default=False) days = models.IntegerField() + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.expiry.api import PasswordExpiryPolicySerializer + + return PasswordExpiryPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.expiry.forms import PasswordExpiryPolicyForm diff --git a/passbook/policies/expression/models.py b/passbook/policies/expression/models.py index 31c3c398e..e88c6d5af 100644 --- a/passbook/policies/expression/models.py +++ b/passbook/policies/expression/models.py @@ -4,6 +4,7 @@ from typing import Type from django.db import models from django.forms import ModelForm from django.utils.translation import gettext as _ +from rest_framework.serializers import BaseSerializer from passbook.policies.expression.evaluator import PolicyEvaluator from passbook.policies.models import Policy @@ -15,6 +16,12 @@ class ExpressionPolicy(Policy): expression = models.TextField() + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.expression.api import ExpressionPolicySerializer + + return ExpressionPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.expression.forms import ExpressionPolicyForm diff --git a/passbook/policies/group_membership/models.py b/passbook/policies/group_membership/models.py index 7f87a9a00..3c8dbdafd 100644 --- a/passbook/policies/group_membership/models.py +++ b/passbook/policies/group_membership/models.py @@ -4,6 +4,7 @@ from typing import Type from django.db import models from django.forms import ModelForm from django.utils.translation import gettext as _ +from rest_framework.serializers import BaseSerializer from passbook.core.models import Group from passbook.policies.models import Policy @@ -15,6 +16,14 @@ class GroupMembershipPolicy(Policy): group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.group_membership.api import ( + GroupMembershipPolicySerializer, + ) + + return GroupMembershipPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.group_membership.forms import GroupMembershipPolicyForm diff --git a/passbook/policies/hibp/models.py b/passbook/policies/hibp/models.py index f9a27fd19..51f759c6e 100644 --- a/passbook/policies/hibp/models.py +++ b/passbook/policies/hibp/models.py @@ -6,6 +6,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext as _ from requests import get +from rest_framework.serializers import BaseSerializer from structlog import get_logger from passbook.policies.models import Policy, PolicyResult @@ -27,6 +28,12 @@ class HaveIBeenPwendPolicy(Policy): allowed_count = models.IntegerField(default=0) + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.hibp.api import HaveIBeenPwendPolicySerializer + + return HaveIBeenPwendPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm diff --git a/passbook/policies/models.py b/passbook/policies/models.py index 06c096ec7..6933af07b 100644 --- a/passbook/policies/models.py +++ b/passbook/policies/models.py @@ -6,11 +6,13 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from model_utils.managers import InheritanceManager +from rest_framework.serializers import BaseSerializer from passbook.lib.models import ( CreatedUpdatedModel, InheritanceAutoManager, InheritanceForeignKey, + SerializerModel, ) from passbook.policies.exceptions import PolicyException from passbook.policies.types import PolicyRequest, PolicyResult @@ -32,7 +34,7 @@ class PolicyBindingModel(models.Model): verbose_name_plural = _("Policy Binding Models") -class PolicyBinding(models.Model): +class PolicyBinding(SerializerModel): """Relationship between a Policy and a PolicyBindingModel.""" policy_binding_uuid = models.UUIDField( @@ -55,6 +57,12 @@ class PolicyBinding(models.Model): order = models.IntegerField() + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.api import PolicyBindingSerializer + + return PolicyBindingSerializer + def __str__(self) -> str: return f"PolicyBinding policy={self.policy} target={self.target} order={self.order}" @@ -65,7 +73,7 @@ class PolicyBinding(models.Model): unique_together = ("policy", "target", "order") -class Policy(CreatedUpdatedModel): +class Policy(SerializerModel, CreatedUpdatedModel): """Policies which specify if a user is authorized to use an Application. Can be overridden by other types to add other fields, more logic, etc.""" diff --git a/passbook/policies/password/models.py b/passbook/policies/password/models.py index bdd40bd92..2c0981434 100644 --- a/passbook/policies/password/models.py +++ b/passbook/policies/password/models.py @@ -5,6 +5,7 @@ from typing import Type from django.db import models from django.forms import ModelForm from django.utils.translation import gettext as _ +from rest_framework.serializers import BaseSerializer from structlog import get_logger from passbook.policies.models import Policy @@ -30,6 +31,12 @@ class PasswordPolicy(Policy): symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") error_message = models.TextField() + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.password.api import PasswordPolicySerializer + + return PasswordPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.password.forms import PasswordPolicyForm diff --git a/passbook/policies/reputation/models.py b/passbook/policies/reputation/models.py index 9ed0366a4..a703fffd1 100644 --- a/passbook/policies/reputation/models.py +++ b/passbook/policies/reputation/models.py @@ -5,6 +5,7 @@ from django.core.cache import cache from django.db import models from django.forms import ModelForm from django.utils.translation import gettext as _ +from rest_framework.serializers import BaseSerializer from passbook.core.models import User from passbook.lib.utils.http import get_client_ip @@ -22,6 +23,12 @@ class ReputationPolicy(Policy): check_username = models.BooleanField(default=True) threshold = models.IntegerField(default=-5) + @property + def serializer(self) -> BaseSerializer: + from passbook.policies.reputation.api import ReputationPolicySerializer + + return ReputationPolicySerializer + def form(self) -> Type[ModelForm]: from passbook.policies.reputation.forms import ReputationPolicyForm diff --git a/passbook/stages/captcha/models.py b/passbook/stages/captcha/models.py index 584a5ca41..58e23ae64 100644 --- a/passbook/stages/captcha/models.py +++ b/passbook/stages/captcha/models.py @@ -5,6 +5,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -23,6 +24,12 @@ class CaptchaStage(Stage): ) ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.captcha.api import CaptchaStageSerializer + + return CaptchaStageSerializer + def type(self) -> Type[View]: from passbook.stages.captcha.stage import CaptchaStageView diff --git a/passbook/stages/consent/models.py b/passbook/stages/consent/models.py index b0b17e2a0..21a3e6780 100644 --- a/passbook/stages/consent/models.py +++ b/passbook/stages/consent/models.py @@ -5,6 +5,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.core.models import Application, ExpiringModel, User from passbook.flows.models import Stage @@ -37,6 +38,12 @@ class ConsentStage(Stage): ), ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.consent.api import ConsentStageSerializer + + return ConsentStageSerializer + def type(self) -> Type[View]: from passbook.stages.consent.stage import ConsentStageView diff --git a/passbook/stages/dummy/models.py b/passbook/stages/dummy/models.py index 14d77ba6a..839fce0d1 100644 --- a/passbook/stages/dummy/models.py +++ b/passbook/stages/dummy/models.py @@ -4,6 +4,7 @@ from typing import Type from django.forms import ModelForm from django.utils.translation import gettext as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -13,6 +14,12 @@ class DummyStage(Stage): __debug_only__ = True + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.dummy.api import DummyStageSerializer + + return DummyStageSerializer + def type(self) -> Type[View]: from passbook.stages.dummy.stage import DummyStageView diff --git a/passbook/stages/email/models.py b/passbook/stages/email/models.py index 3c575f692..624aaba61 100644 --- a/passbook/stages/email/models.py +++ b/passbook/stages/email/models.py @@ -7,6 +7,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -44,6 +45,12 @@ class EmailStage(Stage): choices=EmailTemplates.choices, default=EmailTemplates.PASSWORD_RESET ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.email.api import EmailStageSerializer + + return EmailStageSerializer + def type(self) -> Type[View]: from passbook.stages.email.stage import EmailStageView diff --git a/passbook/stages/identification/models.py b/passbook/stages/identification/models.py index 86963d931..4e7e3719c 100644 --- a/passbook/stages/identification/models.py +++ b/passbook/stages/identification/models.py @@ -6,6 +6,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Flow, Stage @@ -56,6 +57,12 @@ class IdentificationStage(Stage): ), ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.identification.api import IdentificationStageSerializer + + return IdentificationStageSerializer + def type(self) -> Type[View]: from passbook.stages.identification.stage import IdentificationStageView diff --git a/passbook/stages/invitation/models.py b/passbook/stages/invitation/models.py index b4ecbfc02..c892b526b 100644 --- a/passbook/stages/invitation/models.py +++ b/passbook/stages/invitation/models.py @@ -6,6 +6,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.core.models import User from passbook.flows.models import Stage @@ -26,6 +27,12 @@ class InvitationStage(Stage): ), ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.invitation.api import InvitationStageSerializer + + return InvitationStageSerializer + def type(self) -> Type[View]: from passbook.stages.invitation.stage import InvitationStageView diff --git a/passbook/stages/otp_static/models.py b/passbook/stages/otp_static/models.py index 7005fed8c..2b65324b0 100644 --- a/passbook/stages/otp_static/models.py +++ b/passbook/stages/otp_static/models.py @@ -6,6 +6,7 @@ from django.forms import ModelForm from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.core.types import UIUserSettings from passbook.flows.models import Stage @@ -16,6 +17,12 @@ class OTPStaticStage(Stage): token_count = models.IntegerField(default=6) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.otp_static.api import OTPStaticStageSerializer + + return OTPStaticStageSerializer + def type(self) -> Type[View]: from passbook.stages.otp_static.stage import OTPStaticStageView diff --git a/passbook/stages/otp_time/models.py b/passbook/stages/otp_time/models.py index 7b9460cc5..130c3d79e 100644 --- a/passbook/stages/otp_time/models.py +++ b/passbook/stages/otp_time/models.py @@ -6,6 +6,7 @@ from django.forms import ModelForm from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.core.types import UIUserSettings from passbook.flows.models import Stage @@ -23,6 +24,12 @@ class OTPTimeStage(Stage): digits = models.IntegerField(choices=TOTPDigits.choices) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.otp_time.api import OTPTimeStageSerializer + + return OTPTimeStageSerializer + def type(self) -> Type[View]: from passbook.stages.otp_time.stage import OTPTimeStageView diff --git a/passbook/stages/otp_validate/models.py b/passbook/stages/otp_validate/models.py index f89735025..e8d995234 100644 --- a/passbook/stages/otp_validate/models.py +++ b/passbook/stages/otp_validate/models.py @@ -5,6 +5,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import NotConfiguredAction, Stage @@ -16,6 +17,12 @@ class OTPValidateStage(Stage): choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.otp_validate.api import OTPValidateStageSerializer + + return OTPValidateStageSerializer + def type(self) -> Type[View]: from passbook.stages.otp_validate.stage import OTPValidateStageView diff --git a/passbook/stages/password/models.py b/passbook/stages/password/models.py index c9d7fce3c..3d4b5acfe 100644 --- a/passbook/stages/password/models.py +++ b/passbook/stages/password/models.py @@ -8,6 +8,7 @@ from django.shortcuts import reverse from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.core.types import UIUserSettings from passbook.flows.models import Flow, Stage @@ -35,6 +36,12 @@ class PasswordStage(Stage): ), ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.password.api import PasswordStageSerializer + + return PasswordStageSerializer + def type(self) -> Type[View]: from passbook.stages.password.stage import PasswordStageView diff --git a/passbook/stages/prompt/models.py b/passbook/stages/prompt/models.py index d641f7405..f7cf1742f 100644 --- a/passbook/stages/prompt/models.py +++ b/passbook/stages/prompt/models.py @@ -7,8 +7,10 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage +from passbook.lib.models import SerializerModel from passbook.policies.models import PolicyBindingModel from passbook.stages.prompt.widgets import HorizontalRuleWidget, StaticTextWidget @@ -40,7 +42,7 @@ class FieldTypes(models.TextChoices): STATIC = "static", _("Static: Static value, displayed as-is.") -class Prompt(models.Model): +class Prompt(SerializerModel): """Single Prompt, part of a prompt stage.""" prompt_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) @@ -51,10 +53,16 @@ class Prompt(models.Model): label = models.TextField() type = models.CharField(max_length=100, choices=FieldTypes.choices) required = models.BooleanField(default=True) - placeholder = models.TextField() + placeholder = models.TextField(blank=True) order = models.IntegerField(default=0) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.prompt.api import PromptSerializer + + return PromptSerializer + @property def field(self): """Return instantiated form input field""" @@ -120,6 +128,12 @@ class PromptStage(PolicyBindingModel, Stage): fields = models.ManyToManyField(Prompt) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.prompt.api import PromptStageSerializer + + return PromptStageSerializer + def type(self) -> Type[View]: from passbook.stages.prompt.stage import PromptStageView diff --git a/passbook/stages/user_delete/models.py b/passbook/stages/user_delete/models.py index 667673c72..c5a896421 100644 --- a/passbook/stages/user_delete/models.py +++ b/passbook/stages/user_delete/models.py @@ -4,6 +4,7 @@ from typing import Type from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -12,6 +13,12 @@ class UserDeleteStage(Stage): """Deletes the currently pending user without confirmation. Use with caution.""" + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.user_delete.api import UserDeleteStageSerializer + + return UserDeleteStageSerializer + def type(self) -> Type[View]: from passbook.stages.user_delete.stage import UserDeleteStageView diff --git a/passbook/stages/user_login/models.py b/passbook/stages/user_login/models.py index 96c86661e..a8c5a4539 100644 --- a/passbook/stages/user_login/models.py +++ b/passbook/stages/user_login/models.py @@ -5,6 +5,7 @@ from django.db import models from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -20,6 +21,12 @@ class UserLoginStage(Stage): ), ) + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.user_login.api import UserLoginStageSerializer + + return UserLoginStageSerializer + def type(self) -> Type[View]: from passbook.stages.user_login.stage import UserLoginStageView diff --git a/passbook/stages/user_logout/models.py b/passbook/stages/user_logout/models.py index d85bf2252..bd9677511 100644 --- a/passbook/stages/user_logout/models.py +++ b/passbook/stages/user_logout/models.py @@ -4,6 +4,7 @@ from typing import Type from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -11,6 +12,12 @@ from passbook.flows.models import Stage class UserLogoutStage(Stage): """Resets the users current session.""" + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.user_logout.api import UserLogoutStageSerializer + + return UserLogoutStageSerializer + def type(self) -> Type[View]: from passbook.stages.user_logout.stage import UserLogoutStageView diff --git a/passbook/stages/user_write/models.py b/passbook/stages/user_write/models.py index bc7635b6d..c1ba6be7a 100644 --- a/passbook/stages/user_write/models.py +++ b/passbook/stages/user_write/models.py @@ -4,6 +4,7 @@ from typing import Type from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from django.views import View +from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage @@ -12,6 +13,12 @@ class UserWriteStage(Stage): """Writes currently pending data into the pending user, or if no user exists, creates a new user with the data.""" + @property + def serializer(self) -> BaseSerializer: + from passbook.stages.user_write.api import UserWriteStageSerializer + + return UserWriteStageSerializer + def type(self) -> Type[View]: from passbook.stages.user_write.stage import UserWriteStageView diff --git a/swagger.yaml b/swagger.yaml index 177735a26..92456170c 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -6859,7 +6859,6 @@ definitions: - field_key - label - type - - placeholder type: object properties: pk: @@ -6900,7 +6899,6 @@ definitions: placeholder: title: Placeholder type: string - minLength: 1 order: title: Order type: integer