diff --git a/e2e/test_provider_proxy.py b/e2e/test_provider_proxy.py new file mode 100644 index 000000000..bc90d3c8a --- /dev/null +++ b/e2e/test_provider_proxy.py @@ -0,0 +1,93 @@ +"""Proxy and Outpost e2e tests""" +from time import sleep +from typing import Any, Dict, Optional + +from docker.client import DockerClient, from_env +from docker.models.containers import Container +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 Application +from passbook.flows.models import Flow +from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType +from passbook.providers.proxy.models import ProxyProvider + + +class TestProviderProxy(SeleniumTestCase): + """Proxy and Outpost e2e tests""" + + proxy_container: Container + + def tearDown(self) -> None: + super().tearDown() + self.proxy_container.kill() + + def get_container_specs(self) -> Optional[Dict[str, Any]]: + return { + "image": "traefik/whoami:latest", + "detach": True, + "network_mode": "host", + "auto_remove": True, + } + + def start_proxy(self, outpost: Outpost) -> Container: + """Start proxy container based on outpost created""" + client: DockerClient = from_env() + container = client.containers.run( + image="beryju/passbook-proxy:latest", + detach=True, + network_mode="host", + auto_remove=True, + environment={ + "PASSBOOK_HOST": self.live_server_url, + "PASSBOOK_TOKEN": outpost.token.token_uuid.hex, + }, + ) + return container + + def test_proxy_simple(self): + """Test simple outpost setup with single provider""" + proxy: ProxyProvider = ProxyProvider.objects.create( + name="proxy_provider", + authorization_flow=Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ), + internal_host="http://localhost:80", + external_host="http://localhost:4180", + ) + # Ensure OAuth2 Params are set + proxy.set_oauth_defaults() + proxy.save() + # we need to create an application to actually access the proxy + Application.objects.create(name="proxy", slug="proxy", provider=proxy) + outpost: Outpost = Outpost.objects.create( + name="proxy_outpost", + type=OutpostType.PROXY, + deployment_type=OutpostDeploymentType.CUSTOM, + ) + outpost.providers.add(proxy) + outpost.save() + + self.proxy_container = self.start_proxy(outpost) + + # Wait until outpost healthcheck succeeds + healthcheck_retries = 0 + while healthcheck_retries < 50: + if outpost.health: + break + healthcheck_retries += 1 + sleep(0.5) + + self.driver.get("http://localhost:4180") + + 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(1) + + full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text + self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text) diff --git a/e2e/utils.py b/e2e/utils.py index 5896c127e..5592c6a68 100644 --- a/e2e/utils.py +++ b/e2e/utils.py @@ -17,6 +17,7 @@ from docker.models.containers import Container from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.ui import WebDriverWait from structlog import get_logger @@ -50,6 +51,8 @@ class SeleniumTestCase(StaticLiveServerTestCase): def _start_container(self, specs: Dict[str, Any]) -> Container: client: DockerClient = from_env() container = client.containers.run(**specs) + if "healthcheck" not in specs: + return container while True: container.reload() status = container.attrs.get("State", {}).get("Health", {}).get("Status") @@ -88,7 +91,7 @@ class SeleniumTestCase(StaticLiveServerTestCase): def wait_for_url(self, desired_url): """Wait until URL is `desired_url`.""" self.wait.until( - lambda driver: driver.current_url == desired_url, + ec.url_to_be(desired_url), f"URL {self.driver.current_url} doesn't match expected URL {desired_url}", ) diff --git a/passbook/admin/fields.py b/passbook/admin/fields.py index 59ebb1fc9..8959b323f 100644 --- a/passbook/admin/fields.py +++ b/passbook/admin/fields.py @@ -15,11 +15,8 @@ class CodeMirrorWidget(forms.Textarea): self.mode = mode def render(self, *args, **kwargs): - if "attrs" not in kwargs: - kwargs["attrs"] = {} - attrs = kwargs["attrs"] - if "class" not in attrs: - attrs["class"] = "" + attrs = kwargs.setdefault("attrs", {}) + attrs.setdefault("class", "") attrs["class"] += " codemirror" attrs["data-cm-mode"] = self.mode return super().render(*args, **kwargs) diff --git a/passbook/core/tests/tests_tasks.py b/passbook/core/tests/test_tasks.py similarity index 100% rename from passbook/core/tests/tests_tasks.py rename to passbook/core/tests/test_tasks.py diff --git a/passbook/flows/management/commands/apply_flow.py b/passbook/flows/management/commands/apply_flow.py index 36e59c037..84e63206b 100644 --- a/passbook/flows/management/commands/apply_flow.py +++ b/passbook/flows/management/commands/apply_flow.py @@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, no_translations from passbook.flows.transfer.importer import FlowImporter -class Command(BaseCommand): +class Command(BaseCommand): # pragma: no cover """Apply flow from commandline""" @no_translations diff --git a/passbook/lib/utils/template.py b/passbook/lib/utils/template.py index af6b492b6..664c2c3dd 100644 --- a/passbook/lib/utils/template.py +++ b/passbook/lib/utils/template.py @@ -1,11 +1,5 @@ """passbook lib template utilities""" -from django.template import Context, Template, loader - - -def render_from_string(tmpl: str, ctx: Context) -> str: - """Render template from string to string""" - template = Template(tmpl) - return template.render(ctx) +from django.template import Context, loader def render_to_string(template_path: str, ctx: Context) -> str: diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 8e1edec90..928eb0f62 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -25,6 +25,19 @@ from passbook.lib.config import CONFIG from passbook.lib.logging import add_process_id from passbook.lib.sentry import before_send + +def j_print(event: str, log_level: str = "info", **kwargs): + """Print event in the same format as structlog with JSON. + Used before structlog is configured.""" + data = { + "event": event, + "level": log_level, + "logger": __name__, + } + data.update(**kwargs) + print(dumps(data)) + + LOGGER = structlog.get_logger() # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -276,16 +289,7 @@ if CONFIG.y("postgresql.backup"): AWS_STORAGE_BUCKET_NAME = CONFIG.y("postgresql.backup.bucket") AWS_S3_ENDPOINT_URL = CONFIG.y("postgresql.backup.host") AWS_DEFAULT_ACL = None - print( - dumps( - { - "event": "Database backup is configured.", - "level": "info", - "logger": __name__, - "host": CONFIG.y("postgresql.backup.host"), - } - ) - ) + j_print("Database backup is configured.", host=CONFIG.y("postgresql.backup.host")) # Add automatic task to backup CELERY_BEAT_SCHEDULE["db_backup"] = { "task": "passbook.lib.tasks.backup_database", @@ -295,15 +299,6 @@ if CONFIG.y("postgresql.backup"): # Sentry integration _ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False) if not DEBUG and _ERROR_REPORTING: - print( - dumps( - { - "event": "Error reporting is enabled.", - "level": "info", - "logger": __name__, - } - ) - ) sentry_init( dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3", integrations=[ @@ -316,6 +311,10 @@ if not DEBUG and _ERROR_REPORTING: environment=CONFIG.y("error_reporting.environment", "customer"), send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False), ) + j_print( + "Error reporting is enabled.", + env=CONFIG.y("error_reporting.environment", "customer"), + ) # Static files (CSS, JavaScript, Images) @@ -434,3 +433,5 @@ if DEBUG: MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") INSTALLED_APPS.append("passbook.core.apps.PassbookCoreConfig") + +j_print("Booting passbook", version=__version__)