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
This commit is contained in:
parent
8b17e8be99
commit
0e0898c3cf
1
Pipfile
1
Pipfile
|
@ -38,6 +38,7 @@ signxml = "*"
|
|||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
dacite = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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())
|
|
@ -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"""
|
|
@ -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)
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue