diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14c689911..1d3144371 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,10 +121,28 @@ jobs: - uses: actions/setup-python@v1 with: python-version: '3.8' + - uses: actions/setup-node@v1 + with: + node-version: '12' - name: Install dependencies - run: sudo pip install -U wheel pipenv && pipenv install --dev + run: | + sudo pip install -U wheel pipenv + pipenv install --dev + - name: Prepare Chrome node + run: | + cd e2e + docker-compose pull -q chrome + docker-compose up -d chrome + - name: Build static files for e2e test + run: | + cd passbook/static/static + yarn - name: Run coverage - run: pipenv run ./scripts/coverage.sh + run: pipenv run coverage run ./manage.py test --failfast + - uses: actions/upload-artifact@v2 + if: failure() + with: + path: out/ - name: Create XML Report run: pipenv run coverage xml - uses: codecov/codecov-action@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c96c84dac..8a9a9ff7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,8 +82,7 @@ jobs: - uses: actions/checkout@v1 - name: Run test suite in final docker images run: | - export PASSBOOK_DOMAIN=localhost - docker-compose pull + docker-compose pull -q docker-compose up --no-start docker-compose start postgresql redis docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test" diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index a65546dfb..8497e400e 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -13,8 +13,7 @@ jobs: - uses: actions/checkout@master - name: Pre-release test run: | - export PASSBOOK_DOMAIN=localhost - docker-compose pull + docker-compose pull -q docker build \ --no-cache \ -t beryju/passbook:latest \ diff --git a/Pipfile b/Pipfile index 5a6c688d9..cad4fd1b6 100644 --- a/Pipfile +++ b/Pipfile @@ -40,6 +40,7 @@ signxml = "*" structlog = "*" swagger-spec-validator = "*" urllib3 = {extras = ["secure"],version = "*"} +facebook-sdk = "*" [requires] python_version = "3.8" @@ -55,6 +56,8 @@ pylint = "*" pylint-django = "*" unittest-xml-reporting = "*" black = "*" +selenium = "*" +docker = "*" [pipenv] allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index b695d1b6e..ec8ab4b7e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "541f26a45f249fb2e61a597af7be7dee51eb8b40aa1035ae4081a455168128cc" + "sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b" }, "pipfile-spec": 6, "requires": { @@ -306,6 +306,14 @@ ], "version": "==1.0.0" }, + "facebook-sdk": { + "hashes": [ + "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e", + "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d" + ], + "index": "pypi", + "version": "==3.1.0" + }, "future": { "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" @@ -871,6 +879,53 @@ "index": "pypi", "version": "==0.6.0" }, + "certifi": { + "hashes": [ + "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", + "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" + ], + "version": "==2020.4.5.2" + }, + "cffi": { + "hashes": [ + "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", + "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", + "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", + "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", + "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", + "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", + "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", + "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", + "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", + "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", + "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", + "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", + "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", + "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", + "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", + "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", + "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", + "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", + "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", + "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", + "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", + "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", + "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", + "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", + "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", + "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", + "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", + "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" + ], + "version": "==1.14.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", @@ -923,6 +978,30 @@ "index": "pypi", "version": "==5.1" }, + "cryptography": { + "hashes": [ + "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", + "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", + "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", + "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", + "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", + "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", + "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", + "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", + "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", + "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", + "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", + "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", + "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", + "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", + "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", + "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", + "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", + "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", + "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" + ], + "version": "==2.9.2" + }, "django": { "hashes": [ "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2", @@ -939,6 +1018,14 @@ "index": "pypi", "version": "==2.2" }, + "docker": { + "hashes": [ + "sha256:380a20d38fbfaa872e96ee4d0d23ad9beb0f9ed57ff1c30653cbeb0c9c0964f2", + "sha256:672f51aead26d90d1cfce84a87e6f71fca401bbc2a6287be18603583620a28ba" + ], + "index": "pypi", + "version": "==4.2.1" + }, "gitdb": { "hashes": [ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", @@ -953,6 +1040,13 @@ ], "version": "==3.1.3" }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -1014,6 +1108,13 @@ ], "version": "==2.6.0" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, "pylint": { "hashes": [ "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", @@ -1037,6 +1138,13 @@ ], "version": "==0.6" }, + "pyopenssl": { + "hashes": [ + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" + ], + "version": "==19.1.0" + }, "pytz": { "hashes": [ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", @@ -1087,6 +1195,21 @@ ], "version": "==2020.6.8" }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "version": "==2.24.0" + }, + "selenium": { + "hashes": [ + "sha256:5f5489a0c5fe2f09cc6bc3f32a0d53441ab36882c987269f2afe805979633ac1", + "sha256:a9779ddc69cf03b75d94062c5e948f763919cf3341c77272f94cd05e6b4c7b32" + ], + "index": "pypi", + "version": "==4.0.0a6.post2" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -1156,6 +1279,25 @@ "index": "pypi", "version": "==3.0.2" }, + "urllib3": { + "extras": [ + "secure" + ], + "hashes": [ + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + ], + "index": "pypi", + "markers": null, + "version": "==1.25.9" + }, + "websocket-client": { + "hashes": [ + "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549", + "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010" + ], + "version": "==0.57.0" + }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" diff --git a/docker.env.yml b/docker.env.yml new file mode 100644 index 000000000..f8961f7ef --- /dev/null +++ b/docker.env.yml @@ -0,0 +1,9 @@ +debug: true +postgresql: + user: postgres + host: postgresql + +redis: + host: redis + +log_level: debug diff --git a/e2e/__init__.py b/e2e/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 000000000..1786d5c5e --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.7' + +services: + chrome: + image: selenium/standalone-chrome-debug:3.141.59-20200525 + volumes: + - /dev/shm:/dev/shm + network_mode: host + + postgresql: + image: postgres:11 + restart: always + environment: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: passbook + network_mode: host + redis: + image: redis + restart: always + network_mode: host diff --git a/e2e/passbook.side b/e2e/passbook.side new file mode 100644 index 000000000..af2abbb3c --- /dev/null +++ b/e2e/passbook.side @@ -0,0 +1,300 @@ +{ + "id": "7d9b2407-1520-4c04-b040-68e8ada9aecc", + "version": "2.0", + "name": "passbook", + "url": "http://localhost:8000", + "tests": [{ + "id": "94b39863-74ec-4b7d-98c5-2b380b6d2c55", + "name": "passbook login simple", + "commands": [{ + "id": "e60e4382-4f96-44c3-ba06-5e18609c9c2b", + "comment": "", + "command": "open", + "target": "/flows/default-authentication-flow/?next=%2F", + "targets": [], + "value": "" + }, { + "id": "b2652f24-931e-45b0-b01d-2f0ac0f74db8", + "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": "f1930f8a-984a-4076-a925-20937bb2f8d3", + "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": "admin@example.tld" + }, { + "id": "0b568ee3-1bed-4821-a3bc-f6b960dbed9d", + "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": "6d98e479-2825-484d-996a-ccf350d2761f", + "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": "6f7abec6-ff44-4eb5-ae23-520c1c29a706", + "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": "04c5876f-1405-4077-a98b-e911f09113d7", + "comment": "", + "command": "assertText", + "target": "xpath=//a[contains(@href, '/-/user/')]", + "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": "pbadmin" + }] + }, { + "id": "61948b3c-3012-4f97-aa52-bc8f34fec333", + "name": "passbook enroll simple", + "commands": [{ + "id": "0f4884b3-4891-41bc-956d-1fa433e892e9", + "comment": "", + "command": "open", + "target": "/flows/default-authentication-flow/?next=%2F", + "targets": [], + "value": "" + }, { + "id": "84d3861f-a60c-4650-8689-535f82b39577", + "comment": "", + "command": "click", + "target": "linkText=Sign up.", + "targets": [ + ["linkText=Sign up.", "linkText"], + ["css=.pf-c-login__main-footer-band-item > a", "css:finder"], + ["xpath=//a[contains(text(),'Sign up.')]", "xpath:link"], + ["xpath=//main[@id='flow-body']/footer/div/p/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/flows/default-enrollment-flow/')]", "xpath:href"], + ["xpath=//a", "xpath:position"], + ["xpath=//a[contains(.,'Sign up.')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a32435ca-d84a-41e7-a915-fcbbc5f88341", + "comment": "", + "command": "type", + "target": "id=id_username", + "targets": [ + ["id=id_username", "id"], + ["name=username", "name"], + ["css=#id_username", "css:finder"], + ["xpath=//input[@id='id_username']", "xpath:attributes"], + ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], + ["xpath=//div/input", "xpath:position"] + ], + "value": "foo" + }, { + "id": "3b5dcf53-8297-46c5-88b7-11c2eb25f34f", + "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": "e948d61c-dae6-4994-b56f-ff130892b342", + "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[3]/input", "xpath:idRelative"], + ["xpath=//div[3]/input", "xpath:position"] + ], + "value": "pbadmin" + }, { + "id": "e7527bfc-ec74-4d96-86f0-5a3a55a59025", + "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[4]/button", "xpath:idRelative"], + ["xpath=//button", "xpath:position"], + ["xpath=//button[contains(.,'Continue')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "434b842c-a659-4ff5-aca8-06a6a3489597", + "comment": "", + "command": "type", + "target": "id=id_name", + "targets": [ + ["id=id_name", "id"], + ["name=name", "name"], + ["css=#id_name", "css:finder"], + ["xpath=//input[@id='id_name']", "xpath:attributes"], + ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"], + ["xpath=//div/input", "xpath:position"] + ], + "value": "some name" + }, { + "id": "cbc43a1b-2cfe-46e2-85bc-476fb32c6cb1", + "comment": "", + "command": "type", + "target": "id=id_email", + "targets": [ + ["id=id_email", "id"], + ["name=email", "name"], + ["css=#id_email", "css:finder"], + ["xpath=//input[@id='id_email']", "xpath:attributes"], + ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"], + ["xpath=//div[2]/input", "xpath:position"] + ], + "value": "foo@bar.baz" + }, { + "id": "e74389a0-228b-4312-9677-e9add6358de3", + "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": "" + }, { + "id": "3e22f9c2-5ebd-49c2-81b1-340fa0435bbc", + "comment": "", + "command": "click", + "target": "linkText=foo", + "targets": [ + ["linkText=foo", "linkText"], + ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"], + ["xpath=//a[contains(text(),'foo')]", "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(.,'foo')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "60124cfd-f11c-4d7f-8b01-bef54c8cbd73", + "comment": "", + "command": "assertText", + "target": "xpath=//a[contains(@href, '/-/user/')]", + "targets": [ + ["linkText=foo", "linkText"], + ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"], + ["xpath=//a[contains(text(),'foo')]", "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(.,'foo')]", "xpath:innerText"] + ], + "value": "foo" + }, { + "id": "429ee61b-9991-4919-8131-55f8e1bd9a0d", + "comment": "", + "command": "assertValue", + "target": "id=id_username", + "targets": [], + "value": "foo" + }, { + "id": "f6c50760-52ed-4c1d-b232-30f8afe144eb", + "comment": "", + "command": "assertText", + "target": "id=id_name", + "targets": [ + ["id=id_name", "id"], + ["name=name", "name"], + ["css=#id_name", "css:finder"], + ["xpath=//input[@id='id_name']", "xpath:attributes"], + ["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[2]/div/input", "xpath:idRelative"], + ["xpath=//div[2]/div/input", "xpath:position"] + ], + "value": "some name" + }, { + "id": "b26905b5-89b5-4b41-abf5-a9f848f08622", + "comment": "", + "command": "assertText", + "target": "id=id_email", + "targets": [ + ["id=id_email", "id"], + ["name=email", "name"], + ["css=#id_email", "css:finder"], + ["xpath=//input[@id='id_email']", "xpath:attributes"], + ["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[3]/div/input", "xpath:idRelative"], + ["xpath=//div[3]/div/input", "xpath:position"] + ], + "value": "foo@bar.baz" + }] + }], + "suites": [{ + "id": "495657fb-3f5e-4431-877c-4d0b248c0841", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["94b39863-74ec-4b7d-98c5-2b380b6d2c55"] + }], + "urls": ["http://localhost:8000/"], + "plugins": [] +} \ No newline at end of file diff --git a/e2e/setup.sh b/e2e/setup.sh new file mode 100755 index 000000000..99c795e1f --- /dev/null +++ b/e2e/setup.sh @@ -0,0 +1,20 @@ +#!/bin/bash -x +# Setup docker & compose +curl -fsSL https://get.docker.com | bash +sudo usermod -a -G docker ubuntu +sudo curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose +# Setup nodejs +curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - +sudo apt-get install -y nodejs +sudo npm install -g yarn +# Setup python +sudo apt install -y python3.8 python3-pip +# Setup docker +sudo pip3 install pipenv + +cd e2e +sudo docker-compose up -d +cd .. +pipenv sync --dev +pipenv shell diff --git a/e2e/test_enroll.py b/e2e/test_enroll.py new file mode 100644 index 000000000..800fac47f --- /dev/null +++ b/e2e/test_enroll.py @@ -0,0 +1,476 @@ +"""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.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", + ) diff --git a/e2e/test_login_default.py b/e2e/test_login_default.py new file mode 100644 index 000000000..5b0d8bcad --- /dev/null +++ b/e2e/test_login_default.py @@ -0,0 +1,22 @@ +"""test default login flow""" +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys + +from e2e.utils import USER, SeleniumTestCase + + +class TestLogin(SeleniumTestCase): + """test default login flow""" + + def test_login(self): + """test default login flow""" + self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/") + self.driver.find_element(By.ID, "id_uid_field").click() + 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.assertEqual( + self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text, + USER().username, + ) diff --git a/e2e/test_provider_oauth.py b/e2e/test_provider_oauth.py new file mode 100644 index 000000000..7f4225480 --- /dev/null +++ b/e2e/test_provider_oauth.py @@ -0,0 +1,191 @@ +"""test OAuth Provider flow""" +from time import sleep + +from oauth2_provider.generators import generate_client_id, generate_client_secret +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys + +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.core.models import Application +from passbook.flows.models import Flow +from passbook.providers.oauth.models import OAuth2Provider + + +class TestProviderOAuth(SeleniumTestCase): + """test OAuth Provider flow""" + + def setUp(self): + super().setUp() + self.client_id = generate_client_id() + self.client_secret = generate_client_secret() + self.container = self.setup_client() + + def setup_client(self) -> Container: + """Setup client grafana container which we test OAuth against""" + client: DockerClient = from_env() + container = client.containers.run( + image="grafana/grafana:latest", + detach=True, + network_mode="host", + auto_remove=True, + healthcheck=Healthcheck( + test=["CMD", "wget", "--spider", "http://localhost:3000"], + interval=5 * 100 * 1000000, + start_period=1 * 100 * 1000000, + ), + environment={ + "GF_AUTH_GITHUB_ENABLED": "true", + "GF_AUTH_GITHUB_allow_sign_up": "true", + "GF_AUTH_GITHUB_CLIENT_ID": self.client_id, + "GF_AUTH_GITHUB_CLIENT_SECRET": self.client_secret, + "GF_AUTH_GITHUB_SCOPES": "user:email,read:org", + "GF_AUTH_GITHUB_AUTH_URL": self.url( + "passbook_providers_oauth:github-authorize" + ), + "GF_AUTH_GITHUB_TOKEN_URL": self.url( + "passbook_providers_oauth:github-access-token" + ), + "GF_AUTH_GITHUB_API_URL": self.url( + "passbook_providers_oauth:github-user" + ), + "GF_LOG_LEVEL": "debug", + }, + ) + 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_authorization_consent_implied(self): + """test OAuth Provider flow (default authorization flow with implied consent)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + provider = OAuth2Provider.objects.create( + name="grafana", + client_type=OAuth2Provider.CLIENT_CONFIDENTIAL, + authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE, + client_id=self.client_id, + client_secret=self.client_secret, + redirect_uris="http://localhost:3000/login/github", + skip_authorization=True, + authorization_flow=authorization_flow, + ) + Application.objects.create( + name="Grafana", slug="grafana", provider=provider, + ) + + self.driver.get("http://localhost:3000") + self.driver.find_element(By.CLASS_NAME, "btn-service--github").click() + self.driver.find_element(By.ID, "id_uid_field").click() + 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.XPATH, "//a[contains(@href, '/profile')]").click() + self.assertEqual( + self.driver.find_element(By.CLASS_NAME, "page-header__title").text, + USER().username, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input", + ).get_attribute("value"), + USER().username, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input", + ).get_attribute("value"), + USER().email, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input", + ).get_attribute("value"), + USER().username, + ) + + def test_authorization_consent_explicit(self): + """test OAuth Provider flow (default authorization flow with explicit consent)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-explicit-consent" + ) + provider = OAuth2Provider.objects.create( + name="grafana", + client_type=OAuth2Provider.CLIENT_CONFIDENTIAL, + authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE, + client_id=self.client_id, + client_secret=self.client_secret, + redirect_uris="http://localhost:3000/login/github", + skip_authorization=True, + authorization_flow=authorization_flow, + ) + app = Application.objects.create( + name="Grafana", slug="grafana", provider=provider, + ) + + self.driver.get("http://localhost:3000") + self.driver.find_element(By.CLASS_NAME, "btn-service--github").click() + self.driver.find_element(By.ID, "id_uid_field").click() + 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.assertIn( + app.name, + self.driver.find_element( + By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]" + ).text, + ) + self.assertEqual( + "GitHub Compatibility: User Email", + self.driver.find_element( + By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]" + ).text, + ) + self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() + + self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() + self.assertEqual( + self.driver.find_element(By.CLASS_NAME, "page-header__title").text, + USER().username, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input", + ).get_attribute("value"), + USER().username, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input", + ).get_attribute("value"), + USER().email, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input", + ).get_attribute("value"), + USER().username, + ) diff --git a/e2e/test_provider_oidc.py b/e2e/test_provider_oidc.py new file mode 100644 index 000000000..3c44b9544 --- /dev/null +++ b/e2e/test_provider_oidc.py @@ -0,0 +1,254 @@ +"""test OpenID Provider flow""" +from time import sleep + +from django.shortcuts import reverse +from oauth2_provider.generators import generate_client_id, generate_client_secret +from oidc_provider.models import Client, ResponseType +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, ensure_rsa_key +from passbook.core.models import Application +from passbook.flows.models import Flow +from passbook.providers.oidc.models import OpenIDProvider + + +class TestProviderOIDC(SeleniumTestCase): + """test OpenID Provider flow""" + + def setUp(self): + super().setUp() + self.client_id = generate_client_id() + self.client_secret = generate_client_secret() + self.container = self.setup_client() + + def setup_client(self) -> Container: + """Setup client grafana container which we test OIDC against""" + client: DockerClient = from_env() + container = client.containers.run( + image="grafana/grafana:latest", + detach=True, + network_mode="host", + auto_remove=True, + healthcheck=Healthcheck( + test=["CMD", "wget", "--spider", "http://localhost:3000"], + interval=5 * 100 * 1000000, + start_period=1 * 100 * 1000000, + ), + environment={ + "GF_AUTH_GENERIC_OAUTH_ENABLED": "true", + "GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id, + "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret, + "GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile", + "GF_AUTH_GENERIC_OAUTH_AUTH_URL": ( + self.live_server_url + reverse("passbook_providers_oidc:authorize") + ), + "GF_AUTH_GENERIC_OAUTH_TOKEN_URL": ( + self.live_server_url + reverse("oidc_provider:token") + ), + "GF_AUTH_GENERIC_OAUTH_API_URL": ( + self.live_server_url + reverse("oidc_provider:userinfo") + ), + "GF_LOG_LEVEL": "debug", + }, + ) + 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_redirect_uri_error(self): + """test OpenID Provider flow (invalid redirect URI, check error message)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + client = Client.objects.create( + name="grafana", + client_type="confidential", + client_id=self.client_id, + client_secret=self.client_secret, + _redirect_uris="http://localhost:3000/", + _scope="openid userinfo", + ) + # At least one of these objects must exist + ensure_rsa_key() + # This response_code object might exist or not, depending on the order the tests are run + rp_type, _ = ResponseType.objects.get_or_create(value="code") + client.response_types.set([rp_type]) + client.save() + provider = OpenIDProvider.objects.create( + oidc_client=client, authorization_flow=authorization_flow, + ) + Application.objects.create( + name="Grafana", slug="grafana", provider=provider, + ) + + self.driver.get("http://localhost:3000") + self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() + self.driver.find_element(By.ID, "id_uid_field").click() + 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) + sleep(2) + self.assertEqual( + self.driver.find_element(By.CLASS_NAME, "pf-c-title").text, + "Redirect URI Error", + ) + + def test_authorization_consent_implied(self): + """test OpenID Provider flow (default authorization flow with implied consent)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + client = Client.objects.create( + name="grafana", + client_type="confidential", + client_id=self.client_id, + client_secret=self.client_secret, + _redirect_uris="http://localhost:3000/login/generic_oauth", + _scope="openid profile email", + reuse_consent=False, + require_consent=False, + ) + # At least one of these objects must exist + ensure_rsa_key() + # This response_code object might exist or not, depending on the order the tests are run + rp_type, _ = ResponseType.objects.get_or_create(value="code") + client.response_types.set([rp_type]) + client.save() + provider = OpenIDProvider.objects.create( + oidc_client=client, authorization_flow=authorization_flow, + ) + Application.objects.create( + name="Grafana", slug="grafana", provider=provider, + ) + + self.driver.get("http://localhost:3000") + self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() + self.driver.find_element(By.ID, "id_uid_field").click() + 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.XPATH, "//a[contains(@href, '/profile')]").click() + self.assertEqual( + self.driver.find_element(By.CLASS_NAME, "page-header__title").text, + USER().name, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input", + ).get_attribute("value"), + USER().name, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input", + ).get_attribute("value"), + USER().email, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input", + ).get_attribute("value"), + USER().email, + ) + + def test_authorization_consent_explicit(self): + """test OpenID Provider flow (default authorization flow with explicit consent)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-explicit-consent" + ) + client = Client.objects.create( + name="grafana", + client_type="confidential", + client_id=self.client_id, + client_secret=self.client_secret, + _redirect_uris="http://localhost:3000/login/generic_oauth", + _scope="openid profile email", + reuse_consent=False, + require_consent=False, + ) + # At least one of these objects must exist + ensure_rsa_key() + # This response_code object might exist or not, depending on the order the tests are run + rp_type, _ = ResponseType.objects.get_or_create(value="code") + client.response_types.set([rp_type]) + client.save() + provider = OpenIDProvider.objects.create( + oidc_client=client, authorization_flow=authorization_flow, + ) + app = Application.objects.create( + name="Grafana", slug="grafana", provider=provider, + ) + + self.driver.get("http://localhost:3000") + self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click() + self.driver.find_element(By.ID, "id_uid_field").click() + 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.assertIn( + app.name, + self.driver.find_element( + By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]" + ).text, + ) + self.wait.until( + ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]")) + ) + self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() + + self.wait.until( + ec.presence_of_element_located( + (By.XPATH, "//a[contains(@href, '/profile')]") + ) + ) + self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click() + self.assertEqual( + self.driver.find_element(By.CLASS_NAME, "page-header__title").text, + USER().name, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input", + ).get_attribute("value"), + USER().name, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input", + ).get_attribute("value"), + USER().email, + ) + self.assertEqual( + self.driver.find_element( + By.XPATH, + "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input", + ).get_attribute("value"), + USER().email, + ) diff --git a/e2e/test_provider_saml.py b/e2e/test_provider_saml.py new file mode 100644 index 000000000..790a0ec37 --- /dev/null +++ b/e2e/test_provider_saml.py @@ -0,0 +1,172 @@ +"""test SAML Provider flow""" +from time import sleep + +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys + +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.core.models import Application +from passbook.crypto.models import CertificateKeyPair +from passbook.flows.models import Flow +from passbook.lib.utils.reflection import class_to_path +from passbook.providers.saml.models import ( + SAMLBindings, + SAMLPropertyMapping, + SAMLProvider, +) +from passbook.providers.saml.processors.generic import GenericProcessor + + +class TestProviderSAML(SeleniumTestCase): + """test SAML Provider flow""" + + container: Container + + def setup_client(self, provider: SAMLProvider) -> Container: + """Setup client saml-sp container which we test SAML against""" + client: DockerClient = from_env() + container = client.containers.run( + image="beryju/saml-test-sp", + detach=True, + network_mode="host", + auto_remove=True, + healthcheck=Healthcheck( + test=["CMD", "wget", "--spider", "http://localhost:9009/health"], + interval=5 * 100 * 1000000, + start_period=1 * 100 * 1000000, + ), + environment={ + "SP_ENTITY_ID": provider.issuer, + "SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + "SP_METADATA_URL": ( + self.url( + "passbook_providers_saml:metadata", + application_slug=provider.application.slug, + ) + ), + }, + ) + 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_sp_initiated_implicit(self): + """test SAML Provider flow SP-initiated flow (implicit consent)""" + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + provider: SAMLProvider = SAMLProvider.objects.create( + name="saml-test", + processor_path=class_to_path(GenericProcessor), + acs_url="http://localhost:9009/saml/acs", + audience="passbook-e2e", + issuer="passbook-e2e", + sp_binding=SAMLBindings.POST, + authorization_flow=authorization_flow, + signing_kp=CertificateKeyPair.objects.first(), + ) + provider.property_mappings.set(SAMLPropertyMapping.objects.all()) + provider.save() + Application.objects.create( + name="SAML", slug="passbook-saml", provider=provider, + ) + self.container = self.setup_client(provider) + self.driver.get("http://localhost:9009") + self.driver.find_element(By.ID, "id_uid_field").click() + 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.assertEqual( + self.driver.find_element(By.XPATH, "/html/body/pre").text, + f"Hello, {USER().name}!", + ) + + def test_sp_initiated_explicit(self): + """test SAML Provider flow SP-initiated flow (explicit consent)""" + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-explicit-consent" + ) + provider: SAMLProvider = SAMLProvider.objects.create( + name="saml-test", + processor_path=class_to_path(GenericProcessor), + acs_url="http://localhost:9009/saml/acs", + audience="passbook-e2e", + issuer="passbook-e2e", + sp_binding=SAMLBindings.POST, + authorization_flow=authorization_flow, + signing_kp=CertificateKeyPair.objects.first(), + ) + provider.property_mappings.set(SAMLPropertyMapping.objects.all()) + provider.save() + app = Application.objects.create( + name="SAML", slug="passbook-saml", provider=provider, + ) + self.container = self.setup_client(provider) + self.driver.get("http://localhost:9009") + self.driver.find_element(By.ID, "id_uid_field").click() + 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.assertIn( + app.name, + self.driver.find_element( + By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]" + ).text, + ) + self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click() + self.assertEqual( + self.driver.find_element(By.XPATH, "/html/body/pre").text, + f"Hello, {USER().name}!", + ) + + def test_idp_initiated_implicit(self): + """test SAML Provider flow IdP-initiated flow (implicit consent)""" + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + provider: SAMLProvider = SAMLProvider.objects.create( + name="saml-test", + processor_path=class_to_path(GenericProcessor), + acs_url="http://localhost:9009/saml/acs", + audience="passbook-e2e", + issuer="passbook-e2e", + sp_binding=SAMLBindings.POST, + authorization_flow=authorization_flow, + signing_kp=CertificateKeyPair.objects.first(), + ) + provider.property_mappings.set(SAMLPropertyMapping.objects.all()) + provider.save() + Application.objects.create( + name="SAML", slug="passbook-saml", provider=provider, + ) + self.container = self.setup_client(provider) + self.driver.get( + self.url( + "passbook_providers_saml:sso-init", + application_slug=provider.application.slug, + ) + ) + self.driver.find_element(By.ID, "id_uid_field").click() + 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.assertEqual( + self.driver.find_element(By.XPATH, "/html/body/pre").text, + f"Hello, {USER().name}!", + ) diff --git a/e2e/test_source_saml.py b/e2e/test_source_saml.py new file mode 100644 index 000000000..78591d850 --- /dev/null +++ b/e2e/test_source_saml.py @@ -0,0 +1,127 @@ +"""test SAML Source""" +from time import sleep + +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 SeleniumTestCase +from passbook.crypto.models import CertificateKeyPair +from passbook.flows.models import Flow +from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource + +IDP_CERT = """-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav +Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+ +YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc ++TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix +YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8 +jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C +YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b +lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs +X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7 +yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7 +NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG +99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n +aQ== +-----END CERTIFICATE-----""" + + +class TestSourceSAML(SeleniumTestCase): + """test SAML Source 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="kristophjunge/test-saml-idp", + detach=True, + network_mode="host", + auto_remove=True, + healthcheck=Healthcheck( + test=["CMD", "curl", "http://localhost:8080"], + interval=5 * 100 * 1000000, + start_period=1 * 100 * 1000000, + ), + environment={ + "SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id", + "SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": ( + f"{self.live_server_url}/source/saml/saml-idp-test/acs/" + ), + }, + ) + 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_idp_redirect(self): + """test SAML Source With redirect binding""" + sleep(1) + # Bootstrap all needed objects + authentication_flow = Flow.objects.get(slug="default-source-authentication") + enrollment_flow = Flow.objects.get(slug="default-source-enrollment") + keypair = CertificateKeyPair.objects.create( + name="test-idp-cert", certificate_data=IDP_CERT + ) + + SAMLSource.objects.create( + name="saml-idp-test", + slug="saml-idp-test", + authentication_flow=authentication_flow, + enrollment_flow=enrollment_flow, + issuer="entity-id", + sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php", + binding_type=SAMLBindingTypes.Redirect, + signing_kp=keypair, + ) + + self.driver.get(self.live_server_url) + + self.wait.until( + ec.presence_of_element_located( + (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") + ) + ) + self.driver.find_element( + By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" + ).click() + + # Now we should be at the IDP, wait for the username field + self.wait.until(ec.presence_of_element_located((By.ID, "username"))) + self.driver.find_element(By.ID, "username").send_keys("user1") + self.driver.find_element(By.ID, "password").send_keys("user1pass") + self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER) + + # Wait until we're logged in + 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() + + # Wait until we've loaded the user info page + self.wait.until(ec.presence_of_element_located((By.ID, "id_username"))) + self.assertNotEqual( + self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" + ) diff --git a/e2e/utils.py b/e2e/utils.py new file mode 100644 index 000000000..775ce6d44 --- /dev/null +++ b/e2e/utils.py @@ -0,0 +1,92 @@ +"""passbook e2e testing utilities""" +from functools import lru_cache +from glob import glob +from importlib.util import module_from_spec, spec_from_file_location +from inspect import getmembers, isfunction +from os import makedirs +from time import time + +from Cryptodome.PublicKey import RSA +from django.apps import apps +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.db import connection, transaction +from django.db.utils import IntegrityError +from django.shortcuts import reverse +from selenium import webdriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support.ui import WebDriverWait + +from passbook.core.models import User + + +@lru_cache +# pylint: disable=invalid-name +def USER() -> User: # noqa + """Cached function that always returns pbadmin""" + return User.objects.get(username="pbadmin") + + +def ensure_rsa_key(): + """Ensure that at least one RSAKey Object exists, create one if none exist""" + from oidc_provider.models import RSAKey + + if not RSAKey.objects.exists(): + key = RSA.generate(2048) + rsakey = RSAKey(key=key.exportKey("PEM").decode("utf8")) + rsakey.save() + + +class SeleniumTestCase(StaticLiveServerTestCase): + """StaticLiveServerTestCase which automatically creates a Webdriver instance""" + + def setUp(self): + super().setUp() + makedirs("out", exist_ok=True) + self.driver = self._get_driver() + self.driver.maximize_window() + self.driver.implicitly_wait(5) + self.wait = WebDriverWait(self.driver, 60) + self.apply_default_data() + + def _get_driver(self) -> WebDriver: + return webdriver.Remote( + command_executor="http://localhost:4444/wd/hub", + desired_capabilities=DesiredCapabilities.CHROME, + ) + + def tearDown(self): + self.driver.save_screenshot(f"out/{self.__class__.__name__}_{time()}.png") + self.driver.quit() + super().tearDown() + + def url(self, view, **kwargs) -> str: + """reverse `view` with `**kwargs` into full URL using live_server_url""" + return self.live_server_url + reverse(view, kwargs=kwargs) + + def apply_default_data(self): + """apply objects created by migrations after tables have been truncated""" + # Find all migration files + # load all functions + migration_files = glob("**/migrations/*.py", recursive=True) + matches = [] + for migration in migration_files: + with open(migration, "r+") as migration_file: + # Check if they have a `RunPython` + if "RunPython" in migration_file.read(): + matches.append(migration) + + with connection.schema_editor() as schema_editor: + for match in matches: + # Load module from file path + spec = spec_from_file_location("", match) + migration_module = module_from_spec(spec) + # pyright: reportGeneralTypeIssues=false + spec.loader.exec_module(migration_module) + # Call all functions from module + for _, func in getmembers(migration_module, isfunction): + with transaction.atomic(): + try: + func(apps, schema_editor) + except IntegrityError: + pass diff --git a/manage.py b/manage.py index 5a70f4ada..8b16cf9a3 100755 --- a/manage.py +++ b/manage.py @@ -2,6 +2,7 @@ """Django manage.py""" import os import sys + from defusedxml import defuse_stdlib defuse_stdlib() diff --git a/passbook/admin/templates/administration/stage_binding/list.html b/passbook/admin/templates/administration/stage_binding/list.html index efdf7f235..a4342cf0e 100644 --- a/passbook/admin/templates/administration/stage_binding/list.html +++ b/passbook/admin/templates/administration/stage_binding/list.html @@ -39,8 +39,8 @@ {% for flow in grouped_bindings %}
+ {% blocktrans with name=context.application.name %} + You're about to sign into {{ name }}. + {% endblocktrans %} +
+{% trans "Application requires following permissions" %}
++ {% blocktrans with name=context.application.name %} + You're about to sign into {{ name }}. + {% endblocktrans %} +
+{% trans "Application requires following permissions" %}
++ {% blocktrans with name=context.application.name %} + You're about to sign into {{ name }}. + {% endblocktrans %} +
+ {{ hidden_inputs }} +