Merge branch 'master' into otp-rework
# Conflicts: # passbook/flows/models.py # passbook/stages/otp/models.py # swagger.yaml
This commit is contained in:
		
							
								
								
									
										30
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -46,18 +46,18 @@ | |||||||
|         }, |         }, | ||||||
|         "boto3": { |         "boto3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:8eeaa2d6374f02d6f0d6b8bee55838add9a4246de81b1402e59372296402f6c7", |                 "sha256:2616351c98eec18d20a1d64b33355c86cd855ac96219d1b8428c9bfc590bde53", | ||||||
|                 "sha256:d1d93ed75f477e8910b8b074ae76e3189d1c3a3998ea679ab52fdbacb8b4f390" |                 "sha256:7daad26a008c91dd7b82fde17d246d1fe6e4b3813426689ef8bac9017a277cfb" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.14.11" |             "version": "==1.14.12" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:64454a6dff9a3ced0dd75c0e69a8842aa663e0682d1dc5c8913fb76d354bfe3d", |                 "sha256:45934d880378777cefeca727f369d1f5aebf6b254e9be58e7c77dd0b059338bb", | ||||||
|                 "sha256:715b41f3215214e75bf7b8e88bdbc38dc055eef761b37dbd559bac1a5becb3c2" |                 "sha256:a94e0e2307f1b9fe3a84660842909cd2680b57a9fc9fb0c3a03b0afb2eadbe21" | ||||||
|             ], |             ], | ||||||
|             "version": "==1.17.11" |             "version": "==1.17.12" | ||||||
|         }, |         }, | ||||||
|         "celery": { |         "celery": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -322,10 +322,10 @@ | |||||||
|         }, |         }, | ||||||
|         "idna": { |         "idna": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", |                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||||
|                 "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" |                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||||
|             ], |             ], | ||||||
|             "version": "==2.9" |             "version": "==2.10" | ||||||
|         }, |         }, | ||||||
|         "inflection": { |         "inflection": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -604,10 +604,10 @@ | |||||||
|         }, |         }, | ||||||
|         "pyparsing": { |         "pyparsing": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", |                 "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", | ||||||
|                 "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" |                 "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.0.0a1" |             "version": "==3.0.0a2" | ||||||
|         }, |         }, | ||||||
|         "pyrsistent": { |         "pyrsistent": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1044,10 +1044,10 @@ | |||||||
|         }, |         }, | ||||||
|         "idna": { |         "idna": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", |                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||||
|                 "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" |                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||||
|             ], |             ], | ||||||
|             "version": "==2.9" |             "version": "==2.10" | ||||||
|         }, |         }, | ||||||
|         "isort": { |         "isort": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  | |||||||
| @ -286,6 +286,204 @@ | |||||||
|       ], |       ], | ||||||
|       "value": "foo@bar.baz" |       "value": "foo@bar.baz" | ||||||
|     }] |     }] | ||||||
|  |   }, { | ||||||
|  |     "id": "1a3172e0-ac23-4781-9367-19afccee4f4a", | ||||||
|  |     "name": "flows stage setup password", | ||||||
|  |     "commands": [{ | ||||||
|  |       "id": "77784f77-d840-4b3d-a42f-7928f02fb7e1", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "open", | ||||||
|  |       "target": "/flows/default-authentication-flow/?next=%2F", | ||||||
|  |       "targets": [], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "783aa9a6-81e5-49c6-8789-2f360a5750b1", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "setWindowSize", | ||||||
|  |       "target": "1699x1417", | ||||||
|  |       "targets": [], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "cb0cd63e-30e9-4443-af59-5345fe26dc88", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "id=id_uid_field", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_uid_field", "id"], | ||||||
|  |         ["name=uid_field", "name"], | ||||||
|  |         ["css=#id_uid_field", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_uid_field']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "8466ded1-c5f6-451c-b63f-0889da38503a", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "type", | ||||||
|  |       "target": "id=id_uid_field", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_uid_field", "id"], | ||||||
|  |         ["name=uid_field", "name"], | ||||||
|  |         ["css=#id_uid_field", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_uid_field']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "pbadmin" | ||||||
|  |     }, { | ||||||
|  |       "id": "27383093-d01a-4416-8fc6-9caad4926cd3", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "sendKeys", | ||||||
|  |       "target": "id=id_uid_field", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_uid_field", "id"], | ||||||
|  |         ["name=uid_field", "name"], | ||||||
|  |         ["css=#id_uid_field", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_uid_field']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "${KEY_ENTER}" | ||||||
|  |     }, { | ||||||
|  |       "id": "4602745a-0ebb-4425-a841-a1ed4899659d", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "type", | ||||||
|  |       "target": "id=id_password", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_password", "id"], | ||||||
|  |         ["name=password", "name"], | ||||||
|  |         ["css=#id_password", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_password']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div[2]/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "pbadmin" | ||||||
|  |     }, { | ||||||
|  |       "id": "d1ff4f81-d8f9-45dc-ad5d-f99b54c0cd18", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "sendKeys", | ||||||
|  |       "target": "id=id_password", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_password", "id"], | ||||||
|  |         ["name=password", "name"], | ||||||
|  |         ["css=#id_password", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_password']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div[2]/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "${KEY_ENTER}" | ||||||
|  |     }, { | ||||||
|  |       "id": "014c8f57-7ef2-469c-b700-efa94ba81b66", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "css=.pf-c-page__header", | ||||||
|  |       "targets": [ | ||||||
|  |         ["css=.pf-c-page__header", "css:finder"], | ||||||
|  |         ["xpath=//div[@id='page-default-nav-example']/header", "xpath:idRelative"], | ||||||
|  |         ["xpath=//header", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "14e86b6f-6add-4bcc-913a-42b1e7322c79", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "linkText=pbadmin", | ||||||
|  |       "targets": [ | ||||||
|  |         ["linkText=pbadmin", "linkText"], | ||||||
|  |         ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"], | ||||||
|  |         ["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"], | ||||||
|  |         ["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"], | ||||||
|  |         ["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"], | ||||||
|  |         ["xpath=//div[2]/a", "xpath:position"], | ||||||
|  |         ["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "8280da13-632e-4cba-9e18-ecae0d57d052", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "linkText=Change password", | ||||||
|  |       "targets": [ | ||||||
|  |         ["linkText=Change password", "linkText"], | ||||||
|  |         ["css=.pf-c-nav__section:nth-child(2) .pf-c-nav__link", "css:finder"], | ||||||
|  |         ["xpath=//a[contains(text(),'Change password')]", "xpath:link"], | ||||||
|  |         ["xpath=//nav[@id='page-default-nav-example-primary-nav']/section[2]/ul/li/a", "xpath:idRelative"], | ||||||
|  |         ["xpath=//a[contains(@href, '/-/user/stage/password/b929b529-e384-4409-8d40-ac4a195fcab2/change/?next=%2F-%2Fuser%2F')]", "xpath:href"], | ||||||
|  |         ["xpath=//section[2]/ul/li/a", "xpath:position"], | ||||||
|  |         ["xpath=//a[contains(.,'Change password')]", "xpath:innerText"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "716d7e0c-79dc-469b-a31f-dceaa0765e9c", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "id=id_password", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_password", "id"], | ||||||
|  |         ["name=password", "name"], | ||||||
|  |         ["css=#id_password", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_password']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "77005d70-adf0-4add-8329-b092d43f829a", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "type", | ||||||
|  |       "target": "id=id_password", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_password", "id"], | ||||||
|  |         ["name=password", "name"], | ||||||
|  |         ["css=#id_password", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_password']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "test" | ||||||
|  |     }, { | ||||||
|  |       "id": "965ca365-99f4-45d1-97c3-c944269341b9", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "id=id_password_repeat", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_password_repeat", "id"], | ||||||
|  |         ["name=password_repeat", "name"], | ||||||
|  |         ["css=#id_password_repeat", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_password_repeat']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div[2]/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }, { | ||||||
|  |       "id": "9b421468-c65e-4943-b6b1-1e80410a6b87", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "type", | ||||||
|  |       "target": "id=id_password_repeat", | ||||||
|  |       "targets": [ | ||||||
|  |         ["id=id_password_repeat", "id"], | ||||||
|  |         ["name=password_repeat", "name"], | ||||||
|  |         ["css=#id_password_repeat", "css:finder"], | ||||||
|  |         ["xpath=//input[@id='id_password_repeat']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"], | ||||||
|  |         ["xpath=//div[2]/input", "xpath:position"] | ||||||
|  |       ], | ||||||
|  |       "value": "test" | ||||||
|  |     }, { | ||||||
|  |       "id": "572c1400-a0f2-499f-808a-18c1f56bf13f", | ||||||
|  |       "comment": "", | ||||||
|  |       "command": "click", | ||||||
|  |       "target": "css=.pf-c-button", | ||||||
|  |       "targets": [ | ||||||
|  |         ["css=.pf-c-button", "css:finder"], | ||||||
|  |         ["xpath=//button[@type='submit']", "xpath:attributes"], | ||||||
|  |         ["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"], | ||||||
|  |         ["xpath=//button", "xpath:position"], | ||||||
|  |         ["xpath=//button[contains(.,'Continue')]", "xpath:innerText"] | ||||||
|  |       ], | ||||||
|  |       "value": "" | ||||||
|  |     }] | ||||||
|   }], |   }], | ||||||
|   "suites": [{ |   "suites": [{ | ||||||
|     "id": "495657fb-3f5e-4431-877c-4d0b248c0841", |     "id": "495657fb-3f5e-4431-877c-4d0b248c0841", | ||||||
|  | |||||||
| @ -1,477 +0,0 @@ | |||||||
| """Test Enroll flow""" |  | ||||||
| from time import sleep |  | ||||||
|  |  | ||||||
| from django.test import override_settings |  | ||||||
| from selenium.webdriver.common.by import By |  | ||||||
| from selenium.webdriver.common.keys import Keys |  | ||||||
| from selenium.webdriver.support import expected_conditions as ec |  | ||||||
|  |  | ||||||
| from docker import DockerClient, from_env |  | ||||||
| from docker.models.containers import Container |  | ||||||
| from docker.types import Healthcheck |  | ||||||
| from e2e.utils import USER, SeleniumTestCase |  | ||||||
| from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding |  | ||||||
| from passbook.policies.expression.models import ExpressionPolicy |  | ||||||
| from passbook.policies.models import PolicyBinding |  | ||||||
| from passbook.stages.email.models import EmailStage, EmailTemplates |  | ||||||
| from passbook.stages.identification.models import IdentificationStage |  | ||||||
| from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage |  | ||||||
| from passbook.stages.user_login.models import UserLoginStage |  | ||||||
| from passbook.stages.user_write.models import UserWriteStage |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestEnroll(SeleniumTestCase): |  | ||||||
|     """Test Enroll flow""" |  | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         super().setUp() |  | ||||||
|         self.container = self.setup_client() |  | ||||||
|  |  | ||||||
|     def setup_client(self) -> Container: |  | ||||||
|         """Setup test IdP container""" |  | ||||||
|         client: DockerClient = from_env() |  | ||||||
|         container = client.containers.run( |  | ||||||
|             image="mailhog/mailhog", |  | ||||||
|             detach=True, |  | ||||||
|             network_mode="host", |  | ||||||
|             auto_remove=True, |  | ||||||
|             healthcheck=Healthcheck( |  | ||||||
|                 test=["CMD", "wget", "-s", "http://localhost:8025"], |  | ||||||
|                 interval=5 * 100 * 1000000, |  | ||||||
|                 start_period=1 * 100 * 1000000, |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|         while True: |  | ||||||
|             container.reload() |  | ||||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") |  | ||||||
|             if status == "healthy": |  | ||||||
|                 return container |  | ||||||
|             sleep(1) |  | ||||||
|  |  | ||||||
|     def tearDown(self): |  | ||||||
|         self.container.kill() |  | ||||||
|         super().tearDown() |  | ||||||
|  |  | ||||||
|     # pylint: disable=too-many-statements |  | ||||||
|     def setup_test_enroll_2_step(self): |  | ||||||
|         """Setup all required objects""" |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |  | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Administrate").click() |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Prompts").click() |  | ||||||
|  |  | ||||||
|         # Create Password Prompt |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_field_key").send_keys("password") |  | ||||||
|         self.driver.find_element(By.ID, "id_label").send_keys("Password") |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_type") |  | ||||||
|         dropdown.find_element(By.XPATH, "//option[. = 'Password']").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_placeholder").send_keys("Password") |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("1") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Password Repeat Prompt |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_field_key").send_keys("password_repeat") |  | ||||||
|         self.driver.find_element(By.ID, "id_label").send_keys("Password (repeat)") |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_type") |  | ||||||
|         dropdown.find_element(By.XPATH, "//option[. = 'Password']").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_placeholder").send_keys("Password (repeat)") |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("2") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Name Prompt |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_field_key").send_keys("name") |  | ||||||
|         self.driver.find_element(By.ID, "id_label").send_keys("Name") |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_type") |  | ||||||
|         dropdown.find_element(By.XPATH, "//option[. = 'Text']").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_placeholder").send_keys("Name") |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("0") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Email Prompt |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_field_key").send_keys("email") |  | ||||||
|         self.driver.find_element(By.ID, "id_label").send_keys("Email") |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_type") |  | ||||||
|         dropdown.find_element(By.XPATH, "//option[. = 'Email']").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_placeholder").send_keys("Email") |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("1") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Stages").click() |  | ||||||
|  |  | ||||||
|         # Create first enroll prompt stage |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, "li:nth-child(9) > .pf-c-dropdown__menu-item > small" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys( |  | ||||||
|             "enroll-prompt-stage-first" |  | ||||||
|         ) |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_fields") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, "//option[. = \"Prompt 'username' type=text\"]" |  | ||||||
|         ).click() |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, "//option[. = \"Prompt 'password' type=password\"]" |  | ||||||
|         ).click() |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, "//option[. = \"Prompt 'password_repeat' type=password\"]" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create second enroll prompt stage |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, "li:nth-child(9) > .pf-c-dropdown__menu-item" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys( |  | ||||||
|             "enroll-prompt-stage-second" |  | ||||||
|         ) |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_fields") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, "//option[. = \"Prompt 'name' type=text\"]" |  | ||||||
|         ).click() |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, "//option[. = \"Prompt 'email' type=email\"]" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create user write stage |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, "li:nth-child(13) > .pf-c-dropdown__menu-item" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys("enroll-user-write") |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys(Keys.ENTER) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click() |  | ||||||
|  |  | ||||||
|         # Create user login stage |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, "li:nth-child(11) > .pf-c-dropdown__menu-item" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys("enroll-user-login") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, |  | ||||||
|             ".pf-c-nav__item:nth-child(7) .pf-c-nav__item:nth-child(1) > .pf-c-nav__link", |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create password policy |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-dropdown__toggle").click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, "li:nth-child(2) > .pf-c-dropdown__menu-item > small" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys( |  | ||||||
|             "policy-enrollment-password-equals" |  | ||||||
|         ) |  | ||||||
|         self.wait.until( |  | ||||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, ".CodeMirror-scroll")) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".CodeMirror-scroll").click() |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".CodeMirror textarea").send_keys( |  | ||||||
|             "return request.context['password'] == request.context['password_repeat']" |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create password policy binding |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, |  | ||||||
|             ".pf-c-nav__item:nth-child(7) .pf-c-nav__item:nth-child(2) > .pf-c-nav__link", |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_policy") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Policy policy-enrollment-password-equals"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_target").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_target") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Prompt Stage enroll-prompt-stage-first"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("0") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Flow |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, |  | ||||||
|             ".pf-c-nav__item:nth-child(6) .pf-c-nav__item:nth-child(1) > .pf-c-nav__link", |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys("Welcome") |  | ||||||
|         self.driver.find_element(By.ID, "id_slug").clear() |  | ||||||
|         self.driver.find_element(By.ID, "id_slug").send_keys("default-enrollment-flow") |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_designation") |  | ||||||
|         dropdown.find_element(By.XPATH, '//option[. = "Enrollment"]').click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Stages").click() |  | ||||||
|  |  | ||||||
|         # Edit identification stage |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, "tr:nth-child(11) .pf-m-secondary" |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, |  | ||||||
|             ".pf-c-form__group:nth-child(5) .pf-c-form__horizontal-group", |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_enrollment_flow").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_enrollment_flow") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_user_fields_add_all_link").click() |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Bindings").click() |  | ||||||
|  |  | ||||||
|         # Create Stage binding for first prompt stage |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_flow").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_flow") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-form").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_stage").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_stage") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Stage enroll-prompt-stage-first"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("0") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Stage binding for second prompt stage |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_flow").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_flow") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_stage").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_stage") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Stage enroll-prompt-stage-second"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("1") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Stage binding for user write stage |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_flow").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_flow") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_stage").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_stage") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Stage enroll-user-write"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("2") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         # Create Stage binding for user login stage |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "Create").click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_flow") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Flow Welcome (default-enrollment-flow)"]' |  | ||||||
|         ).click() |  | ||||||
|         dropdown = self.driver.find_element(By.ID, "id_stage") |  | ||||||
|         dropdown.find_element( |  | ||||||
|             By.XPATH, '//option[. = "Stage enroll-user-login"]' |  | ||||||
|         ).click() |  | ||||||
|         self.driver.find_element(By.ID, "id_order").send_keys("3") |  | ||||||
|         self.driver.find_element( |  | ||||||
|             By.CSS_SELECTOR, ".pf-c-form__actions > .pf-m-primary" |  | ||||||
|         ).click() |  | ||||||
|  |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click() |  | ||||||
|  |  | ||||||
|     def test_enroll_2_step(self): |  | ||||||
|         """Test 2-step enroll flow""" |  | ||||||
|         self.driver.get(self.live_server_url) |  | ||||||
|         self.setup_test_enroll_2_step() |  | ||||||
|         self.wait.until( |  | ||||||
|             ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]")) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click() |  | ||||||
|  |  | ||||||
|         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) |  | ||||||
|         self.driver.find_element(By.ID, "id_username").send_keys("foo") |  | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys("some name") |  | ||||||
|         self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz") |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() |  | ||||||
|  |  | ||||||
|         self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo"))) |  | ||||||
|         self.driver.find_element(By.LINK_TEXT, "foo").click() |  | ||||||
|  |  | ||||||
|         self.wait_for_url(self.url("passbook_core:user-settings")) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |  | ||||||
|             "foo", |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.ID, "id_name").get_attribute("value"), |  | ||||||
|             "some name", |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.ID, "id_email").get_attribute("value"), |  | ||||||
|             "foo@bar.baz", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend") |  | ||||||
|     def test_enroll_email(self): |  | ||||||
|         """Test enroll with Email verification""" |  | ||||||
|         # 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, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Second stage fields |  | ||||||
|         name_field = Prompt.objects.create( |  | ||||||
|             field_key="name", label="Name", order=0, type=FieldTypes.TEXT |  | ||||||
|         ) |  | ||||||
|         email = Prompt.objects.create( |  | ||||||
|             field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Stages |  | ||||||
|         first_stage = PromptStage.objects.create(name="prompt-stage-first") |  | ||||||
|         first_stage.fields.set([username_prompt, password, password_repeat]) |  | ||||||
|         first_stage.save() |  | ||||||
|         second_stage = PromptStage.objects.create(name="prompt-stage-second") |  | ||||||
|         second_stage.fields.set([name_field, email]) |  | ||||||
|         second_stage.save() |  | ||||||
|         email_stage = EmailStage.objects.create( |  | ||||||
|             name="enroll-email", |  | ||||||
|             host="localhost", |  | ||||||
|             port=1025, |  | ||||||
|             template=EmailTemplates.ACCOUNT_CONFIRM, |  | ||||||
|         ) |  | ||||||
|         user_write = UserWriteStage.objects.create(name="enroll-user-write") |  | ||||||
|         user_login = UserLoginStage.objects.create(name="enroll-user-login") |  | ||||||
|  |  | ||||||
|         # 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, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Attach enrollment flow to identification stage |  | ||||||
|         ident_stage: IdentificationStage = IdentificationStage.objects.first() |  | ||||||
|         ident_stage.enrollment_flow = flow |  | ||||||
|         ident_stage.save() |  | ||||||
|  |  | ||||||
|         FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0) |  | ||||||
|         FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1) |  | ||||||
|         FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2) |  | ||||||
|         FlowStageBinding.objects.create(flow=flow, stage=email_stage, order=3) |  | ||||||
|         FlowStageBinding.objects.create(flow=flow, stage=user_login, order=4) |  | ||||||
|  |  | ||||||
|         self.driver.get(self.live_server_url) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_username").send_keys("foo") |  | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username) |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() |  | ||||||
|         self.driver.find_element(By.ID, "id_name").send_keys("some name") |  | ||||||
|         self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz") |  | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() |  | ||||||
|         sleep(3) |  | ||||||
|  |  | ||||||
|         # Open Mailhog |  | ||||||
|         self.driver.get("http://localhost:8025") |  | ||||||
|  |  | ||||||
|         # Click on first message |  | ||||||
|         self.driver.find_element(By.CLASS_NAME, "msglist-message").click() |  | ||||||
|         sleep(3) |  | ||||||
|         self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane")) |  | ||||||
|         self.driver.find_element(By.ID, "confirm").click() |  | ||||||
|         self.driver.close() |  | ||||||
|         self.driver.switch_to.window(self.driver.window_handles[0]) |  | ||||||
|  |  | ||||||
|         # We're now logged in |  | ||||||
|         sleep(3) |  | ||||||
|         self.wait.until( |  | ||||||
|             ec.presence_of_element_located( |  | ||||||
|                 (By.XPATH, "//a[contains(@href, '/-/user/')]") |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() |  | ||||||
|  |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, |  | ||||||
|             "foo", |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.ID, "id_name").get_attribute("value"), |  | ||||||
|             "some name", |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             self.driver.find_element(By.ID, "id_email").get_attribute("value"), |  | ||||||
|             "foo@bar.baz", |  | ||||||
|         ) |  | ||||||
							
								
								
									
										260
									
								
								e2e/test_flows_enroll.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								e2e/test_flows_enroll.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,260 @@ | |||||||
|  | """Test Enroll flow""" | ||||||
|  | from time import sleep | ||||||
|  |  | ||||||
|  | from django.test import override_settings | ||||||
|  | from selenium.webdriver.common.by import By | ||||||
|  | from selenium.webdriver.support import expected_conditions as ec | ||||||
|  |  | ||||||
|  | from docker import DockerClient, from_env | ||||||
|  | from docker.models.containers import Container | ||||||
|  | from docker.types import Healthcheck | ||||||
|  | from e2e.utils import USER, SeleniumTestCase | ||||||
|  | from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||||
|  | from passbook.policies.expression.models import ExpressionPolicy | ||||||
|  | from passbook.policies.models import PolicyBinding | ||||||
|  | from passbook.stages.email.models import EmailStage, EmailTemplates | ||||||
|  | from passbook.stages.identification.models import IdentificationStage | ||||||
|  | from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage | ||||||
|  | from passbook.stages.user_login.models import UserLoginStage | ||||||
|  | from passbook.stages.user_write.models import UserWriteStage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestFlowsEnroll(SeleniumTestCase): | ||||||
|  |     """Test Enroll flow""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super().setUp() | ||||||
|  |         self.container = self.setup_client() | ||||||
|  |  | ||||||
|  |     def setup_client(self) -> Container: | ||||||
|  |         """Setup test IdP container""" | ||||||
|  |         client: DockerClient = from_env() | ||||||
|  |         container = client.containers.run( | ||||||
|  |             image="mailhog/mailhog", | ||||||
|  |             detach=True, | ||||||
|  |             network_mode="host", | ||||||
|  |             auto_remove=True, | ||||||
|  |             healthcheck=Healthcheck( | ||||||
|  |                 test=["CMD", "wget", "-s", "http://localhost:8025"], | ||||||
|  |                 interval=5 * 100 * 1000000, | ||||||
|  |                 start_period=1 * 100 * 1000000, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         while True: | ||||||
|  |             container.reload() | ||||||
|  |             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||||
|  |             if status == "healthy": | ||||||
|  |                 return container | ||||||
|  |             sleep(1) | ||||||
|  |  | ||||||
|  |     def tearDown(self): | ||||||
|  |         self.container.kill() | ||||||
|  |         super().tearDown() | ||||||
|  |  | ||||||
|  |     def test_enroll_2_step(self): | ||||||
|  |         """Test 2-step enroll flow""" | ||||||
|  |         # 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, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Second stage fields | ||||||
|  |         name_field = Prompt.objects.create( | ||||||
|  |             field_key="name", label="Name", order=0, type=FieldTypes.TEXT | ||||||
|  |         ) | ||||||
|  |         email = Prompt.objects.create( | ||||||
|  |             field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Stages | ||||||
|  |         first_stage = PromptStage.objects.create(name="prompt-stage-first") | ||||||
|  |         first_stage.fields.set([username_prompt, password, password_repeat]) | ||||||
|  |         first_stage.save() | ||||||
|  |         second_stage = PromptStage.objects.create(name="prompt-stage-second") | ||||||
|  |         second_stage.fields.set([name_field, email]) | ||||||
|  |         second_stage.save() | ||||||
|  |         user_write = UserWriteStage.objects.create(name="enroll-user-write") | ||||||
|  |         user_login = UserLoginStage.objects.create(name="enroll-user-login") | ||||||
|  |  | ||||||
|  |         # 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, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Attach enrollment flow to identification stage | ||||||
|  |         ident_stage: IdentificationStage = IdentificationStage.objects.first() | ||||||
|  |         ident_stage.enrollment_flow = flow | ||||||
|  |         ident_stage.save() | ||||||
|  |  | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=user_login, order=3) | ||||||
|  |  | ||||||
|  |         self.driver.get(self.live_server_url) | ||||||
|  |         self.wait.until( | ||||||
|  |             ec.presence_of_element_located((By.CSS_SELECTOR, "[role=enroll]")) | ||||||
|  |         ) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click() | ||||||
|  |  | ||||||
|  |         self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) | ||||||
|  |         self.driver.find_element(By.ID, "id_username").send_keys("foo") | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_name").send_keys("some name") | ||||||
|  |         self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz") | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||||
|  |  | ||||||
|  |         self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo"))) | ||||||
|  |         self.driver.find_element(By.LINK_TEXT, "foo").click() | ||||||
|  |  | ||||||
|  |         self.wait_for_url(self.url("passbook_core:user-settings")) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, | ||||||
|  |             "foo", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.ID, "id_name").get_attribute("value"), | ||||||
|  |             "some name", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.ID, "id_email").get_attribute("value"), | ||||||
|  |             "foo@bar.baz", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend") | ||||||
|  |     def test_enroll_email(self): | ||||||
|  |         """Test enroll with Email verification""" | ||||||
|  |         # 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, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Second stage fields | ||||||
|  |         name_field = Prompt.objects.create( | ||||||
|  |             field_key="name", label="Name", order=0, type=FieldTypes.TEXT | ||||||
|  |         ) | ||||||
|  |         email = Prompt.objects.create( | ||||||
|  |             field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Stages | ||||||
|  |         first_stage = PromptStage.objects.create(name="prompt-stage-first") | ||||||
|  |         first_stage.fields.set([username_prompt, password, password_repeat]) | ||||||
|  |         first_stage.save() | ||||||
|  |         second_stage = PromptStage.objects.create(name="prompt-stage-second") | ||||||
|  |         second_stage.fields.set([name_field, email]) | ||||||
|  |         second_stage.save() | ||||||
|  |         email_stage = EmailStage.objects.create( | ||||||
|  |             name="enroll-email", | ||||||
|  |             host="localhost", | ||||||
|  |             port=1025, | ||||||
|  |             template=EmailTemplates.ACCOUNT_CONFIRM, | ||||||
|  |         ) | ||||||
|  |         user_write = UserWriteStage.objects.create(name="enroll-user-write") | ||||||
|  |         user_login = UserLoginStage.objects.create(name="enroll-user-login") | ||||||
|  |  | ||||||
|  |         # 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, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Attach enrollment flow to identification stage | ||||||
|  |         ident_stage: IdentificationStage = IdentificationStage.objects.first() | ||||||
|  |         ident_stage.enrollment_flow = flow | ||||||
|  |         ident_stage.save() | ||||||
|  |  | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=email_stage, order=3) | ||||||
|  |         FlowStageBinding.objects.create(flow=flow, stage=user_login, order=4) | ||||||
|  |  | ||||||
|  |         self.driver.get(self.live_server_url) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_username").send_keys("foo") | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password_repeat").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_name").send_keys("some name") | ||||||
|  |         self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz") | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||||
|  |         sleep(3) | ||||||
|  |  | ||||||
|  |         # Open Mailhog | ||||||
|  |         self.driver.get("http://localhost:8025") | ||||||
|  |  | ||||||
|  |         # Click on first message | ||||||
|  |         self.driver.find_element(By.CLASS_NAME, "msglist-message").click() | ||||||
|  |         sleep(3) | ||||||
|  |         self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane")) | ||||||
|  |         self.driver.find_element(By.ID, "confirm").click() | ||||||
|  |         self.driver.close() | ||||||
|  |         self.driver.switch_to.window(self.driver.window_handles[0]) | ||||||
|  |  | ||||||
|  |         # We're now logged in | ||||||
|  |         sleep(3) | ||||||
|  |         self.wait.until( | ||||||
|  |             ec.presence_of_element_located( | ||||||
|  |                 (By.XPATH, "//a[contains(@href, '/-/user/')]") | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() | ||||||
|  |  | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, | ||||||
|  |             "foo", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.ID, "id_name").get_attribute("value"), | ||||||
|  |             "some name", | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.driver.find_element(By.ID, "id_email").get_attribute("value"), | ||||||
|  |             "foo@bar.baz", | ||||||
|  |         ) | ||||||
| @ -5,7 +5,7 @@ from selenium.webdriver.common.keys import Keys | |||||||
| from e2e.utils import USER, SeleniumTestCase | from e2e.utils import USER, SeleniumTestCase | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestLogin(SeleniumTestCase): | class TestFlowsLogin(SeleniumTestCase): | ||||||
|     """test default login flow""" |     """test default login flow""" | ||||||
| 
 | 
 | ||||||
|     def test_login(self): |     def test_login(self): | ||||||
							
								
								
									
										41
									
								
								e2e/test_flows_stage_setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								e2e/test_flows_stage_setup.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | """test stage setup flows (password change)""" | ||||||
|  | import string | ||||||
|  | from random import SystemRandom | ||||||
|  | from time import sleep | ||||||
|  |  | ||||||
|  | from selenium.webdriver.common.by import By | ||||||
|  | from selenium.webdriver.common.keys import Keys | ||||||
|  |  | ||||||
|  | from e2e.utils import USER, SeleniumTestCase | ||||||
|  | from passbook.core.models import User | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestFlowsStageSetup(SeleniumTestCase): | ||||||
|  |     """test stage setup flows""" | ||||||
|  |  | ||||||
|  |     def test_password_change(self): | ||||||
|  |         """test password change flow""" | ||||||
|  |         new_password = "".join( | ||||||
|  |             SystemRandom().choice(string.ascii_uppercase + string.digits) | ||||||
|  |             for _ in range(8) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.driver.get( | ||||||
|  |             f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F" | ||||||
|  |         ) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click() | ||||||
|  |         self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click() | ||||||
|  |         self.driver.find_element(By.LINK_TEXT, "Change password").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_password").send_keys(new_password) | ||||||
|  |         self.driver.find_element(By.ID, "id_password_repeat").click() | ||||||
|  |         self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password) | ||||||
|  |         self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click() | ||||||
|  |  | ||||||
|  |         sleep(2) | ||||||
|  |         # Because USER() is cached, we need to get the user manually here | ||||||
|  |         user = User.objects.get(username=USER().username) | ||||||
|  |         self.assertTrue(user.check_password(new_password)) | ||||||
| @ -88,6 +88,7 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         self.wait_for_url("http://localhost:9009/") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, |             self.driver.find_element(By.XPATH, "/html/body/pre").text, | ||||||
|             f"Hello, {USER().name}!", |             f"Hello, {USER().name}!", | ||||||
| @ -128,6 +129,7 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|             ).text, |             ).text, | ||||||
|         ) |         ) | ||||||
|         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() |         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() | ||||||
|  |         self.wait_for_url("http://localhost:9009/") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, |             self.driver.find_element(By.XPATH, "/html/body/pre").text, | ||||||
|             f"Hello, {USER().name}!", |             f"Hello, {USER().name}!", | ||||||
| @ -166,6 +168,7 @@ class TestProviderSAML(SeleniumTestCase): | |||||||
|         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) |         self.driver.find_element(By.ID, "id_password").send_keys(USER().username) | ||||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) |         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||||
|  |         self.wait_for_url("http://localhost:9009/") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, |             self.driver.find_element(By.XPATH, "/html/body/pre").text, | ||||||
|             f"Hello, {USER().name}!", |             f"Hello, {USER().name}!", | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ | |||||||
|         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> |         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||||
|             <thead> |             <thead> | ||||||
|                 <tr role="row"> |                 <tr role="row"> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Name' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Identifier' %}</th> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Designation' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Designation' %}</th> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Stages' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Stages' %}</th> | ||||||
|                     <th role="columnheader" scope="col">{% trans 'Policies' %}</th> |                     <th role="columnheader" scope="col">{% trans 'Policies' %}</th> | ||||||
| @ -39,8 +39,8 @@ | |||||||
|                 <tr role="row"> |                 <tr role="row"> | ||||||
|                     <th role="columnheader"> |                     <th role="columnheader"> | ||||||
|                         <div> |                         <div> | ||||||
|                             <div>{{ flow.name }}</div> |                             <div>{{ flow.slug }}</div> | ||||||
|                             <small>{{ flow.slug }}</small> |                             <small>{{ flow.name }}</small> | ||||||
|                         </div> |                         </div> | ||||||
|                     </th> |                     </th> | ||||||
|                     <td role="cell"> |                     <td role="cell"> | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from django.core.exceptions import ValidationError | |||||||
| from django.db import models | from django.db import models | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
|  | from django.views.debug import CLEANSED_SUBSTITUTE, HIDDEN_SETTINGS | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
| @ -20,6 +21,22 @@ from passbook.lib.utils.http import get_client_ip | |||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||||
|  |     """Cleanse a dictionary, recursively""" | ||||||
|  |     final_dict = {} | ||||||
|  |     for key, value in source.items(): | ||||||
|  |         try: | ||||||
|  |             if HIDDEN_SETTINGS.search(key): | ||||||
|  |                 final_dict[key] = CLEANSED_SUBSTITUTE | ||||||
|  |             else: | ||||||
|  |                 final_dict[key] = value | ||||||
|  |         except TypeError: | ||||||
|  |             final_dict[key] = value | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             final_dict[key] = cleanse_dict(value) | ||||||
|  |     return final_dict | ||||||
|  |  | ||||||
|  |  | ||||||
| def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||||
|     """clean source of all Models that would interfere with the JSONField. |     """clean source of all Models that would interfere with the JSONField. | ||||||
|     Models are replaced with a dictionary of { |     Models are replaced with a dictionary of { | ||||||
| @ -27,15 +44,16 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | |||||||
|         name: str, |         name: str, | ||||||
|         pk: Any |         pk: Any | ||||||
|     }""" |     }""" | ||||||
|  |     final_dict = {} | ||||||
|     for key, value in source.items(): |     for key, value in source.items(): | ||||||
|         if isinstance(value, dict): |         if isinstance(value, dict): | ||||||
|             source[key] = sanitize_dict(value) |             final_dict[key] = sanitize_dict(value) | ||||||
|         elif isinstance(value, models.Model): |         elif isinstance(value, models.Model): | ||||||
|             model_content_type = ContentType.objects.get_for_model(value) |             model_content_type = ContentType.objects.get_for_model(value) | ||||||
|             name = str(value) |             name = str(value) | ||||||
|             if hasattr(value, "name"): |             if hasattr(value, "name"): | ||||||
|                 name = value.name |                 name = value.name | ||||||
|             source[key] = sanitize_dict( |             final_dict[key] = sanitize_dict( | ||||||
|                 { |                 { | ||||||
|                     "app": model_content_type.app_label, |                     "app": model_content_type.app_label, | ||||||
|                     "model_name": model_content_type.model, |                     "model_name": model_content_type.model, | ||||||
| @ -44,8 +62,10 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | |||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         elif isinstance(value, UUID): |         elif isinstance(value, UUID): | ||||||
|             source[key] = value.hex |             final_dict[key] = value.hex | ||||||
|     return source |         else: | ||||||
|  |             final_dict[key] = value | ||||||
|  |     return final_dict | ||||||
|  |  | ||||||
|  |  | ||||||
| class EventAction(Enum): | class EventAction(Enum): | ||||||
| @ -104,7 +124,7 @@ class Event(models.Model): | |||||||
|             ) |             ) | ||||||
|         if not app: |         if not app: | ||||||
|             app = getmodule(stack()[_inspect_offset][0]).__name__ |             app = getmodule(stack()[_inspect_offset][0]).__name__ | ||||||
|         cleaned_kwargs = sanitize_dict(kwargs) |         cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs)) | ||||||
|         event = Event(action=action.value, app=app, context=cleaned_kwargs) |         event = Event(action=action.value, app=app, context=cleaned_kwargs) | ||||||
|         return event |         return event | ||||||
|  |  | ||||||
|  | |||||||
| @ -25,8 +25,7 @@ | |||||||
|                 <ul class="pf-c-nav__list"> |                 <ul class="pf-c-nav__list"> | ||||||
|                     {% for stage in user_stages_loc %} |                     {% for stage in user_stages_loc %} | ||||||
|                     <li class="pf-c-nav__item"> |                     <li class="pf-c-nav__item"> | ||||||
|                         <a href="{% url stage.view_name %}" class="pf-c-nav__link {% is_active stage.view_name %}"> |                         <a href="{{ stage.url }}" class="pf-c-nav__link {% is_active stage.view_name %}"> | ||||||
|                             <i class="{{ stage.icon }}"></i> |  | ||||||
|                             {{ stage.name }} |                             {{ stage.name }} | ||||||
|                         </a> |                         </a> | ||||||
|                     </li> |                     </li> | ||||||
| @ -43,7 +42,6 @@ | |||||||
|                     <li class="pf-c-nav__item"> |                     <li class="pf-c-nav__item"> | ||||||
|                         <a href="{{ source.view_name }}" |                         <a href="{{ source.view_name }}" | ||||||
|                             class="pf-c-nav__link {% if user_settings.view_name == request.get_full_path %} pf-m-current {% endif %}"> |                             class="pf-c-nav__link {% if user_settings.view_name == request.get_full_path %} pf-m-current {% endif %}"> | ||||||
|                             <i class="{{ source.icon }}"></i> |  | ||||||
|                             {{ source.name }} |                             {{ source.name }} | ||||||
|                         </a> |                         </a> | ||||||
|                     </li> |                     </li> | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ def user_stages(context: RequestContext) -> List[UIUserSettings]: | |||||||
|     _all_stages: Iterable[Stage] = Stage.__subclasses__() |     _all_stages: Iterable[Stage] = Stage.__subclasses__() | ||||||
|     matching_stages: List[UIUserSettings] = [] |     matching_stages: List[UIUserSettings] = [] | ||||||
|     for stage in _all_stages: |     for stage in _all_stages: | ||||||
|         user_settings = stage.ui_user_settings(context) |         user_settings = stage.ui_user_settings | ||||||
|         if not user_settings: |         if not user_settings: | ||||||
|             continue |             continue | ||||||
|         matching_stages.append(user_settings) |         matching_stages.append(user_settings) | ||||||
| @ -38,9 +38,7 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]: | |||||||
|         user_settings = source.ui_user_settings |         user_settings = source.ui_user_settings | ||||||
|         if not user_settings: |         if not user_settings: | ||||||
|             continue |             continue | ||||||
|         policy_engine = PolicyEngine( |         policy_engine = PolicyEngine(source, user, context.get("request")) | ||||||
|             source.policies.all(), user, context.get("request") |  | ||||||
|         ) |  | ||||||
|         policy_engine.build() |         policy_engine.build() | ||||||
|         if policy_engine.passing: |         if policy_engine.passing: | ||||||
|             matching_sources.append(user_settings) |             matching_sources.append(user_settings) | ||||||
|  | |||||||
| @ -8,8 +8,7 @@ class UIUserSettings: | |||||||
|     """Dataclass for Stage and Source's user_settings""" |     """Dataclass for Stage and Source's user_settings""" | ||||||
|  |  | ||||||
|     name: str |     name: str | ||||||
|     icon: str |     url: str | ||||||
|     view_name: str |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
|  | |||||||
| @ -20,42 +20,38 @@ def create_default_authentication_flow( | |||||||
|     ) |     ) | ||||||
|     db_alias = schema_editor.connection.alias |     db_alias = schema_editor.connection.alias | ||||||
|  |  | ||||||
|     if ( |     identification_stage, _ = IdentificationStage.objects.using( | ||||||
|         Flow.objects.using(db_alias) |         db_alias | ||||||
|         .filter(designation=FlowDesignation.AUTHENTICATION) |     ).update_or_create( | ||||||
|         .exists() |         name="default-authentication-identification", | ||||||
|     ): |         defaults={ | ||||||
|         # Only create default flow when none exist |             "user_fields": [UserFields.E_MAIL, UserFields.USERNAME], | ||||||
|         return |             "template": Templates.DEFAULT_LOGIN, | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     if not IdentificationStage.objects.using(db_alias).exists(): |     password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create( | ||||||
|         IdentificationStage.objects.using(db_alias).create( |         name="default-authentication-password", | ||||||
|             name="identification", |         defaults={"backends": ["django.contrib.auth.backends.ModelBackend"]}, | ||||||
|             user_fields=[UserFields.E_MAIL, UserFields.USERNAME], |     ) | ||||||
|             template=Templates.DEFAULT_LOGIN, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     if not PasswordStage.objects.using(db_alias).exists(): |     login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create( | ||||||
|         PasswordStage.objects.using(db_alias).create( |         name="default-authentication-login" | ||||||
|             name="password", backends=["django.contrib.auth.backends.ModelBackend"], |     ) | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     if not UserLoginStage.objects.using(db_alias).exists(): |     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||||
|         UserLoginStage.objects.using(db_alias).create(name="authentication") |  | ||||||
|  |  | ||||||
|     flow = Flow.objects.using(db_alias).create( |  | ||||||
|         name="Welcome to passbook!", |  | ||||||
|         slug="default-authentication-flow", |         slug="default-authentication-flow", | ||||||
|         designation=FlowDesignation.AUTHENTICATION, |         designation=FlowDesignation.AUTHENTICATION, | ||||||
|  |         defaults={"name": "Welcome to passbook!",}, | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=IdentificationStage.objects.using(db_alias).first(), order=0, |         flow=flow, stage=identification_stage, defaults={"order": 0,}, | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=PasswordStage.objects.using(db_alias).first(), order=1, |         flow=flow, stage=password_stage, defaults={"order": 1,}, | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=UserLoginStage.objects.using(db_alias).first(), order=2, |         flow=flow, stage=login_stage, defaults={"order": 2,}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -67,24 +63,19 @@ def create_default_invalidation_flow( | |||||||
|     UserLogoutStage = apps.get_model("passbook_stages_user_logout", "UserLogoutStage") |     UserLogoutStage = apps.get_model("passbook_stages_user_logout", "UserLogoutStage") | ||||||
|     db_alias = schema_editor.connection.alias |     db_alias = schema_editor.connection.alias | ||||||
|  |  | ||||||
|     if ( |     UserLogoutStage.objects.using(db_alias).update_or_create( | ||||||
|         Flow.objects.using(db_alias) |         name="default-invalidation-logout" | ||||||
|         .filter(designation=FlowDesignation.INVALIDATION) |     ) | ||||||
|         .exists() |  | ||||||
|     ): |  | ||||||
|         # Only create default flow when none exist |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     if not UserLogoutStage.objects.using(db_alias).exists(): |     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||||
|         UserLogoutStage.objects.using(db_alias).create(name="logout") |  | ||||||
|  |  | ||||||
|     flow = Flow.objects.using(db_alias).create( |  | ||||||
|         name="default-invalidation-flow", |  | ||||||
|         slug="default-invalidation-flow", |         slug="default-invalidation-flow", | ||||||
|         designation=FlowDesignation.INVALIDATION, |         designation=FlowDesignation.INVALIDATION, | ||||||
|  |         defaults={"name": "Logout",}, | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=UserLogoutStage.objects.using(db_alias).first(), order=0, |         flow=flow, | ||||||
|  |         stage=UserLogoutStage.objects.using(db_alias).first(), | ||||||
|  |         defaults={"order": 0,}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -34,60 +34,63 @@ def create_default_source_enrollment_flow( | |||||||
|     db_alias = schema_editor.connection.alias |     db_alias = schema_editor.connection.alias | ||||||
|  |  | ||||||
|     # Create a policy that only allows this flow when doing an SSO Request |     # Create a policy that only allows this flow when doing an SSO Request | ||||||
|     flow_policy = ExpressionPolicy.objects.using(db_alias).create( |     flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-enrollment-if-sso", expression=FLOW_POLICY_EXPRESSION |         name="default-source-enrollment-if-sso", | ||||||
|  |         defaults={"expression": FLOW_POLICY_EXPRESSION}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # This creates a Flow used by sources to enroll users |     # This creates a Flow used by sources to enroll users | ||||||
|     # It makes sure that a username is set, and if not, prompts the user for a Username |     # It makes sure that a username is set, and if not, prompts the user for a Username | ||||||
|     flow = Flow.objects.using(db_alias).create( |     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-enrollment", |  | ||||||
|         slug="default-source-enrollment", |         slug="default-source-enrollment", | ||||||
|         designation=FlowDesignation.ENROLLMENT, |         designation=FlowDesignation.ENROLLMENT, | ||||||
|  |         defaults={"name": "Welcome to passbook!",}, | ||||||
|     ) |     ) | ||||||
|     PolicyBinding.objects.using(db_alias).create( |     PolicyBinding.objects.using(db_alias).update_or_create( | ||||||
|         policy=flow_policy, target=flow, order=0 |         policy=flow_policy, target=flow, defaults={"order": 0} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # PromptStage to ask user for their username |     # PromptStage to ask user for their username | ||||||
|     prompt_stage = PromptStage.objects.using(db_alias).create( |     prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-enrollment-username-prompt", |         name="default-source-enrollment-username-prompt", | ||||||
|     ) |     ) | ||||||
|     prompt_stage.fields.add( |     prompt, _ = Prompt.objects.using(db_alias).update_or_create( | ||||||
|         Prompt.objects.using(db_alias).create( |         field_key="username", | ||||||
|             field_key="username", |         defaults={ | ||||||
|             label="Username", |             "label": "Username", | ||||||
|             type=FieldTypes.TEXT, |             "type": FieldTypes.TEXT, | ||||||
|             required=True, |             "required": True, | ||||||
|             placeholder="Username", |             "placeholder": "Username", | ||||||
|         ) |         }, | ||||||
|     ) |     ) | ||||||
|  |     prompt_stage.fields.add(prompt) | ||||||
|  |  | ||||||
|     # Policy to only trigger prompt when no username is given |     # Policy to only trigger prompt when no username is given | ||||||
|     prompt_policy = ExpressionPolicy.objects.using(db_alias).create( |     prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-enrollment-if-username", |         name="default-source-enrollment-if-username", | ||||||
|         expression=PROMPT_POLICY_EXPRESSION, |         defaults={"expression": PROMPT_POLICY_EXPRESSION}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # UserWrite stage to create the user, and login stage to log user in |     # UserWrite stage to create the user, and login stage to log user in | ||||||
|     user_write = UserWriteStage.objects.using(db_alias).create( |     user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-enrollment-write" |         name="default-source-enrollment-write" | ||||||
|     ) |     ) | ||||||
|     user_login = UserLoginStage.objects.using(db_alias).create( |     user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-enrollment-login" |         name="default-source-enrollment-login" | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     binding = FlowStageBinding.objects.using(db_alias).create( |     binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=prompt_stage, order=0 |         flow=flow, stage=prompt_stage, defaults={"order": 0} | ||||||
|     ) |     ) | ||||||
|     PolicyBinding.objects.using(db_alias).create( |     PolicyBinding.objects.using(db_alias).update_or_create( | ||||||
|         policy=prompt_policy, target=binding, order=0 |         policy=prompt_policy, target=binding, defaults={"order": 0} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=user_write, order=1 |         flow=flow, stage=user_write, defaults={"order": 1} | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=user_login, order=2 |         flow=flow, stage=user_login, defaults={"order": 2} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -107,25 +110,26 @@ def create_default_source_authentication_flow( | |||||||
|     db_alias = schema_editor.connection.alias |     db_alias = schema_editor.connection.alias | ||||||
|  |  | ||||||
|     # Create a policy that only allows this flow when doing an SSO Request |     # Create a policy that only allows this flow when doing an SSO Request | ||||||
|     flow_policy = ExpressionPolicy.objects.using(db_alias).create( |     flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-authentication-if-sso", expression=FLOW_POLICY_EXPRESSION |         name="default-source-authentication-if-sso", | ||||||
|  |         defaults={"expression": FLOW_POLICY_EXPRESSION,}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # This creates a Flow used by sources to authenticate users |     # This creates a Flow used by sources to authenticate users | ||||||
|     flow = Flow.objects.using(db_alias).create( |     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-authentication", |  | ||||||
|         slug="default-source-authentication", |         slug="default-source-authentication", | ||||||
|         designation=FlowDesignation.AUTHENTICATION, |         designation=FlowDesignation.AUTHENTICATION, | ||||||
|  |         defaults={"name": "Welcome to passbook!",}, | ||||||
|     ) |     ) | ||||||
|     PolicyBinding.objects.using(db_alias).create( |     PolicyBinding.objects.using(db_alias).update_or_create( | ||||||
|         policy=flow_policy, target=flow, order=0 |         policy=flow_policy, target=flow, defaults={"order": 0} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     user_login = UserLoginStage.objects.using(db_alias).create( |     user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create( | ||||||
|         name="default-source-authentication-login" |         name="default-source-authentication-login" | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create( |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|         flow=flow, stage=user_login, order=0 |         flow=flow, stage=user_login, defaults={"order": 0} | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor | |||||||
| from passbook.flows.models import FlowDesignation | from passbook.flows.models import FlowDesignation | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_default_provider_authz_flow( | def create_default_provider_authorization_flow( | ||||||
|     apps: Apps, schema_editor: BaseDatabaseSchemaEditor |     apps: Apps, schema_editor: BaseDatabaseSchemaEditor | ||||||
| ): | ): | ||||||
|     Flow = apps.get_model("passbook_flows", "Flow") |     Flow = apps.get_model("passbook_flows", "Flow") | ||||||
| @ -18,22 +18,24 @@ def create_default_provider_authz_flow( | |||||||
|     db_alias = schema_editor.connection.alias |     db_alias = schema_editor.connection.alias | ||||||
|  |  | ||||||
|     # Empty flow for providers where consent is implicitly given |     # Empty flow for providers where consent is implicitly given | ||||||
|     Flow.objects.using(db_alias).create( |     Flow.objects.using(db_alias).update_or_create( | ||||||
|         name="Authorize Application", |  | ||||||
|         slug="default-provider-authorization-implicit-consent", |         slug="default-provider-authorization-implicit-consent", | ||||||
|         designation=FlowDesignation.AUTHORIZATION, |         designation=FlowDesignation.AUTHORIZATION, | ||||||
|  |         defaults={"name": "Authorize Application"}, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # Flow with consent form to obtain explicit user consent |     # Flow with consent form to obtain explicit user consent | ||||||
|     flow = Flow.objects.using(db_alias).create( |     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||||
|         name="Authorize Application", |  | ||||||
|         slug="default-provider-authorization-explicit-consent", |         slug="default-provider-authorization-explicit-consent", | ||||||
|         designation=FlowDesignation.AUTHORIZATION, |         designation=FlowDesignation.AUTHORIZATION, | ||||||
|  |         defaults={"name": "Authorize Application"}, | ||||||
|     ) |     ) | ||||||
|     stage = ConsentStage.objects.using(db_alias).create( |     stage, _ = ConsentStage.objects.using(db_alias).update_or_create( | ||||||
|         name="default-provider-authorization-consent" |         name="default-provider-authorization-consent" | ||||||
|     ) |     ) | ||||||
|     FlowStageBinding.objects.using(db_alias).create(flow=flow, stage=stage, order=0) |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|  |         flow=flow, stage=stage, defaults={"order": 0} | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
| @ -43,4 +45,4 @@ class Migration(migrations.Migration): | |||||||
|         ("passbook_stages_consent", "0001_initial"), |         ("passbook_stages_consent", "0001_initial"), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     operations = [migrations.RunPython(create_default_provider_authz_flow)] |     operations = [migrations.RunPython(create_default_provider_authorization_flow)] | ||||||
|  | |||||||
							
								
								
									
										29
									
								
								passbook/flows/migrations/0006_auto_20200629_0857.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								passbook/flows/migrations/0006_auto_20200629_0857.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | # Generated by Django 3.0.7 on 2020-06-29 08:57 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("passbook_flows", "0005_provider_flows"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="flow", | ||||||
|  |             name="designation", | ||||||
|  |             field=models.CharField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("authentication", "Authentication"), | ||||||
|  |                     ("authorization", "Authorization"), | ||||||
|  |                     ("invalidation", "Invalidation"), | ||||||
|  |                     ("enrollment", "Enrollment"), | ||||||
|  |                     ("unenrollment", "Unrenollment"), | ||||||
|  |                     ("recovery", "Recovery"), | ||||||
|  |                     ("stage_setup", "Stage Setup"), | ||||||
|  |                 ], | ||||||
|  |                 max_length=100, | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -7,7 +7,6 @@ from django.http import HttpRequest | |||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from model_utils.managers import InheritanceManager | from model_utils.managers import InheritanceManager | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
| from django.template.context import RequestContext |  | ||||||
|  |  | ||||||
| from passbook.core.types import UIUserSettings | from passbook.core.types import UIUserSettings | ||||||
| from passbook.lib.utils.reflection import class_to_path | from passbook.lib.utils.reflection import class_to_path | ||||||
| @ -33,7 +32,7 @@ class FlowDesignation(models.TextChoices): | |||||||
|     ENROLLMENT = "enrollment" |     ENROLLMENT = "enrollment" | ||||||
|     UNRENOLLMENT = "unenrollment" |     UNRENOLLMENT = "unenrollment" | ||||||
|     RECOVERY = "recovery" |     RECOVERY = "recovery" | ||||||
|     USER_SETTINGS = "user_settings" |     STAGE_SETUP = "stage_setup" | ||||||
|  |  | ||||||
|  |  | ||||||
| class Stage(models.Model): | class Stage(models.Model): | ||||||
| @ -48,8 +47,8 @@ class Stage(models.Model): | |||||||
|     type = "" |     type = "" | ||||||
|     form = "" |     form = "" | ||||||
|  |  | ||||||
|     @staticmethod |     @property | ||||||
|     def ui_user_settings(context: RequestContext) -> Optional[UIUserSettings]: |     def ui_user_settings(self) -> Optional[UIUserSettings]: | ||||||
|         """Entrypoint to integrate with User settings. Can either return None if no |         """Entrypoint to integrate with User settings. Can either return None if no | ||||||
|         user settings are available, or an instanace of UIUserSettings.""" |         user settings are available, or an instanace of UIUserSettings.""" | ||||||
|         return None |         return None | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ def delete_cache_prefix(prefix: str) -> int: | |||||||
|     cache.delete_many(keys) |     cache.delete_many(keys) | ||||||
|     return len(keys) |     return len(keys) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save) | @receiver(post_save) | ||||||
| # pylint: disable=unused-argument | # pylint: disable=unused-argument | ||||||
| def invalidate_flow_cache(sender, instance, **_): | def invalidate_flow_cache(sender, instance, **_): | ||||||
| @ -25,7 +26,9 @@ def invalidate_flow_cache(sender, instance, **_): | |||||||
|         LOGGER.debug("Invalidating Flow cache", flow=instance, len=total) |         LOGGER.debug("Invalidating Flow cache", flow=instance, len=total) | ||||||
|     if isinstance(instance, FlowStageBinding): |     if isinstance(instance, FlowStageBinding): | ||||||
|         total = delete_cache_prefix(f"{cache_key(instance.flow)}*") |         total = delete_cache_prefix(f"{cache_key(instance.flow)}*") | ||||||
|         LOGGER.debug("Invalidating Flow cache from FlowStageBinding", binding=instance, len=total) |         LOGGER.debug( | ||||||
|  |             "Invalidating Flow cache from FlowStageBinding", binding=instance, len=total | ||||||
|  |         ) | ||||||
|     if isinstance(instance, Stage): |     if isinstance(instance, Stage): | ||||||
|         total = 0 |         total = 0 | ||||||
|         for binding in FlowStageBinding.objects.filter(stage=instance): |         for binding in FlowStageBinding.objects.filter(stage=instance): | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin | |||||||
| from django.views.generic import TemplateView, View | from django.views.generic import TemplateView, View | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
|  | from passbook.audit.models import cleanse_dict | ||||||
| from passbook.core.views.utils import PermissionDeniedView | from passbook.core.views.utils import PermissionDeniedView | ||||||
| from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException | from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||||
| from passbook.flows.models import Flow, FlowDesignation, Stage | from passbook.flows.models import Flow, FlowDesignation, Stage | ||||||
| @ -161,7 +162,7 @@ class FlowExecutorView(View): | |||||||
|         LOGGER.debug( |         LOGGER.debug( | ||||||
|             "f(exec): User passed all stages", |             "f(exec): User passed all stages", | ||||||
|             flow_slug=self.flow.slug, |             flow_slug=self.flow.slug, | ||||||
|             context=self.plan.context, |             context=cleanse_dict(self.plan.context), | ||||||
|         ) |         ) | ||||||
|         return self._flow_done() |         return self._flow_done() | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| """OAuth Client models""" | """OAuth Client models""" | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.urls import reverse, reverse_lazy | from django.urls import reverse, reverse_lazy | ||||||
| @ -61,16 +62,10 @@ class OAuthSource(Source): | |||||||
|         return f"Callback URL: <pre>{url}</pre>" |         return f"Callback URL: <pre>{url}</pre>" | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def ui_user_settings(self) -> UIUserSettings: |     def ui_user_settings(self) -> Optional[UIUserSettings]: | ||||||
|         icon_type = self.provider_type |  | ||||||
|         if icon_type == "azure ad": |  | ||||||
|             icon_type = "windows" |  | ||||||
|         icon_class = f"fab fa-{icon_type}" |  | ||||||
|         view_name = "passbook_sources_oauth:oauth-client-user" |         view_name = "passbook_sources_oauth:oauth-client-user" | ||||||
|         return UIUserSettings( |         return UIUserSettings( | ||||||
|             name=self.name, |             name=self.name, url=reverse(view_name, kwargs={"source_slug": self.slug}), | ||||||
|             icon=icon_class, |  | ||||||
|             view_name=reverse((view_name), kwargs={"source_slug": self.slug}), |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|  | |||||||
| @ -153,7 +153,7 @@ class Processor: | |||||||
|         self, request: HttpRequest, flow: Flow, **kwargs |         self, request: HttpRequest, flow: Flow, **kwargs | ||||||
|     ) -> HttpResponse: |     ) -> HttpResponse: | ||||||
|         kwargs[PLAN_CONTEXT_SSO] = True |         kwargs[PLAN_CONTEXT_SSO] = True | ||||||
|         request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(request, kwargs,) |         request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(request, kwargs) | ||||||
|         return redirect_with_qs( |         return redirect_with_qs( | ||||||
|             "passbook_flows:flow-executor-shell", request.GET, flow_slug=flow.slug, |             "passbook_flows:flow-executor-shell", request.GET, flow_slug=flow.slug, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ from django.core.validators import validate_email | |||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from structlog import get_logger | from structlog import get_logger | ||||||
|  |  | ||||||
|  | from passbook.flows.models import Flow, FlowDesignation | ||||||
| from passbook.lib.utils.ui import human_list | from passbook.lib.utils.ui import human_list | ||||||
| from passbook.stages.identification.models import IdentificationStage, UserFields | from passbook.stages.identification.models import IdentificationStage, UserFields | ||||||
|  |  | ||||||
| @ -14,6 +15,15 @@ LOGGER = get_logger() | |||||||
| class IdentificationStageForm(forms.ModelForm): | class IdentificationStageForm(forms.ModelForm): | ||||||
|     """Form to create/edit IdentificationStage instances""" |     """Form to create/edit IdentificationStage instances""" | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         self.fields["enrollment_flow"].queryset = Flow.objects.filter( | ||||||
|  |             designation=FlowDesignation.ENROLLMENT | ||||||
|  |         ) | ||||||
|  |         self.fields["recovery_flow"].queryset = Flow.objects.filter( | ||||||
|  |             designation=FlowDesignation.RECOVERY | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = IdentificationStage |         model = IdentificationStage | ||||||
|  | |||||||
| @ -8,3 +8,4 @@ class PassbookStagePasswordConfig(AppConfig): | |||||||
|     name = "passbook.stages.password" |     name = "passbook.stages.password" | ||||||
|     label = "passbook_stages_password" |     label = "passbook_stages_password" | ||||||
|     verbose_name = "passbook Stages.Password" |     verbose_name = "passbook Stages.Password" | ||||||
|  |     mountpoint = "-/user/stage/password/" | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ from django import forms | |||||||
| from django.contrib.admin.widgets import FilteredSelectMultiple | from django.contrib.admin.widgets import FilteredSelectMultiple | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  | from passbook.flows.models import Flow, FlowDesignation | ||||||
| from passbook.stages.password.models import PasswordStage | from passbook.stages.password.models import PasswordStage | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -40,14 +41,19 @@ class PasswordForm(forms.Form): | |||||||
| class PasswordStageForm(forms.ModelForm): | class PasswordStageForm(forms.ModelForm): | ||||||
|     """Form to create/edit Password Stages""" |     """Form to create/edit Password Stages""" | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         self.fields["change_flow"].queryset = Flow.objects.filter( | ||||||
|  |             designation=FlowDesignation.STAGE_SETUP | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = PasswordStage |         model = PasswordStage | ||||||
|         fields = ["name", "backends"] |         fields = ["name", "backends", "change_flow"] | ||||||
|         widgets = { |         widgets = { | ||||||
|             "name": forms.TextInput(), |             "name": forms.TextInput(), | ||||||
|             "backends": FilteredSelectMultiple( |             "backends": FilteredSelectMultiple( | ||||||
|                 _("backends"), False, choices=get_authentication_backends() |                 _("backends"), False, choices=get_authentication_backends() | ||||||
|             ), |             ), | ||||||
|             "password_policies": FilteredSelectMultiple(_("password policies"), False), |  | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -0,0 +1,126 @@ | |||||||
|  | # Generated by Django 3.0.7 on 2020-06-29 08:51 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.apps.registry import Apps | ||||||
|  | from django.db import migrations, models | ||||||
|  | from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||||
|  |  | ||||||
|  | from passbook.flows.models import FlowDesignation | ||||||
|  | from passbook.stages.prompt.models import FieldTypes | ||||||
|  |  | ||||||
|  | PROMPT_POLICY_EXPRESSION = """# Check that both passwords are equal. | ||||||
|  | return request.context['password'] == request.context['password_repeat']""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_default_password_change(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||||
|  |     Flow = apps.get_model("passbook_flows", "Flow") | ||||||
|  |     FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding") | ||||||
|  |  | ||||||
|  |     PolicyBinding = apps.get_model("passbook_policies", "PolicyBinding") | ||||||
|  |  | ||||||
|  |     ExpressionPolicy = apps.get_model( | ||||||
|  |         "passbook_policies_expression", "ExpressionPolicy" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     PromptStage = apps.get_model("passbook_stages_prompt", "PromptStage") | ||||||
|  |     Prompt = apps.get_model("passbook_stages_prompt", "Prompt") | ||||||
|  |  | ||||||
|  |     UserWriteStage = apps.get_model("passbook_stages_user_write", "UserWriteStage") | ||||||
|  |  | ||||||
|  |     db_alias = schema_editor.connection.alias | ||||||
|  |  | ||||||
|  |     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||||
|  |         slug="default-password-change", | ||||||
|  |         designation=FlowDesignation.STAGE_SETUP, | ||||||
|  |         defaults={"name": "Change Password"}, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create( | ||||||
|  |         name="default-password-change-prompt", | ||||||
|  |     ) | ||||||
|  |     password_prompt, _ = Prompt.objects.using(db_alias).update_or_create( | ||||||
|  |         field_key="password", | ||||||
|  |         defaults={ | ||||||
|  |             "label": "Password", | ||||||
|  |             "type": FieldTypes.PASSWORD, | ||||||
|  |             "required": True, | ||||||
|  |             "placeholder": "Password", | ||||||
|  |             "order": 0, | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     password_rep_prompt, _ = Prompt.objects.using(db_alias).update_or_create( | ||||||
|  |         field_key="password_repeat", | ||||||
|  |         defaults={ | ||||||
|  |             "label": "Password (repeat)", | ||||||
|  |             "type": FieldTypes.PASSWORD, | ||||||
|  |             "required": True, | ||||||
|  |             "placeholder": "Password (repeat)", | ||||||
|  |             "order": 1, | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     prompt_stage.fields.add(password_prompt) | ||||||
|  |     prompt_stage.fields.add(password_rep_prompt) | ||||||
|  |  | ||||||
|  |     # Policy to only trigger prompt when no username is given | ||||||
|  |     prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( | ||||||
|  |         name="default-password-change-password-equal", | ||||||
|  |         defaults={"expression": PROMPT_POLICY_EXPRESSION}, | ||||||
|  |     ) | ||||||
|  |     PolicyBinding.objects.using(db_alias).update_or_create( | ||||||
|  |         policy=prompt_policy, target=prompt_stage, defaults={"order": 0} | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create( | ||||||
|  |         name="default-password-change-write" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|  |         flow=flow, stage=prompt_stage, defaults={"order": 0} | ||||||
|  |     ) | ||||||
|  |     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||||
|  |         flow=flow, stage=user_write, defaults={"order": 1} | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_default_stage_change(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||||
|  |     PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage") | ||||||
|  |     Flow = apps.get_model("passbook_flows", "Flow") | ||||||
|  |  | ||||||
|  |     flow = Flow.objects.get( | ||||||
|  |         slug="default-password-change", designation=FlowDesignation.STAGE_SETUP, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     stages = PasswordStage.objects.filter(name="default-authentication-password") | ||||||
|  |     if not stages.exists(): | ||||||
|  |         return | ||||||
|  |     stage = stages.first() | ||||||
|  |     stage.change_flow = flow | ||||||
|  |     stage.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("passbook_flows", "0006_auto_20200629_0857"), | ||||||
|  |         ("passbook_policies_expression", "0001_initial"), | ||||||
|  |         ("passbook_policies", "0001_initial"), | ||||||
|  |         ("passbook_stages_password", "0001_initial"), | ||||||
|  |         ("passbook_stages_prompt", "0004_auto_20200618_1735"), | ||||||
|  |         ("passbook_stages_user_write", "0001_initial"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="passwordstage", | ||||||
|  |             name="change_flow", | ||||||
|  |             field=models.ForeignKey( | ||||||
|  |                 blank=True, | ||||||
|  |                 help_text="Flow used by an authenticated user to change their password. If empty, user will be unable to change their password.", | ||||||
|  |                 null=True, | ||||||
|  |                 on_delete=django.db.models.deletion.SET_NULL, | ||||||
|  |                 to="passbook_flows.Flow", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |         migrations.RunPython(create_default_password_change), | ||||||
|  |         migrations.RunPython(update_default_stage_change), | ||||||
|  |     ] | ||||||
| @ -1,9 +1,15 @@ | |||||||
| """password stage models""" | """password stage models""" | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from django.contrib.postgres.fields import ArrayField | from django.contrib.postgres.fields import ArrayField | ||||||
| from django.db import models | from django.db import models | ||||||
|  | from django.shortcuts import reverse | ||||||
|  | from django.utils.http import urlencode | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
| from passbook.flows.models import Stage | from passbook.core.types import UIUserSettings | ||||||
|  | from passbook.flows.models import Flow, Stage | ||||||
|  | from passbook.flows.views import NEXT_ARG_NAME | ||||||
|  |  | ||||||
|  |  | ||||||
| class PasswordStage(Stage): | class PasswordStage(Stage): | ||||||
| @ -14,9 +20,32 @@ class PasswordStage(Stage): | |||||||
|         help_text=_("Selection of backends to test the password against."), |         help_text=_("Selection of backends to test the password against."), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     change_flow = models.ForeignKey( | ||||||
|  |         Flow, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         null=True, | ||||||
|  |         blank=True, | ||||||
|  |         help_text=_( | ||||||
|  |             ( | ||||||
|  |                 "Flow used by an authenticated user to change their password. " | ||||||
|  |                 "If empty, user will be unable to change their password." | ||||||
|  |             ) | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     type = "passbook.stages.password.stage.PasswordStage" |     type = "passbook.stages.password.stage.PasswordStage" | ||||||
|     form = "passbook.stages.password.forms.PasswordStageForm" |     form = "passbook.stages.password.forms.PasswordStageForm" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def ui_user_settings(self) -> Optional[UIUserSettings]: | ||||||
|  |         if not self.change_flow: | ||||||
|  |             return None | ||||||
|  |         base_url = reverse( | ||||||
|  |             "passbook_stages_password:change", kwargs={"stage_uuid": self.pk} | ||||||
|  |         ) | ||||||
|  |         args = urlencode({NEXT_ARG_NAME: reverse("passbook_core:user-settings")}) | ||||||
|  |         return UIUserSettings(name=_("Change password"), url=f"{base_url}?{args}") | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return f"Password Stage {self.name}" |         return f"Password Stage {self.name}" | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								passbook/stages/password/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								passbook/stages/password/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | """password stage URLs""" | ||||||
|  | from django.urls import path | ||||||
|  |  | ||||||
|  | from passbook.stages.password.views import ChangeFlowInitView | ||||||
|  |  | ||||||
|  | urlpatterns = [ | ||||||
|  |     path("<uuid:stage_uuid>/change/", ChangeFlowInitView.as_view(), name="change") | ||||||
|  | ] | ||||||
							
								
								
									
										32
									
								
								passbook/stages/password/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								passbook/stages/password/views.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | """password stage views""" | ||||||
|  | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
|  | from django.http import Http404, HttpRequest, HttpResponse | ||||||
|  | from django.shortcuts import get_object_or_404 | ||||||
|  | from django.views import View | ||||||
|  |  | ||||||
|  | from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner | ||||||
|  | from passbook.flows.views import SESSION_KEY_PLAN | ||||||
|  | from passbook.lib.utils.urls import redirect_with_qs | ||||||
|  | from passbook.stages.password.models import PasswordStage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ChangeFlowInitView(LoginRequiredMixin, View): | ||||||
|  |     """Initiate planner for selected change flow and redirect to flow executor, | ||||||
|  |     or raise Http404 if no change_flow has been set.""" | ||||||
|  |  | ||||||
|  |     def get(self, request: HttpRequest, stage_uuid: str) -> HttpResponse: | ||||||
|  |         """Initiate planner for selected change flow and redirect to flow executor, | ||||||
|  |         or raise Http404 if no change_flow has been set.""" | ||||||
|  |         stage: PasswordStage = get_object_or_404(PasswordStage, pk=stage_uuid) | ||||||
|  |         if not stage.change_flow: | ||||||
|  |             raise Http404 | ||||||
|  |  | ||||||
|  |         plan = FlowPlanner(stage.change_flow).plan( | ||||||
|  |             request, {PLAN_CONTEXT_PENDING_USER: request.user} | ||||||
|  |         ) | ||||||
|  |         request.session[SESSION_KEY_PLAN] = plan | ||||||
|  |         return redirect_with_qs( | ||||||
|  |             "passbook_flows:flow-executor-shell", | ||||||
|  |             self.request.GET, | ||||||
|  |             flow_slug=stage.change_flow.slug, | ||||||
|  |         ) | ||||||
| @ -1,5 +1,7 @@ | |||||||
| """Prompt forms""" | """Prompt forms""" | ||||||
| from django import forms | from django import forms | ||||||
|  | from django.contrib.admin.widgets import FilteredSelectMultiple | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
|  |  | ||||||
| from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||||
| @ -16,6 +18,7 @@ class PromptStageForm(forms.ModelForm): | |||||||
|         fields = ["name", "fields"] |         fields = ["name", "fields"] | ||||||
|         widgets = { |         widgets = { | ||||||
|             "name": forms.TextInput(), |             "name": forms.TextInput(), | ||||||
|  |             "fields": FilteredSelectMultiple(_("prompts"), False), | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| """Write stage logic""" | """Write stage logic""" | ||||||
| from django.contrib import messages | from django.contrib import messages | ||||||
|  | from django.contrib.auth import update_session_auth_hash | ||||||
| from django.contrib.auth.backends import ModelBackend | from django.contrib.auth.backends import ModelBackend | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| @ -48,6 +49,10 @@ class UserWriteStageView(StageView): | |||||||
|             else: |             else: | ||||||
|                 user.attributes[key] = value |                 user.attributes[key] = value | ||||||
|         user.save() |         user.save() | ||||||
|  |         # Check if the password has been updated, and update the session auth hash | ||||||
|  |         if any(["password" in x for x in data.keys()]): | ||||||
|  |             update_session_auth_hash(self.request, user) | ||||||
|  |             LOGGER.debug("Updated session hash", user=user) | ||||||
|         LOGGER.debug( |         LOGGER.debug( | ||||||
|             "Updated existing user", user=user, flow_slug=self.executor.flow.slug, |             "Updated existing user", user=user, flow_slug=self.executor.flow.slug, | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -5177,7 +5177,7 @@ definitions: | |||||||
|           - enrollment |           - enrollment | ||||||
|           - unenrollment |           - unenrollment | ||||||
|           - recovery |           - recovery | ||||||
|           - user_settings |           - stage_setup | ||||||
|       stages: |       stages: | ||||||
|         type: array |         type: array | ||||||
|         items: |         items: | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer