def __init__(self, verbosity=1, failfast=False, keepdb=False, **_): self.verbosity = verbosity self.failfast = failfast self.keepdb = keepdb settings.TEST = True settings.CELERY_TASK_ALWAYS_EAGER = True CONFIG.y_set("authentik.avatars", "none")
def should_backup() -> bool: """Check if we should be doing backups""" if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"): LOGGER.info("Running in k8s and s3 backups are not configured, skipping") return False if not CONFIG.y_bool("postgresql.backup.enabled"): return False return True
def get(self, request: Request) -> Response: """Retrive public configuration options""" config = ConfigSerializer({ "error_reporting_enabled": CONFIG.y("error_reporting.enabled"), "error_reporting_environment": CONFIG.y("error_reporting.environment"), "error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"), "capabilities": self.get_capabilities(), }) return Response(config.data)
def test_invalid_flow_redirect(self): """Tests that an invalid flow still redirects""" flow = Flow.objects.create( name="test-empty", slug="test-empty", designation=FlowDesignation.AUTHENTICATION, ) CONFIG.update_from_dict({"domain": "testserver"}) dest = "/unique-string" url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}") self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
def test_invalid_empty_flow(self): """Tests that an empty flow returns the correct error message""" flow = Flow.objects.create( name="test-empty", slug="test-empty", designation=FlowDesignation.AUTHENTICATION, ) CONFIG.update_from_dict({"domain": "testserver"}) response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
def context_processor(request: HttpRequest) -> dict[str, Any]: """Context Processor that injects tenant object into every template""" return { "tenant": request.tenant, "ak_version": __version__, "footer_links": CONFIG.y("authentik.footer_links"), }
def test_deployment_reconciler(self): """test that deployment requires update""" controller = ProxyKubernetesController(self.outpost, self.service_connection) deployment_reconciler = DeploymentReconciler(controller) self.assertIsNotNone(deployment_reconciler.retrieve()) config = self.outpost.config config.kubernetes_replicas = 3 self.outpost.config = config with self.assertRaises(NeedsUpdate): deployment_reconciler.reconcile( deployment_reconciler.retrieve(), deployment_reconciler.get_reference_object(), ) with CONFIG.patch("outposts.container_image_base", "test"): with self.assertRaises(NeedsUpdate): deployment_reconciler.reconcile( deployment_reconciler.retrieve(), deployment_reconciler.get_reference_object(), ) deployment_reconciler.delete( deployment_reconciler.get_reference_object())
class OutpostConfig: """Configuration an outpost uses to configure it self""" # update website/docs/outposts/outposts.md authentik_host: str = "" authentik_host_insecure: bool = False authentik_host_browser: str = "" log_level: str = CONFIG.y("log_level") object_naming_template: str = field(default="ak-outpost-%(name)s") docker_network: Optional[str] = field(default=None) docker_map_ports: bool = field(default=True) container_image: Optional[str] = field(default=None) kubernetes_replicas: int = field(default=1) kubernetes_namespace: str = field(default_factory=get_namespace) kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) kubernetes_ingress_secret_name: str = field( default="authentik-outpost-tls") kubernetes_service_type: str = field(default="ClusterIP") kubernetes_disabled_components: list[str] = field(default_factory=list) kubernetes_image_pull_secrets: Optional[list[str]] = field( default_factory=list)
def test_discovery(self): """Test certificate discovery""" builder = CertificateBuilder() builder.common_name = "test-cert" with self.assertRaises(ValueError): builder.save() builder.build( subject_alt_names=[], validity_days=3, ) with TemporaryDirectory() as temp_dir: with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert: _cert.write(builder.certificate) with open(f"{temp_dir}/foo.key", "w+", encoding="utf-8") as _key: _key.write(builder.private_key) makedirs(f"{temp_dir}/foo.bar", exist_ok=True) with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert: _cert.write(builder.certificate) with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key: _key.write(builder.private_key) with CONFIG.patch("cert_discovery_dir", temp_dir): # pyright: reportGeneralTypeIssues=false certificate_discovery() # pylint: disable=no-value-for-parameter keypair: CertificateKeyPair = CertificateKeyPair.objects.filter( managed=MANAGED_DISCOVERED % "foo").first() self.assertIsNotNone(keypair) self.assertIsNotNone(keypair.certificate) self.assertIsNotNone(keypair.private_key) self.assertTrue( CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists())
def _get_container(self) -> tuple[Container, bool]: container_name = f"authentik-proxy-{self.outpost.uuid.hex}" try: return self.client.containers.get(container_name), False except NotFound: self.logger.info("Container does not exist, creating") image_prefix = CONFIG.y("outposts.docker_image_base") image_name = f"{image_prefix}-{self.outpost.type}:{__version__}" self.client.images.pull(image_name) container_args = { "image": image_name, "name": f"authentik-proxy-{self.outpost.uuid.hex}", "detach": True, "ports": { f"{port.port}/{port.protocol.lower()}": port.port for port in self.deployment_ports }, "environment": self._get_env(), "labels": self._get_labels(), } if settings.TEST: del container_args["ports"] container_args["network_mode"] = "host" return ( self.client.containers.create(**container_args), True, )
def get_static_deployment(self) -> str: """Generate docker-compose yaml for proxy, version 3.5""" ports = [ f"{port.port}:{port.port}/{port.protocol.lower()}" for port in self.deployment_ports ] image_prefix = CONFIG.y("outposts.docker_image_base") compose = { "version": "3.5", "services": { f"authentik_{self.outpost.type}": { "image": f"{image_prefix}-{self.outpost.type}:{__version__}", "ports": ports, "environment": { "AUTHENTIK_HOST": self.outpost.config.authentik_host, "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure), "AUTHENTIK_TOKEN": self.outpost.token.key, }, "labels": self._get_labels(), } }, } return safe_dump(compose, default_flow_style=False)
class OutpostConfig: """Configuration an outpost uses to configure it self""" authentik_host: str authentik_host_insecure: bool = False log_level: str = CONFIG.y("log_level") error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled") error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer") kubernetes_replicas: int = field(default=1) kubernetes_namespace: str = field(default="default") kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) kubernetes_ingress_secret_name: str = field(default="authentik-outpost")
def get_container_image(self) -> str: """Get container image to use for this outpost""" image_name_template: str = CONFIG.y("outposts.docker_image_base") return image_name_template % { "type": self.outpost.type, "version": __version__ }
def run(self): self.cur.execute(SQL_STATEMENT) self.con.commit() # We also need to clean the cache to make sure no pickeled objects still exist for db in [ CONFIG.y("redis.message_queue_db"), CONFIG.y("redis.cache_db"), CONFIG.y("redis.ws_db"), ]: redis = Redis( host=CONFIG.y("redis.host"), port=6379, db=db, password=CONFIG.y("redis.password"), ) redis.flushall()
def get_container_image(self) -> str: """Get container image to use for this outpost""" image_name_template: str = CONFIG.y("outposts.docker_image_base") return image_name_template % { "type": self.outpost.type, "version": __version__, "build_hash": environ.get(ENV_GIT_HASH_KEY, ""), }
def get_reference_object(self) -> V1Deployment: """Get deployment object for outpost""" # Generate V1ContainerPort objects container_ports = [] for port in self.controller.deployment_ports: container_ports.append( V1ContainerPort( container_port=port.port, name=port.name, protocol=port.protocol.upper(), )) meta = self.get_object_meta(name=self.name) secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api" image_prefix = CONFIG.y("outposts.docker_image_base") return V1Deployment( metadata=meta, spec=V1DeploymentSpec( replicas=self.outpost.config.kubernetes_replicas, selector=V1LabelSelector(match_labels=self.get_pod_meta()), template=V1PodTemplateSpec( metadata=V1ObjectMeta(labels=self.get_pod_meta()), spec=V1PodSpec(containers=[ V1Container( name=str(self.outpost.type), image= f"{image_prefix}-{self.outpost.type}:{__version__}", ports=container_ports, env=[ V1EnvVar( name="AUTHENTIK_HOST", value_from=V1EnvVarSource( secret_key_ref=V1SecretKeySelector( name=secret_name, key="authentik_host", )), ), V1EnvVar( name="AUTHENTIK_TOKEN", value_from=V1EnvVarSource( secret_key_ref=V1SecretKeySelector( name=secret_name, key="token", )), ), V1EnvVar( name="AUTHENTIK_INSECURE", value_from=V1EnvVarSource( secret_key_ref=V1SecretKeySelector( name=secret_name, key="authentik_host_insecure", )), ), ], ) ]), ), ), )
def certificate_discovery(self: MonitoredTask): """Discover, import and update certificates from the filesystem""" certs = {} private_keys = {} discovered = 0 for file in glob(CONFIG.y("cert_discovery_dir") + "/**", recursive=True): path = Path(file) if not path.exists(): continue if path.is_dir(): continue # For certbot setups, we want to ignore archive. if "archive" in file: continue # Support certbot's directory structure if path.name in ["fullchain.pem", "privkey.pem"]: cert_name = path.parent.name else: cert_name = path.name.replace(path.suffix, "") try: with open(path, "r+", encoding="utf-8") as _file: body = _file.read() if "PRIVATE KEY" in body: private_keys[cert_name] = ensure_private_key_valid(body) else: certs[cert_name] = ensure_certificate_valid(body) except (OSError, ValueError) as exc: LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path) discovered += 1 for name, cert_data in certs.items(): cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first() if not cert: cert = CertificateKeyPair( name=name, managed=MANAGED_DISCOVERED % name, ) dirty = False if cert.certificate_data != cert_data: cert.certificate_data = cert_data dirty = True if name in private_keys: if cert.key_data != private_keys[name]: cert.key_data = private_keys[name] dirty = True if dirty: cert.save() self.set_status( TaskResult( TaskResultStatus.SUCCESSFUL, messages=[ _("Successfully imported %(count)d files." % {"count": discovered}) ], ))
def validate_email(self, email: str): """Check if the user is allowed to change their email""" if self.instance.group_attributes().get( USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.y_bool("default_user_change_email", True)): return email if email != self.instance.email: raise ValidationError("Not allowed to change email.") return email
def validate_username(self, username: str): """Check if the user is allowed to change their username""" if self.instance.group_attributes().get( USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.y_bool("default_user_change_username", True)): return username if username != self.instance.username: raise ValidationError("Not allowed to change username.") return username
def test_invalid_non_applicable_flow(self): """Tests that a non-applicable flow returns the correct error message""" flow = Flow.objects.create( name="test-non-applicable", slug="test-non-applicable", designation=FlowDesignation.AUTHENTICATION, ) CONFIG.update_from_dict({"domain": "testserver"}) response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 200) self.assertStageResponse( response, flow=flow, error_message=FlowNonApplicableException.__doc__, component="ak-stage-access-denied", )
def get_container_image(self) -> str: """Get container image to use for this outpost""" if self.outpost.config.container_image is not None: return self.outpost.config.container_image image_name_template: str = CONFIG.y("outposts.container_image_base") return image_name_template % { "type": self.outpost.type, "version": __version__, "build_hash": get_build_hash(), }
class OutpostConfig: """Configuration an outpost uses to configure it self""" authentik_host: str authentik_host_insecure: bool = False log_level: str = CONFIG.y("log_level") error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled") error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer") object_naming_template: str = field(default="ak-outpost-%(name)s") kubernetes_replicas: int = field(default=1) kubernetes_namespace: str = field(default_factory=get_namespace) kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) kubernetes_ingress_secret_name: str = field( default="authentik-outpost-tls") kubernetes_service_type: str = field(default="ClusterIP") kubernetes_disabled_components: list[str] = field(default_factory=list)
def get_geoip_reader() -> Optional[Reader]: """Get GeoIP Reader, if configured, otherwise none""" path = CONFIG.y("authentik.geoip") if path == "" or not path: return None try: reader = Reader(path) LOGGER.info("Enabled GeoIP support") return reader except OSError: return None
def __open(self): """Get GeoIP Reader, if configured, otherwise none""" path = CONFIG.y("geoip") if path == "" or not path: return try: self.__reader = Reader(path) self.__last_mtime = stat(path).st_mtime LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime) except OSError as exc: LOGGER.warning("Failed to load GeoIP database", exc=exc)
def context_processor(request: HttpRequest) -> dict[str, Any]: """Context Processor that injects tenant object into every template""" tenant = getattr(request, "tenant", DEFAULT_TENANT) trace = "" span = Hub.current.scope.span if span: trace = span.to_traceparent() return { "tenant": tenant, "footer_links": CONFIG.y("footer_links"), "sentry_trace": trace, }
def test_invalid_non_applicable_flow(self): """Tests that a non-applicable flow returns the correct error message""" flow = Flow.objects.create( name="test-non-applicable", slug="test-non-applicable", designation=FlowDesignation.AUTHENTICATION, ) CONFIG.update_from_dict({"domain": "testserver"}) response = self.client.get( reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 200) self.assertJSONEqual( force_str(response.content), { "component": "ak-stage-access-denied", "error_message": FlowNonApplicableException.__doc__, "title": "", "type": ChallengeTypes.NATIVE.value, }, )
def test_fallback(self): """Test fallback tenant""" Tenant.objects.all().delete() self.assertJSONEqual( self.client.get(reverse("authentik_api:tenant-current")).content.decode(), { "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_favicon": "/static/dist/assets/icons/icon.png", "branding_title": "authentik", "matched_domain": "fallback", "ui_footer_links": CONFIG.y("footer_links"), }, )
def test_current_tenant(self): """Test Current tenant API""" tenant = create_test_tenant() self.assertJSONEqual( self.client.get(reverse("authentik_api:tenant-current")).content.decode(), { "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_favicon": "/static/dist/assets/icons/icon.png", "branding_title": "authentik", "matched_domain": tenant.domain, "ui_footer_links": CONFIG.y("footer_links"), }, )
def get_env() -> str: """Get environment in which authentik is currently running""" if SERVICE_HOST_ENV_NAME in os.environ: return "kubernetes" if "CI" in os.environ: return "ci" if Path("/tmp/authentik-mode").exists(): # nosec return "compose" if CONFIG.y_bool("debug"): return "dev" if "AK_APPLIANCE" in os.environ: return os.environ["AK_APPLIANCE"] return "custom"
def test_current_tenant(self): """Test Current tenant API""" self.assertJSONEqual( force_str( self.client.get( reverse("authentik_api:tenant-current")).content), { "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_title": "authentik", "matched_domain": "authentik-default", "ui_footer_links": CONFIG.y("authentik.footer_links"), }, )