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