diff --git a/e2e/test_provider_oauth.py b/e2e/test_provider_oauth.py new file mode 100644 index 000000000..58a9a7f32 --- /dev/null +++ b/e2e/test_provider_oauth.py @@ -0,0 +1,195 @@ +"""test OAuth Provider flow""" +from time import sleep + +from django.shortcuts import reverse +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.live_server_url + + reverse("passbook_providers_oauth:github-authorize") + ), + "GF_AUTH_GITHUB_TOKEN_URL": ( + self.live_server_url + + reverse("passbook_providers_oauth:github-access-token") + ), + "GF_AUTH_GITHUB_API_URL": ( + self.live_server_url + + reverse("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_saml.py b/e2e/test_provider_saml.py new file mode 100644 index 000000000..242bd8686 --- /dev/null +++ b/e2e/test_provider_saml.py @@ -0,0 +1,175 @@ +"""test SAML Provider flow""" +from time import sleep + +from django.shortcuts import reverse +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.live_server_url + + reverse( + "passbook_providers_saml:metadata", + kwargs={"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.live_server_url + + reverse( + "passbook_providers_saml:sso-init", + kwargs={"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}!", + )