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:
		
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							| @ -38,6 +38,7 @@ signxml = "*" | ||||
| structlog = "*" | ||||
| swagger-spec-validator = "*" | ||||
| urllib3 = {extras = ["secure"],version = "*"} | ||||
| dacite = "*" | ||||
|  | ||||
| [requires] | ||||
| python_version = "3.8" | ||||
|  | ||||
							
								
								
									
										97
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										97
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -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}" | ||||
|  | ||||
|  | ||||
							
								
								
									
										119
									
								
								passbook/flows/tests/test_transfer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								passbook/flows/tests/test_transfer.py
									
									
									
									
									
										Normal file
									
								
							| @ -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
									
								
								passbook/flows/transfer/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/flows/transfer/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										59
									
								
								passbook/flows/transfer/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								passbook/flows/transfer/common.py
									
									
									
									
									
										Normal file
									
								
							| @ -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""" | ||||
							
								
								
									
										78
									
								
								passbook/flows/transfer/exporter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								passbook/flows/transfer/exporter.py
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||
							
								
								
									
										134
									
								
								passbook/flows/transfer/importer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								passbook/flows/transfer/importer.py
									
									
									
									
									
										Normal file
									
								
							| @ -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
	
	Block a user
	 Jens L
					Jens L