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": { | ||||
|             "hashes": [ | ||||
|                 "sha256:8eeaa2d6374f02d6f0d6b8bee55838add9a4246de81b1402e59372296402f6c7", | ||||
|                 "sha256:d1d93ed75f477e8910b8b074ae76e3189d1c3a3998ea679ab52fdbacb8b4f390" | ||||
|                 "sha256:2616351c98eec18d20a1d64b33355c86cd855ac96219d1b8428c9bfc590bde53", | ||||
|                 "sha256:7daad26a008c91dd7b82fde17d246d1fe6e4b3813426689ef8bac9017a277cfb" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.14.11" | ||||
|             "version": "==1.14.12" | ||||
|         }, | ||||
|         "botocore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:64454a6dff9a3ced0dd75c0e69a8842aa663e0682d1dc5c8913fb76d354bfe3d", | ||||
|                 "sha256:715b41f3215214e75bf7b8e88bdbc38dc055eef761b37dbd559bac1a5becb3c2" | ||||
|                 "sha256:45934d880378777cefeca727f369d1f5aebf6b254e9be58e7c77dd0b059338bb", | ||||
|                 "sha256:a94e0e2307f1b9fe3a84660842909cd2680b57a9fc9fb0c3a03b0afb2eadbe21" | ||||
|             ], | ||||
|             "version": "==1.17.11" | ||||
|             "version": "==1.17.12" | ||||
|         }, | ||||
|         "celery": { | ||||
|             "hashes": [ | ||||
| @ -322,10 +322,10 @@ | ||||
|         }, | ||||
|         "idna": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", | ||||
|                 "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" | ||||
|                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||
|                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||
|             ], | ||||
|             "version": "==2.9" | ||||
|             "version": "==2.10" | ||||
|         }, | ||||
|         "inflection": { | ||||
|             "hashes": [ | ||||
| @ -604,10 +604,10 @@ | ||||
|         }, | ||||
|         "pyparsing": { | ||||
|             "hashes": [ | ||||
|                 "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", | ||||
|                 "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" | ||||
|                 "sha256:1060635ca5ac864c2b7bc7b05a448df4e32d7d8c65e33cbe1514810d339672a2", | ||||
|                 "sha256:56a551039101858c9e189ac9e66e330a03fb7079e97ba6b50193643905f450ce" | ||||
|             ], | ||||
|             "version": "==3.0.0a1" | ||||
|             "version": "==3.0.0a2" | ||||
|         }, | ||||
|         "pyrsistent": { | ||||
|             "hashes": [ | ||||
| @ -1044,10 +1044,10 @@ | ||||
|         }, | ||||
|         "idna": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", | ||||
|                 "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" | ||||
|                 "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", | ||||
|                 "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" | ||||
|             ], | ||||
|             "version": "==2.9" | ||||
|             "version": "==2.10" | ||||
|         }, | ||||
|         "isort": { | ||||
|             "hashes": [ | ||||
|  | ||||
| @ -286,6 +286,204 @@ | ||||
|       ], | ||||
|       "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": [{ | ||||
|     "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 | ||||
| 
 | ||||
| 
 | ||||
| class TestLogin(SeleniumTestCase): | ||||
| class TestFlowsLogin(SeleniumTestCase): | ||||
|     """test default login flow""" | ||||
| 
 | ||||
|     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_password").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||
|         self.wait_for_url("http://localhost:9009/") | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, | ||||
|             f"Hello, {USER().name}!", | ||||
| @ -128,6 +129,7 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|             ).text, | ||||
|         ) | ||||
|         self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() | ||||
|         self.wait_for_url("http://localhost:9009/") | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, | ||||
|             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_password").send_keys(USER().username) | ||||
|         self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) | ||||
|         self.wait_for_url("http://localhost:9009/") | ||||
|         self.assertEqual( | ||||
|             self.driver.find_element(By.XPATH, "/html/body/pre").text, | ||||
|             f"Hello, {USER().name}!", | ||||
|  | ||||
| @ -27,7 +27,7 @@ | ||||
|         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||
|             <thead> | ||||
|                 <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 'Stages' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Policies' %}</th> | ||||
| @ -39,8 +39,8 @@ | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader"> | ||||
|                         <div> | ||||
|                             <div>{{ flow.name }}</div> | ||||
|                             <small>{{ flow.slug }}</small> | ||||
|                             <div>{{ flow.slug }}</div> | ||||
|                             <small>{{ flow.name }}</small> | ||||
|                         </div> | ||||
|                     </th> | ||||
|                     <td role="cell"> | ||||
|  | ||||
| @ -12,6 +12,7 @@ from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.http import HttpRequest | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.debug import CLEANSED_SUBSTITUTE, HIDDEN_SETTINGS | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from structlog import get_logger | ||||
|  | ||||
| @ -20,6 +21,22 @@ from passbook.lib.utils.http import get_client_ip | ||||
| 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]: | ||||
|     """clean source of all Models that would interfere with the JSONField. | ||||
|     Models are replaced with a dictionary of { | ||||
| @ -27,15 +44,16 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||
|         name: str, | ||||
|         pk: Any | ||||
|     }""" | ||||
|     final_dict = {} | ||||
|     for key, value in source.items(): | ||||
|         if isinstance(value, dict): | ||||
|             source[key] = sanitize_dict(value) | ||||
|             final_dict[key] = sanitize_dict(value) | ||||
|         elif isinstance(value, models.Model): | ||||
|             model_content_type = ContentType.objects.get_for_model(value) | ||||
|             name = str(value) | ||||
|             if hasattr(value, "name"): | ||||
|                 name = value.name | ||||
|             source[key] = sanitize_dict( | ||||
|             final_dict[key] = sanitize_dict( | ||||
|                 { | ||||
|                     "app": model_content_type.app_label, | ||||
|                     "model_name": model_content_type.model, | ||||
| @ -44,8 +62,10 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||
|                 } | ||||
|             ) | ||||
|         elif isinstance(value, UUID): | ||||
|             source[key] = value.hex | ||||
|     return source | ||||
|             final_dict[key] = value.hex | ||||
|         else: | ||||
|             final_dict[key] = value | ||||
|     return final_dict | ||||
|  | ||||
|  | ||||
| class EventAction(Enum): | ||||
| @ -104,7 +124,7 @@ class Event(models.Model): | ||||
|             ) | ||||
|         if not app: | ||||
|             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) | ||||
|         return event | ||||
|  | ||||
|  | ||||
| @ -25,8 +25,7 @@ | ||||
|                 <ul class="pf-c-nav__list"> | ||||
|                     {% for stage in user_stages_loc %} | ||||
|                     <li class="pf-c-nav__item"> | ||||
|                         <a href="{% url stage.view_name %}" class="pf-c-nav__link {% is_active stage.view_name %}"> | ||||
|                             <i class="{{ stage.icon }}"></i> | ||||
|                         <a href="{{ stage.url }}" class="pf-c-nav__link {% is_active stage.view_name %}"> | ||||
|                             {{ stage.name }} | ||||
|                         </a> | ||||
|                     </li> | ||||
| @ -43,7 +42,6 @@ | ||||
|                     <li class="pf-c-nav__item"> | ||||
|                         <a href="{{ source.view_name }}" | ||||
|                             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 }} | ||||
|                         </a> | ||||
|                     </li> | ||||
|  | ||||
| @ -19,7 +19,7 @@ def user_stages(context: RequestContext) -> List[UIUserSettings]: | ||||
|     _all_stages: Iterable[Stage] = Stage.__subclasses__() | ||||
|     matching_stages: List[UIUserSettings] = [] | ||||
|     for stage in _all_stages: | ||||
|         user_settings = stage.ui_user_settings(context) | ||||
|         user_settings = stage.ui_user_settings | ||||
|         if not user_settings: | ||||
|             continue | ||||
|         matching_stages.append(user_settings) | ||||
| @ -38,9 +38,7 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]: | ||||
|         user_settings = source.ui_user_settings | ||||
|         if not user_settings: | ||||
|             continue | ||||
|         policy_engine = PolicyEngine( | ||||
|             source.policies.all(), user, context.get("request") | ||||
|         ) | ||||
|         policy_engine = PolicyEngine(source, user, context.get("request")) | ||||
|         policy_engine.build() | ||||
|         if policy_engine.passing: | ||||
|             matching_sources.append(user_settings) | ||||
|  | ||||
| @ -8,8 +8,7 @@ class UIUserSettings: | ||||
|     """Dataclass for Stage and Source's user_settings""" | ||||
|  | ||||
|     name: str | ||||
|     icon: str | ||||
|     view_name: str | ||||
|     url: str | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
|  | ||||
| @ -20,42 +20,38 @@ def create_default_authentication_flow( | ||||
|     ) | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     if ( | ||||
|         Flow.objects.using(db_alias) | ||||
|         .filter(designation=FlowDesignation.AUTHENTICATION) | ||||
|         .exists() | ||||
|     ): | ||||
|         # Only create default flow when none exist | ||||
|         return | ||||
|     identification_stage, _ = IdentificationStage.objects.using( | ||||
|         db_alias | ||||
|     ).update_or_create( | ||||
|         name="default-authentication-identification", | ||||
|         defaults={ | ||||
|             "user_fields": [UserFields.E_MAIL, UserFields.USERNAME], | ||||
|             "template": Templates.DEFAULT_LOGIN, | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     if not IdentificationStage.objects.using(db_alias).exists(): | ||||
|         IdentificationStage.objects.using(db_alias).create( | ||||
|             name="identification", | ||||
|             user_fields=[UserFields.E_MAIL, UserFields.USERNAME], | ||||
|             template=Templates.DEFAULT_LOGIN, | ||||
|         ) | ||||
|     password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create( | ||||
|         name="default-authentication-password", | ||||
|         defaults={"backends": ["django.contrib.auth.backends.ModelBackend"]}, | ||||
|     ) | ||||
|  | ||||
|     if not PasswordStage.objects.using(db_alias).exists(): | ||||
|         PasswordStage.objects.using(db_alias).create( | ||||
|             name="password", backends=["django.contrib.auth.backends.ModelBackend"], | ||||
|         ) | ||||
|     login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create( | ||||
|         name="default-authentication-login" | ||||
|     ) | ||||
|  | ||||
|     if not UserLoginStage.objects.using(db_alias).exists(): | ||||
|         UserLoginStage.objects.using(db_alias).create(name="authentication") | ||||
|  | ||||
|     flow = Flow.objects.using(db_alias).create( | ||||
|         name="Welcome to passbook!", | ||||
|     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||
|         slug="default-authentication-flow", | ||||
|         designation=FlowDesignation.AUTHENTICATION, | ||||
|         defaults={"name": "Welcome to passbook!",}, | ||||
|     ) | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=IdentificationStage.objects.using(db_alias).first(), order=0, | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         flow=flow, stage=identification_stage, defaults={"order": 0,}, | ||||
|     ) | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=PasswordStage.objects.using(db_alias).first(), order=1, | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         flow=flow, stage=password_stage, defaults={"order": 1,}, | ||||
|     ) | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=UserLoginStage.objects.using(db_alias).first(), order=2, | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         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") | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     if ( | ||||
|         Flow.objects.using(db_alias) | ||||
|         .filter(designation=FlowDesignation.INVALIDATION) | ||||
|         .exists() | ||||
|     ): | ||||
|         # Only create default flow when none exist | ||||
|         return | ||||
|     UserLogoutStage.objects.using(db_alias).update_or_create( | ||||
|         name="default-invalidation-logout" | ||||
|     ) | ||||
|  | ||||
|     if not UserLogoutStage.objects.using(db_alias).exists(): | ||||
|         UserLogoutStage.objects.using(db_alias).create(name="logout") | ||||
|  | ||||
|     flow = Flow.objects.using(db_alias).create( | ||||
|         name="default-invalidation-flow", | ||||
|     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||
|         slug="default-invalidation-flow", | ||||
|         designation=FlowDesignation.INVALIDATION, | ||||
|         defaults={"name": "Logout",}, | ||||
|     ) | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=UserLogoutStage.objects.using(db_alias).first(), order=0, | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         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 | ||||
|  | ||||
|     # Create a policy that only allows this flow when doing an SSO Request | ||||
|     flow_policy = ExpressionPolicy.objects.using(db_alias).create( | ||||
|         name="default-source-enrollment-if-sso", expression=FLOW_POLICY_EXPRESSION | ||||
|     flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( | ||||
|         name="default-source-enrollment-if-sso", | ||||
|         defaults={"expression": FLOW_POLICY_EXPRESSION}, | ||||
|     ) | ||||
|  | ||||
|     # 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 | ||||
|     flow = Flow.objects.using(db_alias).create( | ||||
|         name="default-source-enrollment", | ||||
|     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||
|         slug="default-source-enrollment", | ||||
|         designation=FlowDesignation.ENROLLMENT, | ||||
|         defaults={"name": "Welcome to passbook!",}, | ||||
|     ) | ||||
|     PolicyBinding.objects.using(db_alias).create( | ||||
|         policy=flow_policy, target=flow, order=0 | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         policy=flow_policy, target=flow, defaults={"order": 0} | ||||
|     ) | ||||
|  | ||||
|     # 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", | ||||
|     ) | ||||
|     prompt_stage.fields.add( | ||||
|         Prompt.objects.using(db_alias).create( | ||||
|             field_key="username", | ||||
|             label="Username", | ||||
|             type=FieldTypes.TEXT, | ||||
|             required=True, | ||||
|             placeholder="Username", | ||||
|         ) | ||||
|     prompt, _ = Prompt.objects.using(db_alias).update_or_create( | ||||
|         field_key="username", | ||||
|         defaults={ | ||||
|             "label": "Username", | ||||
|             "type": FieldTypes.TEXT, | ||||
|             "required": True, | ||||
|             "placeholder": "Username", | ||||
|         }, | ||||
|     ) | ||||
|     prompt_stage.fields.add(prompt) | ||||
|  | ||||
|     # 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", | ||||
|         expression=PROMPT_POLICY_EXPRESSION, | ||||
|         defaults={"expression": PROMPT_POLICY_EXPRESSION}, | ||||
|     ) | ||||
|  | ||||
|     # 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" | ||||
|     ) | ||||
|     user_login = UserLoginStage.objects.using(db_alias).create( | ||||
|     user_login, _ = UserLoginStage.objects.using(db_alias).update_or_create( | ||||
|         name="default-source-enrollment-login" | ||||
|     ) | ||||
|  | ||||
|     binding = FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=prompt_stage, order=0 | ||||
|     binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         flow=flow, stage=prompt_stage, defaults={"order": 0} | ||||
|     ) | ||||
|     PolicyBinding.objects.using(db_alias).create( | ||||
|         policy=prompt_policy, target=binding, order=0 | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         policy=prompt_policy, target=binding, defaults={"order": 0} | ||||
|     ) | ||||
|  | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=user_write, order=1 | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         flow=flow, stage=user_write, defaults={"order": 1} | ||||
|     ) | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=user_login, order=2 | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         flow=flow, stage=user_login, defaults={"order": 2} | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @ -107,25 +110,26 @@ def create_default_source_authentication_flow( | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     # Create a policy that only allows this flow when doing an SSO Request | ||||
|     flow_policy = ExpressionPolicy.objects.using(db_alias).create( | ||||
|         name="default-source-authentication-if-sso", expression=FLOW_POLICY_EXPRESSION | ||||
|     flow_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( | ||||
|         name="default-source-authentication-if-sso", | ||||
|         defaults={"expression": FLOW_POLICY_EXPRESSION,}, | ||||
|     ) | ||||
|  | ||||
|     # This creates a Flow used by sources to authenticate users | ||||
|     flow = Flow.objects.using(db_alias).create( | ||||
|         name="default-source-authentication", | ||||
|     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||
|         slug="default-source-authentication", | ||||
|         designation=FlowDesignation.AUTHENTICATION, | ||||
|         defaults={"name": "Welcome to passbook!",}, | ||||
|     ) | ||||
|     PolicyBinding.objects.using(db_alias).create( | ||||
|         policy=flow_policy, target=flow, order=0 | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         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" | ||||
|     ) | ||||
|     FlowStageBinding.objects.using(db_alias).create( | ||||
|         flow=flow, stage=user_login, order=0 | ||||
|     FlowStageBinding.objects.using(db_alias).update_or_create( | ||||
|         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 | ||||
|  | ||||
|  | ||||
| def create_default_provider_authz_flow( | ||||
| def create_default_provider_authorization_flow( | ||||
|     apps: Apps, schema_editor: BaseDatabaseSchemaEditor | ||||
| ): | ||||
|     Flow = apps.get_model("passbook_flows", "Flow") | ||||
| @ -18,22 +18,24 @@ def create_default_provider_authz_flow( | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     # Empty flow for providers where consent is implicitly given | ||||
|     Flow.objects.using(db_alias).create( | ||||
|         name="Authorize Application", | ||||
|     Flow.objects.using(db_alias).update_or_create( | ||||
|         slug="default-provider-authorization-implicit-consent", | ||||
|         designation=FlowDesignation.AUTHORIZATION, | ||||
|         defaults={"name": "Authorize Application"}, | ||||
|     ) | ||||
|  | ||||
|     # Flow with consent form to obtain explicit user consent | ||||
|     flow = Flow.objects.using(db_alias).create( | ||||
|         name="Authorize Application", | ||||
|     flow, _ = Flow.objects.using(db_alias).update_or_create( | ||||
|         slug="default-provider-authorization-explicit-consent", | ||||
|         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" | ||||
|     ) | ||||
|     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): | ||||
| @ -43,4 +45,4 @@ class Migration(migrations.Migration): | ||||
|         ("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 model_utils.managers import InheritanceManager | ||||
| from structlog import get_logger | ||||
| from django.template.context import RequestContext | ||||
|  | ||||
| from passbook.core.types import UIUserSettings | ||||
| from passbook.lib.utils.reflection import class_to_path | ||||
| @ -33,7 +32,7 @@ class FlowDesignation(models.TextChoices): | ||||
|     ENROLLMENT = "enrollment" | ||||
|     UNRENOLLMENT = "unenrollment" | ||||
|     RECOVERY = "recovery" | ||||
|     USER_SETTINGS = "user_settings" | ||||
|     STAGE_SETUP = "stage_setup" | ||||
|  | ||||
|  | ||||
| class Stage(models.Model): | ||||
| @ -48,8 +47,8 @@ class Stage(models.Model): | ||||
|     type = "" | ||||
|     form = "" | ||||
|  | ||||
|     @staticmethod | ||||
|     def ui_user_settings(context: RequestContext) -> Optional[UIUserSettings]: | ||||
|     @property | ||||
|     def ui_user_settings(self) -> Optional[UIUserSettings]: | ||||
|         """Entrypoint to integrate with User settings. Can either return None if no | ||||
|         user settings are available, or an instanace of UIUserSettings.""" | ||||
|         return None | ||||
|  | ||||
| @ -13,6 +13,7 @@ def delete_cache_prefix(prefix: str) -> int: | ||||
|     cache.delete_many(keys) | ||||
|     return len(keys) | ||||
|  | ||||
|  | ||||
| @receiver(post_save) | ||||
| # pylint: disable=unused-argument | ||||
| 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) | ||||
|     if isinstance(instance, FlowStageBinding): | ||||
|         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): | ||||
|         total = 0 | ||||
|         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 structlog import get_logger | ||||
|  | ||||
| from passbook.audit.models import cleanse_dict | ||||
| from passbook.core.views.utils import PermissionDeniedView | ||||
| from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from passbook.flows.models import Flow, FlowDesignation, Stage | ||||
| @ -161,7 +162,7 @@ class FlowExecutorView(View): | ||||
|         LOGGER.debug( | ||||
|             "f(exec): User passed all stages", | ||||
|             flow_slug=self.flow.slug, | ||||
|             context=self.plan.context, | ||||
|             context=cleanse_dict(self.plan.context), | ||||
|         ) | ||||
|         return self._flow_done() | ||||
|  | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """OAuth Client models""" | ||||
| from typing import Optional | ||||
|  | ||||
| from django.db import models | ||||
| from django.urls import reverse, reverse_lazy | ||||
| @ -61,16 +62,10 @@ class OAuthSource(Source): | ||||
|         return f"Callback URL: <pre>{url}</pre>" | ||||
|  | ||||
|     @property | ||||
|     def ui_user_settings(self) -> UIUserSettings: | ||||
|         icon_type = self.provider_type | ||||
|         if icon_type == "azure ad": | ||||
|             icon_type = "windows" | ||||
|         icon_class = f"fab fa-{icon_type}" | ||||
|     def ui_user_settings(self) -> Optional[UIUserSettings]: | ||||
|         view_name = "passbook_sources_oauth:oauth-client-user" | ||||
|         return UIUserSettings( | ||||
|             name=self.name, | ||||
|             icon=icon_class, | ||||
|             view_name=reverse((view_name), kwargs={"source_slug": self.slug}), | ||||
|             name=self.name, url=reverse(view_name, kwargs={"source_slug": self.slug}), | ||||
|         ) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|  | ||||
| @ -153,7 +153,7 @@ class Processor: | ||||
|         self, request: HttpRequest, flow: Flow, **kwargs | ||||
|     ) -> HttpResponse: | ||||
|         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( | ||||
|             "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 structlog import get_logger | ||||
|  | ||||
| from passbook.flows.models import Flow, FlowDesignation | ||||
| from passbook.lib.utils.ui import human_list | ||||
| from passbook.stages.identification.models import IdentificationStage, UserFields | ||||
|  | ||||
| @ -14,6 +15,15 @@ LOGGER = get_logger() | ||||
| class IdentificationStageForm(forms.ModelForm): | ||||
|     """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: | ||||
|  | ||||
|         model = IdentificationStage | ||||
|  | ||||
| @ -8,3 +8,4 @@ class PassbookStagePasswordConfig(AppConfig): | ||||
|     name = "passbook.stages.password" | ||||
|     label = "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.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from passbook.flows.models import Flow, FlowDesignation | ||||
| from passbook.stages.password.models import PasswordStage | ||||
|  | ||||
|  | ||||
| @ -40,14 +41,19 @@ class PasswordForm(forms.Form): | ||||
| class PasswordStageForm(forms.ModelForm): | ||||
|     """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: | ||||
|  | ||||
|         model = PasswordStage | ||||
|         fields = ["name", "backends"] | ||||
|         fields = ["name", "backends", "change_flow"] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "backends": FilteredSelectMultiple( | ||||
|                 _("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""" | ||||
| from typing import Optional | ||||
|  | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| 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 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): | ||||
| @ -14,9 +20,32 @@ class PasswordStage(Stage): | ||||
|         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" | ||||
|     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): | ||||
|         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""" | ||||
| 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 passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan | ||||
| @ -16,6 +18,7 @@ class PromptStageForm(forms.ModelForm): | ||||
|         fields = ["name", "fields"] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "fields": FilteredSelectMultiple(_("prompts"), False), | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| """Write stage logic""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.auth import update_session_auth_hash | ||||
| from django.contrib.auth.backends import ModelBackend | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.utils.translation import gettext as _ | ||||
| @ -48,6 +49,10 @@ class UserWriteStageView(StageView): | ||||
|             else: | ||||
|                 user.attributes[key] = value | ||||
|         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( | ||||
|             "Updated existing user", user=user, flow_slug=self.executor.flow.slug, | ||||
|         ) | ||||
|  | ||||
| @ -5177,7 +5177,7 @@ definitions: | ||||
|           - enrollment | ||||
|           - unenrollment | ||||
|           - recovery | ||||
|           - user_settings | ||||
|           - stage_setup | ||||
|       stages: | ||||
|         type: array | ||||
|         items: | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer