outposts: set cookies for a domain to authenticate an entire domain (#971)
* outposts: initial cookie domain implementation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add cookie domain setting Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: replace forward_auth_mode with general mode Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: rebuild proxy provider form Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: re-add forward_auth_mode for backwards compat Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix data.mode not being set Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: always set log level to debug when testing Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: use new mode attribute Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: only ingress /akprox on forward_domain Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: fix lint error Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix error on ProxyProviderForm when not using proxy mode Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix default for outpost form's type missing Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: add additional desc for proxy modes Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts: fix service account permissions not always being updated Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost/proxy: fix redirecting to incorrect host for domain mode Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: improve error handling for network errors Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost: fix image naming not matching main imaeg Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/proxy: fix redirects for domain mode and traefik Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix colour for paragraphs Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: fix consent stage not showing permissions correctly Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website/docs: add domain-level docs Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website/docs: fix broken links Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts/proxy: remove dead code Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/flows: fix missing id for #header-text Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
fb8d67a9d9
commit
dad24c03ff
|
@ -149,8 +149,9 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(instance, Outpost):
|
if isinstance(instance, Outpost):
|
||||||
LOGGER.debug("Ensuring token for outpost", instance=instance)
|
LOGGER.debug("Ensuring token and permissions for outpost", instance=instance)
|
||||||
_ = instance.token
|
_ = instance.token
|
||||||
|
_ = instance.user
|
||||||
LOGGER.debug("Trigger reconcile for outpost")
|
LOGGER.debug("Trigger reconcile for outpost")
|
||||||
outpost_controller.delay(instance.pk)
|
outpost_controller.delay(instance.pk)
|
||||||
|
|
||||||
|
@ -201,6 +202,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
|
||||||
# Ensure token again, because this function is called when anything related to an
|
# Ensure token again, because this function is called when anything related to an
|
||||||
# OutpostModel is saved, so we can be sure permissions are right
|
# OutpostModel is saved, so we can be sure permissions are right
|
||||||
_ = outpost.token
|
_ = outpost.token
|
||||||
|
_ = outpost.user
|
||||||
if not layer: # pragma: no cover
|
if not layer: # pragma: no cover
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
for state in OutpostState.for_outpost(outpost):
|
for state in OutpostState.for_outpost(outpost):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||||
|
|
||||||
|
|
||||||
class OpenIDConnectConfigurationSerializer(PassiveSerializer):
|
class OpenIDConnectConfigurationSerializer(PassiveSerializer):
|
||||||
|
@ -36,9 +36,9 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||||
redirect_uris = CharField(read_only=True)
|
redirect_uris = CharField(read_only=True)
|
||||||
|
|
||||||
def validate(self, attrs) -> dict[Any, str]:
|
def validate(self, attrs) -> dict[Any, str]:
|
||||||
"""Check that internal_host is set when forward_auth_mode is disabled"""
|
"""Check that internal_host is set when mode is Proxy"""
|
||||||
if (
|
if (
|
||||||
not attrs.get("forward_auth_mode", False)
|
attrs.get("mode", ProxyMode.PROXY) == ProxyMode.PROXY
|
||||||
and attrs.get("internal_host", "") == ""
|
and attrs.get("internal_host", "") == ""
|
||||||
):
|
):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
@ -70,8 +70,9 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||||
"basic_auth_enabled",
|
"basic_auth_enabled",
|
||||||
"basic_auth_password_attribute",
|
"basic_auth_password_attribute",
|
||||||
"basic_auth_user_attribute",
|
"basic_auth_user_attribute",
|
||||||
"forward_auth_mode",
|
"mode",
|
||||||
"redirect_uris",
|
"redirect_uris",
|
||||||
|
"cookie_domain",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,9 +85,15 @@ class ProxyProviderViewSet(ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
class ProxyOutpostConfigSerializer(ModelSerializer):
|
class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||||
"""ProxyProvider Serializer"""
|
"""Proxy provider serializer for outposts"""
|
||||||
|
|
||||||
oidc_configuration = SerializerMethodField()
|
oidc_configuration = SerializerMethodField()
|
||||||
|
forward_auth_mode = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_forward_auth_mode(self, instance: ProxyProvider) -> bool:
|
||||||
|
"""Legacy field for 2021.5 outposts"""
|
||||||
|
# TODO: remove in 2021.7
|
||||||
|
return instance.mode in [ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
@ -106,6 +113,9 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||||
"basic_auth_enabled",
|
"basic_auth_enabled",
|
||||||
"basic_auth_password_attribute",
|
"basic_auth_password_attribute",
|
||||||
"basic_auth_user_attribute",
|
"basic_auth_user_attribute",
|
||||||
|
"mode",
|
||||||
|
"cookie_domain",
|
||||||
|
# Legacy field, remove in 2021.7
|
||||||
"forward_auth_mode",
|
"forward_auth_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ from authentik.outposts.controllers.k8s.base import (
|
||||||
KubernetesObjectReconciler,
|
KubernetesObjectReconciler,
|
||||||
NeedsUpdate,
|
NeedsUpdate,
|
||||||
)
|
)
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
|
@ -51,7 +51,6 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
||||||
expected_hosts_tls = []
|
expected_hosts_tls = []
|
||||||
for proxy_provider in ProxyProvider.objects.filter(
|
for proxy_provider in ProxyProvider.objects.filter(
|
||||||
outpost__in=[self.controller.outpost],
|
outpost__in=[self.controller.outpost],
|
||||||
forward_auth_mode=False,
|
|
||||||
):
|
):
|
||||||
proxy_provider: ProxyProvider
|
proxy_provider: ProxyProvider
|
||||||
external_host_name = urlparse(proxy_provider.external_host)
|
external_host_name = urlparse(proxy_provider.external_host)
|
||||||
|
@ -105,7 +104,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
||||||
external_host_name = urlparse(proxy_provider.external_host)
|
external_host_name = urlparse(proxy_provider.external_host)
|
||||||
if external_host_name.scheme == "https":
|
if external_host_name.scheme == "https":
|
||||||
tls_hosts.append(external_host_name.hostname)
|
tls_hosts.append(external_host_name.hostname)
|
||||||
if proxy_provider.forward_auth_mode:
|
if proxy_provider.mode in [
|
||||||
|
ProxyMode.FORWARD_SINGLE,
|
||||||
|
ProxyMode.FORWARD_DOMAIN,
|
||||||
|
]:
|
||||||
rule = NetworkingV1beta1IngressRule(
|
rule = NetworkingV1beta1IngressRule(
|
||||||
host=external_host_name.hostname,
|
host=external_host_name.hostname,
|
||||||
http=NetworkingV1beta1HTTPIngressRuleValue(
|
http=NetworkingV1beta1HTTPIngressRuleValue(
|
||||||
|
|
|
@ -10,7 +10,7 @@ from authentik.outposts.controllers.k8s.base import (
|
||||||
KubernetesObjectReconciler,
|
KubernetesObjectReconciler,
|
||||||
NeedsUpdate,
|
NeedsUpdate,
|
||||||
)
|
)
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
|
@ -73,7 +73,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
||||||
def noop(self) -> bool:
|
def noop(self) -> bool:
|
||||||
if not ProxyProvider.objects.filter(
|
if not ProxyProvider.objects.filter(
|
||||||
outpost__in=[self.controller.outpost],
|
outpost__in=[self.controller.outpost],
|
||||||
forward_auth_mode=True,
|
mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
|
||||||
).exists():
|
).exists():
|
||||||
self.logger.debug("No providers with forward auth enabled.")
|
self.logger.debug("No providers with forward auth enabled.")
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.3 on 2021-05-31 20:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_providers_proxy", "0011_proxyprovider_forward_auth_mode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="proxyprovider",
|
||||||
|
name="cookie_domain",
|
||||||
|
field=models.TextField(blank=True, default=""),
|
||||||
|
),
|
||||||
|
]
|
44
authentik/providers/proxy/migrations/0013_mode.py
Normal file
44
authentik/providers/proxy/migrations/0013_mode.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 3.2.3 on 2021-06-06 16:29
|
||||||
|
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_mode(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
from authentik.providers.proxy.models import ProxyMode
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
ProxyProvider = apps.get_model("authentik_providers_proxy", "proxyprovider")
|
||||||
|
for provider in ProxyProvider.objects.using(db_alias).all():
|
||||||
|
if provider.forward_auth_mode:
|
||||||
|
provider.mode = ProxyMode.FORWARD_SINGLE
|
||||||
|
provider.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_providers_proxy", "0012_proxyprovider_cookie_domain"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="proxyprovider",
|
||||||
|
name="mode",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("proxy", "Proxy"),
|
||||||
|
("forward_single", "Forward Single"),
|
||||||
|
("forward_domain", "Forward Domain"),
|
||||||
|
],
|
||||||
|
default="proxy",
|
||||||
|
help_text="Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_mode),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="proxyprovider",
|
||||||
|
name="forward_auth_mode",
|
||||||
|
),
|
||||||
|
]
|
|
@ -37,6 +37,14 @@ def _get_callback_url(uri: str) -> str:
|
||||||
return urljoin(uri, "/akprox/callback")
|
return urljoin(uri, "/akprox/callback")
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyMode(models.TextChoices):
|
||||||
|
"""All modes a Proxy provider can operate in"""
|
||||||
|
|
||||||
|
PROXY = "proxy"
|
||||||
|
FORWARD_SINGLE = "forward_single"
|
||||||
|
FORWARD_DOMAIN = "forward_domain"
|
||||||
|
|
||||||
|
|
||||||
class ProxyProvider(OutpostModel, OAuth2Provider):
|
class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||||
"""Protect applications that don't support any of the other
|
"""Protect applications that don't support any of the other
|
||||||
Protocols by using a Reverse-Proxy."""
|
Protocols by using a Reverse-Proxy."""
|
||||||
|
@ -53,8 +61,9 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||||
help_text=_("Validate SSL Certificates of upstream servers"),
|
help_text=_("Validate SSL Certificates of upstream servers"),
|
||||||
verbose_name=_("Internal host SSL Validation"),
|
verbose_name=_("Internal host SSL Validation"),
|
||||||
)
|
)
|
||||||
forward_auth_mode = models.BooleanField(
|
mode = models.TextField(
|
||||||
default=False,
|
default=ProxyMode.PROXY,
|
||||||
|
choices=ProxyMode.choices,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
|
"Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
|
||||||
"internal_host."
|
"internal_host."
|
||||||
|
@ -107,6 +116,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||||
)
|
)
|
||||||
|
|
||||||
cookie_secret = models.TextField(default=get_cookie_secret)
|
cookie_secret = models.TextField(default=get_cookie_secret)
|
||||||
|
cookie_domain = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
|
|
|
@ -155,6 +155,7 @@ SPECTACULAR_SETTINGS = {
|
||||||
"ChallengeChoices": "authentik.flows.challenge.ChallengeTypes",
|
"ChallengeChoices": "authentik.flows.challenge.ChallengeTypes",
|
||||||
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
|
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
|
||||||
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
|
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
|
||||||
|
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
|
||||||
},
|
},
|
||||||
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
||||||
"POSTPROCESSING_HOOKS": [
|
"POSTPROCESSING_HOOKS": [
|
||||||
|
@ -391,7 +392,10 @@ if _ERROR_REPORTING:
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
MEDIA_URL = "/media/"
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
LOG_LEVEL = CONFIG.y("log_level").upper()
|
TEST = False
|
||||||
|
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
|
||||||
|
|
||||||
|
LOG_LEVEL = CONFIG.y("log_level").upper() if not TEST else "DEBUG"
|
||||||
|
|
||||||
|
|
||||||
structlog.configure_once(
|
structlog.configure_once(
|
||||||
|
@ -449,9 +453,6 @@ LOGGING = {
|
||||||
"loggers": {},
|
"loggers": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST = False
|
|
||||||
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
|
|
||||||
|
|
||||||
_LOGGING_HANDLER_MAP = {
|
_LOGGING_HANDLER_MAP = {
|
||||||
"": LOG_LEVEL,
|
"": LOG_LEVEL,
|
||||||
"authentik": LOG_LEVEL,
|
"authentik": LOG_LEVEL,
|
||||||
|
|
|
@ -77,7 +77,7 @@ stages:
|
||||||
buildContext: '$(Build.SourcesDirectory)'
|
buildContext: '$(Build.SourcesDirectory)'
|
||||||
tags: |
|
tags: |
|
||||||
gh-$(branchName)
|
gh-$(branchName)
|
||||||
gh-$(Build.SourceVersion)
|
gh-$(branchName)-$(timestamp)
|
||||||
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
|
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
|
||||||
- task: Docker@2
|
- task: Docker@2
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -86,7 +86,7 @@ stages:
|
||||||
command: 'push'
|
command: 'push'
|
||||||
tags: |
|
tags: |
|
||||||
gh-$(branchName)
|
gh-$(branchName)
|
||||||
gh-$(Build.SourceVersion)
|
gh-$(branchName)-$(timestamp)
|
||||||
- job: ldap_build_docker
|
- job: ldap_build_docker
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
@ -108,7 +108,7 @@ stages:
|
||||||
buildContext: '$(Build.SourcesDirectory)'
|
buildContext: '$(Build.SourcesDirectory)'
|
||||||
tags: |
|
tags: |
|
||||||
gh-$(branchName)
|
gh-$(branchName)
|
||||||
gh-$(Build.SourceVersion)
|
gh-$(branchName)-$(timestamp)
|
||||||
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
|
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
|
||||||
- task: Docker@2
|
- task: Docker@2
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -117,4 +117,4 @@ stages:
|
||||||
command: 'push'
|
command: 'push'
|
||||||
tags: |
|
tags: |
|
||||||
gh-$(branchName)
|
gh-$(branchName)
|
||||||
gh-$(Build.SourceVersion)
|
gh-$(branchName)-$(timestamp)
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||||
providerOpts.SkipAuthRegex = skipRegexes
|
providerOpts.SkipAuthRegex = skipRegexes
|
||||||
}
|
}
|
||||||
|
|
||||||
if *provider.ForwardAuthMode {
|
if *provider.Mode == api.PROXYMODE_FORWARD_SINGLE || *provider.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||||
providerOpts.UpstreamServers = []options.Upstream{
|
providerOpts.UpstreamServers = []options.Upstream{
|
||||||
{
|
{
|
||||||
ID: "static",
|
ID: "static",
|
||||||
|
@ -111,6 +111,10 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||||
func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||||
opts := pb.prepareOpts(provider)
|
opts := pb.prepareOpts(provider)
|
||||||
|
|
||||||
|
if *provider.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||||
|
opts.Cookie.Domains = []string{*provider.CookieDomain}
|
||||||
|
}
|
||||||
|
|
||||||
chain := alice.New()
|
chain := alice.New()
|
||||||
|
|
||||||
if opts.ForceHTTPS {
|
if opts.ForceHTTPS {
|
||||||
|
@ -123,10 +127,6 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||||
|
|
||||||
healthCheckPaths := []string{opts.PingPath}
|
healthCheckPaths := []string{opts.PingPath}
|
||||||
healthCheckUserAgents := []string{opts.PingUserAgent}
|
healthCheckUserAgents := []string{opts.PingUserAgent}
|
||||||
if opts.GCPHealthChecks {
|
|
||||||
healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check")
|
|
||||||
healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// To silence logging of health checks, register the health check handler before
|
// To silence logging of health checks, register the health check handler before
|
||||||
// the logging handler
|
// the logging handler
|
||||||
|
@ -153,6 +153,8 @@ func (pb *providerBundle) Build(provider api.ProxyOutpostConfig) {
|
||||||
oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute
|
oauthproxy.BasicAuthPasswordAttribute = *provider.BasicAuthPasswordAttribute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oauthproxy.ExternalHost = pb.Host
|
||||||
|
|
||||||
pb.proxy = oauthproxy
|
pb.proxy = oauthproxy
|
||||||
pb.Handler = chain.Then(oauthproxy)
|
pb.Handler = chain.Then(oauthproxy)
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,35 +106,22 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
|
||||||
case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
|
case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
|
||||||
redirectURL, err := url.Parse(redirect)
|
redirectURL, err := url.Parse(redirect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
|
p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
redirectHostname := redirectURL.Hostname()
|
redirectHostname := redirectURL.Hostname()
|
||||||
|
|
||||||
for _, domain := range p.whitelistDomains {
|
for _, domain := range p.CookieDomains {
|
||||||
domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
|
if strings.HasSuffix(redirectHostname, domain) {
|
||||||
if domainHostname == "" {
|
p.logger.WithField("redirect", redirect).WithField("domain", domain).Debug("allowing redirect")
|
||||||
continue
|
return true
|
||||||
}
|
|
||||||
|
|
||||||
if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
|
|
||||||
// the domain names match, now validate the ports
|
|
||||||
// if the whitelisted domain's port is '*', allow all ports
|
|
||||||
// if the whitelisted domain contains a specific port, only allow that port
|
|
||||||
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
|
|
||||||
redirectPort := redirectURL.Port()
|
|
||||||
if (domainPort == "*") ||
|
|
||||||
(domainPort == redirectPort) ||
|
|
||||||
(domainPort == "" && redirectPort == "") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
|
p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
p.logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
|
p.logger.WithField("redirect", redirect).Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ type OAuthProxy struct {
|
||||||
AuthOnlyPath string
|
AuthOnlyPath string
|
||||||
UserInfoPath string
|
UserInfoPath string
|
||||||
|
|
||||||
forwardAuthMode bool
|
mode api.ProxyMode
|
||||||
redirectURL *url.URL // the url to receive requests at
|
redirectURL *url.URL // the url to receive requests at
|
||||||
whitelistDomains []string
|
whitelistDomains []string
|
||||||
provider providers.Provider
|
provider providers.Provider
|
||||||
|
@ -77,6 +77,7 @@ type OAuthProxy struct {
|
||||||
PassUserHeaders bool
|
PassUserHeaders bool
|
||||||
BasicAuthUserAttribute string
|
BasicAuthUserAttribute string
|
||||||
BasicAuthPasswordAttribute string
|
BasicAuthPasswordAttribute string
|
||||||
|
ExternalHost string
|
||||||
PassAccessToken bool
|
PassAccessToken bool
|
||||||
SetAuthorization bool
|
SetAuthorization bool
|
||||||
PassAuthorization bool
|
PassAuthorization bool
|
||||||
|
@ -136,7 +137,7 @@ func NewOAuthProxy(opts *options.Options, provider api.ProxyOutpostConfig, c *ht
|
||||||
CookieRefresh: opts.Cookie.Refresh,
|
CookieRefresh: opts.Cookie.Refresh,
|
||||||
CookieSameSite: opts.Cookie.SameSite,
|
CookieSameSite: opts.Cookie.SameSite,
|
||||||
|
|
||||||
forwardAuthMode: *provider.ForwardAuthMode,
|
mode: *provider.Mode,
|
||||||
RobotsPath: "/robots.txt",
|
RobotsPath: "/robots.txt",
|
||||||
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
|
SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
|
||||||
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
|
SignOutPath: fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
|
||||||
|
@ -216,43 +217,6 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitHostPort separates host and port. If the port is not valid, it returns
|
|
||||||
// the entire input as host, and it doesn't check the validity of the host.
|
|
||||||
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
|
|
||||||
// *** taken from net/url, modified validOptionalPort() to accept ":*"
|
|
||||||
func splitHostPort(hostport string) (host, port string) {
|
|
||||||
host = hostport
|
|
||||||
|
|
||||||
colon := strings.LastIndexByte(host, ':')
|
|
||||||
if colon != -1 && validOptionalPort(host[colon:]) {
|
|
||||||
host, port = host[:colon], host[colon+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
|
||||||
host = host[1 : len(host)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// validOptionalPort reports whether port is either an empty string
|
|
||||||
// or matches /^:\d*$/
|
|
||||||
// *** taken from net/url, modified to accept ":*"
|
|
||||||
func validOptionalPort(port string) bool {
|
|
||||||
if port == "" || port == ":*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if port[0] != ':' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, b := range port[1:] {
|
|
||||||
if b < '0' || b > '9' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
|
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
|
||||||
var noCacheHeaders = map[string]string{
|
var noCacheHeaders = map[string]string{
|
||||||
"Expires": time.Unix(0, 0).Format(time.RFC1123),
|
"Expires": time.Unix(0, 0).Format(time.RFC1123),
|
||||||
|
@ -340,18 +304,41 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
|
||||||
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
|
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
|
||||||
session, err := p.getAuthenticatedSession(rw, req)
|
session, err := p.getAuthenticatedSession(rw, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if p.forwardAuthMode {
|
if p.mode == api.PROXYMODE_FORWARD_SINGLE || p.mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||||
if _, ok := req.URL.Query()["nginx"]; ok {
|
if _, ok := req.URL.Query()["nginx"]; ok {
|
||||||
rw.WriteHeader(401)
|
rw.WriteHeader(401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, ok := req.URL.Query()["traefik"]; ok {
|
if _, ok := req.URL.Query()["traefik"]; ok {
|
||||||
host := getHost(req)
|
host := ""
|
||||||
|
// Optional suffix, which is appended to the URL
|
||||||
|
suffix := ""
|
||||||
|
if p.mode == api.PROXYMODE_FORWARD_SINGLE {
|
||||||
|
host = getHost(req)
|
||||||
|
} else if p.mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||||
|
host = p.ExternalHost
|
||||||
|
// set the ?rd flag to the current URL we have, since we redirect
|
||||||
|
// to a (possibly) different domain, but we want to be redirected back
|
||||||
|
// to the application
|
||||||
|
v := url.Values{
|
||||||
|
// see https://doc.traefik.io/traefik/middlewares/forwardauth/
|
||||||
|
// X-Forwarded-Uri is only the path, so we need to build the entire URL
|
||||||
|
"rd": []string{fmt.Sprintf(
|
||||||
|
"%s://%s%s",
|
||||||
|
req.Header.Get("X-Forwarded-Proto"),
|
||||||
|
req.Header.Get("X-Forwarded-Host"),
|
||||||
|
req.Header.Get("X-Forwarded-Uri"),
|
||||||
|
)},
|
||||||
|
}
|
||||||
|
suffix = fmt.Sprintf("?%s", v.Encode())
|
||||||
|
}
|
||||||
proto := req.Header.Get("X-Forwarded-Proto")
|
proto := req.Header.Get("X-Forwarded-Proto")
|
||||||
if proto != "" {
|
if proto != "" {
|
||||||
proto = proto + ":"
|
proto = proto + ":"
|
||||||
}
|
}
|
||||||
http.Redirect(rw, req, fmt.Sprintf("%s//%s%s", proto, host, p.OAuthStartPath), http.StatusTemporaryRedirect)
|
rdFinal := fmt.Sprintf("%s//%s%s%s", proto, host, p.OAuthStartPath, suffix)
|
||||||
|
p.logger.WithField("url", rdFinal).Debug("Redirecting to login")
|
||||||
|
http.Redirect(rw, req, rdFinal, http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -360,7 +347,7 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request)
|
||||||
}
|
}
|
||||||
// we are authenticated
|
// we are authenticated
|
||||||
p.addHeadersForProxying(rw, req, session)
|
p.addHeadersForProxying(rw, req, session)
|
||||||
if p.forwardAuthMode {
|
if p.mode == api.PROXYMODE_FORWARD_SINGLE || p.mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||||
for headerKey, headers := range req.Header {
|
for headerKey, headers := range req.Header {
|
||||||
for _, value := range headers {
|
for _, value := range headers {
|
||||||
rw.Header().Set(headerKey, value)
|
rw.Header().Set(headerKey, value)
|
||||||
|
|
40
schema.yml
40
schema.yml
|
@ -22966,10 +22966,13 @@ components:
|
||||||
title: HTTP-Basic Username Key
|
title: HTTP-Basic Username Key
|
||||||
description: User/Group Attribute used for the user part of the HTTP-Basic
|
description: User/Group Attribute used for the user part of the HTTP-Basic
|
||||||
Header. If not set, the user's Email address is used.
|
Header. If not set, the user's Email address is used.
|
||||||
forward_auth_mode:
|
mode:
|
||||||
type: boolean
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ProxyMode'
|
||||||
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
||||||
Exclusive with internal_host.
|
Exclusive with internal_host.
|
||||||
|
cookie_domain:
|
||||||
|
type: string
|
||||||
PatchedReputationPolicyRequest:
|
PatchedReputationPolicyRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Reputation Policy Serializer
|
description: Reputation Policy Serializer
|
||||||
|
@ -23971,9 +23974,15 @@ components:
|
||||||
required:
|
required:
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- name
|
- name
|
||||||
|
ProxyMode:
|
||||||
|
enum:
|
||||||
|
- proxy
|
||||||
|
- forward_single
|
||||||
|
- forward_domain
|
||||||
|
type: string
|
||||||
ProxyOutpostConfig:
|
ProxyOutpostConfig:
|
||||||
type: object
|
type: object
|
||||||
description: ProxyProvider Serializer
|
description: Proxy provider serializer for outposts
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -24025,12 +24034,19 @@ components:
|
||||||
title: HTTP-Basic Username Key
|
title: HTTP-Basic Username Key
|
||||||
description: User/Group Attribute used for the user part of the HTTP-Basic
|
description: User/Group Attribute used for the user part of the HTTP-Basic
|
||||||
Header. If not set, the user's Email address is used.
|
Header. If not set, the user's Email address is used.
|
||||||
forward_auth_mode:
|
mode:
|
||||||
type: boolean
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ProxyMode'
|
||||||
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
||||||
Exclusive with internal_host.
|
Exclusive with internal_host.
|
||||||
|
cookie_domain:
|
||||||
|
type: string
|
||||||
|
forward_auth_mode:
|
||||||
|
type: boolean
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- external_host
|
- external_host
|
||||||
|
- forward_auth_mode
|
||||||
- name
|
- name
|
||||||
- oidc_configuration
|
- oidc_configuration
|
||||||
- pk
|
- pk
|
||||||
|
@ -24102,13 +24118,16 @@ components:
|
||||||
title: HTTP-Basic Username Key
|
title: HTTP-Basic Username Key
|
||||||
description: User/Group Attribute used for the user part of the HTTP-Basic
|
description: User/Group Attribute used for the user part of the HTTP-Basic
|
||||||
Header. If not set, the user's Email address is used.
|
Header. If not set, the user's Email address is used.
|
||||||
forward_auth_mode:
|
mode:
|
||||||
type: boolean
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ProxyMode'
|
||||||
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
||||||
Exclusive with internal_host.
|
Exclusive with internal_host.
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
cookie_domain:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- assigned_application_name
|
- assigned_application_name
|
||||||
- assigned_application_slug
|
- assigned_application_slug
|
||||||
|
@ -24167,10 +24186,13 @@ components:
|
||||||
title: HTTP-Basic Username Key
|
title: HTTP-Basic Username Key
|
||||||
description: User/Group Attribute used for the user part of the HTTP-Basic
|
description: User/Group Attribute used for the user part of the HTTP-Basic
|
||||||
Header. If not set, the user's Email address is used.
|
Header. If not set, the user's Email address is used.
|
||||||
forward_auth_mode:
|
mode:
|
||||||
type: boolean
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ProxyMode'
|
||||||
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
description: Enable support for forwardAuth in traefik and nginx auth_request.
|
||||||
Exclusive with internal_host.
|
Exclusive with internal_host.
|
||||||
|
cookie_domain:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- external_host
|
- external_host
|
||||||
|
|
|
@ -25,8 +25,8 @@ export function configureSentry(canDoPpi: boolean = false, tags: { [key: string]
|
||||||
if (hint.originalException instanceof SentryIgnoredError) {
|
if (hint.originalException instanceof SentryIgnoredError) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (hint.originalException instanceof Error) {
|
if ((hint.originalException as Error | undefined)?.hasOwnProperty("name")) {
|
||||||
if (hint.originalException.name == 'NetworkError') {
|
if ((hint.originalException as Error | undefined)?.name == 'NetworkError') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,9 @@ body {
|
||||||
.pf-c-title {
|
.pf-c-title {
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
}
|
}
|
||||||
|
.pf-u-mb-xl {
|
||||||
|
color: var(--ak-dark-foreground);
|
||||||
|
}
|
||||||
/* Header sections */
|
/* Header sections */
|
||||||
.pf-c-page__main-section {
|
.pf-c-page__main-section {
|
||||||
--pf-c-page__main-section--BackgroundColor: var(--ak-dark-background);
|
--pf-c-page__main-section--BackgroundColor: var(--ak-dark-background);
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
|
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
|
||||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||||
|
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
import { BaseStage } from "../base";
|
import { BaseStage } from "../base";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
|
@ -18,7 +20,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
|
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
return [PFBase, PFLogin, PFList, PFForm, PFSpacing, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -44,11 +46,11 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||||
</div>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<div class="pf-c-form__group">
|
<div class="pf-c-form__group">
|
||||||
<p id="header-text">
|
<p id="header-text" class="pf-u-mb-xl">
|
||||||
${this.challenge.headerText}
|
${this.challenge.headerText}
|
||||||
</p>
|
</p>
|
||||||
${this.challenge.permissions.length > 0 ? html`
|
${this.challenge.permissions.length > 0 ? html`
|
||||||
<p>${t`Application requires following permissions`}</p>
|
<p class="pf-u-mb-sm">${t`Application requires following permissions:`}</p>
|
||||||
<ul class="pf-c-list" id="permmissions">
|
<ul class="pf-c-list" id="permmissions">
|
||||||
${this.challenge.permissions.map((permission) => {
|
${this.challenge.permissions.map((permission) => {
|
||||||
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
|
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
|
||||||
|
|
|
@ -793,6 +793,10 @@ msgstr "Continue flow without invitation"
|
||||||
msgid "Control how authentik exposes and interprets information."
|
msgid "Control how authentik exposes and interprets information."
|
||||||
msgstr "Control how authentik exposes and interprets information."
|
msgstr "Control how authentik exposes and interprets information."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "Cookie domain"
|
||||||
|
msgstr "Cookie domain"
|
||||||
|
|
||||||
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
|
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
|
||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
msgstr "Copy"
|
msgstr "Copy"
|
||||||
|
@ -1302,14 +1306,6 @@ msgstr "Enable TOTP"
|
||||||
msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
||||||
msgstr "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
msgstr "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
||||||
|
|
||||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
|
||||||
msgid "Enable forward-auth mode"
|
|
||||||
msgstr "Enable forward-auth mode"
|
|
||||||
|
|
||||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
|
||||||
msgid "Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request."
|
|
||||||
msgstr "Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request."
|
|
||||||
|
|
||||||
#: src/pages/policies/BoundPoliciesList.ts
|
#: src/pages/policies/BoundPoliciesList.ts
|
||||||
#: src/pages/policies/PolicyBindingForm.ts
|
#: src/pages/policies/PolicyBindingForm.ts
|
||||||
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
||||||
|
@ -1471,6 +1467,8 @@ msgstr "External Applications which use authentik as Identity-Provider, utilizin
|
||||||
msgid "External Host"
|
msgid "External Host"
|
||||||
msgstr "External Host"
|
msgstr "External Host"
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
msgid "External host"
|
msgid "External host"
|
||||||
msgstr "External host"
|
msgstr "External host"
|
||||||
|
@ -1624,9 +1622,13 @@ msgstr "Forgot username or password?"
|
||||||
msgid "Form didn't return a promise for submitting"
|
msgid "Form didn't return a promise for submitting"
|
||||||
msgstr "Form didn't return a promise for submitting"
|
msgstr "Form didn't return a promise for submitting"
|
||||||
|
|
||||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
msgid "Forward auth"
|
msgid "Forward auth (domain level)"
|
||||||
msgstr "Forward auth"
|
msgstr "Forward auth (domain level)"
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "Forward auth (single application)"
|
||||||
|
msgstr "Forward auth (single application)"
|
||||||
|
|
||||||
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
||||||
msgid "Friendly Name"
|
msgid "Friendly Name"
|
||||||
|
@ -2179,6 +2181,7 @@ msgstr "Minimum length"
|
||||||
|
|
||||||
#: src/pages/events/TransportForm.ts
|
#: src/pages/events/TransportForm.ts
|
||||||
#: src/pages/events/TransportListPage.ts
|
#: src/pages/events/TransportListPage.ts
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
||||||
#: src/pages/stages/consent/ConsentStageForm.ts
|
#: src/pages/stages/consent/ConsentStageForm.ts
|
||||||
msgid "Mode"
|
msgid "Mode"
|
||||||
msgstr "Mode"
|
msgstr "Mode"
|
||||||
|
@ -2308,7 +2311,6 @@ msgstr "New version available!"
|
||||||
#: src/pages/policies/BoundPoliciesList.ts
|
#: src/pages/policies/BoundPoliciesList.ts
|
||||||
#: src/pages/policies/PolicyTestForm.ts
|
#: src/pages/policies/PolicyTestForm.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
|
||||||
#: src/pages/tenants/TenantListPage.ts
|
#: src/pages/tenants/TenantListPage.ts
|
||||||
#: src/pages/tokens/TokenListPage.ts
|
#: src/pages/tokens/TokenListPage.ts
|
||||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||||
|
@ -2514,6 +2516,10 @@ msgstr "Optionally pre-fill the input value"
|
||||||
msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
|
msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
|
||||||
msgstr "Optionally set the 'FriendlyName' value of the Assertion attribute."
|
msgstr "Optionally set the 'FriendlyName' value of the Assertion attribute."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
|
||||||
|
msgstr "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
|
||||||
|
|
||||||
#: src/pages/flows/BoundStagesList.ts
|
#: src/pages/flows/BoundStagesList.ts
|
||||||
#: src/pages/flows/StageBindingForm.ts
|
#: src/pages/flows/StageBindingForm.ts
|
||||||
#: src/pages/policies/BoundPoliciesList.ts
|
#: src/pages/policies/BoundPoliciesList.ts
|
||||||
|
@ -2753,7 +2759,6 @@ msgstr "Protocol Settings"
|
||||||
|
|
||||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||||
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts
|
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
|
||||||
#: src/pages/providers/saml/SAMLProviderForm.ts
|
#: src/pages/providers/saml/SAMLProviderForm.ts
|
||||||
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
||||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||||
|
@ -2791,6 +2796,7 @@ msgid "Providers"
|
||||||
msgstr "Providers"
|
msgstr "Providers"
|
||||||
|
|
||||||
#: src/pages/outposts/OutpostForm.ts
|
#: src/pages/outposts/OutpostForm.ts
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
msgid "Proxy"
|
msgid "Proxy"
|
||||||
msgstr "Proxy"
|
msgstr "Proxy"
|
||||||
|
|
||||||
|
@ -3776,10 +3782,15 @@ msgstr "Text: Simple Text input"
|
||||||
msgid "The URL \"{0}\" was not found."
|
msgid "The URL \"{0}\" was not found."
|
||||||
msgstr "The URL \"{0}\" was not found."
|
msgstr "The URL \"{0}\" was not found."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
msgid "The external URL you'll access the application at. Include any non-standard port."
|
msgid "The external URL you'll access the application at. Include any non-standard port."
|
||||||
msgstr "The external URL you'll access the application at. Include any non-standard port."
|
msgstr "The external URL you'll access the application at. Include any non-standard port."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
|
||||||
|
msgstr "The external URL you'll authenticate at. Can be the same domain as authentik."
|
||||||
|
|
||||||
#: src/pages/policies/dummy/DummyPolicyForm.ts
|
#: src/pages/policies/dummy/DummyPolicyForm.ts
|
||||||
msgid "The policy takes a random time to execute. This controls the minimum time it will take."
|
msgid "The policy takes a random time to execute. This controls the minimum time it will take."
|
||||||
msgstr "The policy takes a random time to execute. This controls the minimum time it will take."
|
msgstr "The policy takes a random time to execute. This controls the minimum time it will take."
|
||||||
|
@ -3814,6 +3825,10 @@ msgstr ""
|
||||||
msgid "These policies control which users can access this application."
|
msgid "These policies control which users can access this application."
|
||||||
msgstr "These policies control which users can access this application."
|
msgstr "These policies control which users can access this application."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
|
||||||
|
msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
|
||||||
|
|
||||||
#: src/pages/stages/invitation/InvitationStageForm.ts
|
#: src/pages/stages/invitation/InvitationStageForm.ts
|
||||||
msgid "This stage can be included in enrollment flows to accept invitations."
|
msgid "This stage can be included in enrollment flows to accept invitations."
|
||||||
msgstr "This stage can be included in enrollment flows to accept invitations."
|
msgstr "This stage can be included in enrollment flows to accept invitations."
|
||||||
|
@ -4169,6 +4184,14 @@ msgstr "Use the user's email address, but deny enrollment when the email address
|
||||||
msgid "Use the user's username, but deny enrollment when the username already exists."
|
msgid "Use the user's username, but deny enrollment when the username already exists."
|
||||||
msgstr "Use the user's username, but deny enrollment when the username already exists."
|
msgstr "Use the user's username, but deny enrollment when the username already exists."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||||
|
msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||||
|
|
||||||
|
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||||
|
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||||
|
msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||||
|
|
||||||
#: src/pages/tenants/TenantForm.ts
|
#: src/pages/tenants/TenantForm.ts
|
||||||
msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
|
msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
|
||||||
msgstr "Use this tenant for each domain that doesn't have a dedicated tenant."
|
msgstr "Use this tenant for each domain that doesn't have a dedicated tenant."
|
||||||
|
@ -4447,7 +4470,6 @@ msgstr "X509 Subject"
|
||||||
#: src/pages/policies/BoundPoliciesList.ts
|
#: src/pages/policies/BoundPoliciesList.ts
|
||||||
#: src/pages/policies/PolicyTestForm.ts
|
#: src/pages/policies/PolicyTestForm.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
||||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
|
||||||
#: src/pages/tenants/TenantListPage.ts
|
#: src/pages/tenants/TenantListPage.ts
|
||||||
#: src/pages/tokens/TokenListPage.ts
|
#: src/pages/tokens/TokenListPage.ts
|
||||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||||
|
|
|
@ -787,6 +787,10 @@ msgstr ""
|
||||||
msgid "Control how authentik exposes and interprets information."
|
msgid "Control how authentik exposes and interprets information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Cookie domain"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1294,14 +1298,6 @@ msgstr ""
|
||||||
msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
msgid "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#:
|
|
||||||
msgid "Enable forward-auth mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#:
|
|
||||||
msgid "Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
@ -1463,6 +1459,8 @@ msgstr ""
|
||||||
msgid "External Host"
|
msgid "External Host"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
#:
|
||||||
#:
|
#:
|
||||||
msgid "External host"
|
msgid "External host"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1617,7 +1615,11 @@ msgid "Form didn't return a promise for submitting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "Forward auth"
|
msgid "Forward auth (domain level)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Forward auth (single application)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
|
@ -2172,6 +2174,7 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
#:
|
||||||
msgid "Mode"
|
msgid "Mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2304,7 +2307,6 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2506,6 +2508,10 @@ msgstr ""
|
||||||
msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
|
msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
@ -2749,7 +2755,6 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
|
||||||
msgid "Protocol settings"
|
msgid "Protocol settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2782,6 +2787,7 @@ msgstr ""
|
||||||
msgid "Providers"
|
msgid "Providers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
#:
|
#:
|
||||||
msgid "Proxy"
|
msgid "Proxy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -3768,10 +3774,15 @@ msgstr ""
|
||||||
msgid "The URL \"{0}\" was not found."
|
msgid "The URL \"{0}\" was not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
#:
|
#:
|
||||||
msgid "The external URL you'll access the application at. Include any non-standard port."
|
msgid "The external URL you'll access the application at. Include any non-standard port."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "The policy takes a random time to execute. This controls the minimum time it will take."
|
msgid "The policy takes a random time to execute. This controls the minimum time it will take."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -3802,6 +3813,10 @@ msgstr ""
|
||||||
msgid "These policies control which users can access this application."
|
msgid "These policies control which users can access this application."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "This stage can be included in enrollment flows to accept invitations."
|
msgid "This stage can be included in enrollment flows to accept invitations."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -4157,6 +4172,14 @@ msgstr ""
|
||||||
msgid "Use the user's username, but deny enrollment when the username already exists."
|
msgid "Use the user's username, but deny enrollment when the username already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
|
msgid "Use this tenant for each domain that doesn't have a dedicated tenant."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -4437,7 +4460,6 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { ModelForm } from "../../elements/forms/ModelForm";
|
||||||
export class OutpostForm extends ModelForm<Outpost, string> {
|
export class OutpostForm extends ModelForm<Outpost, string> {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
type!: OutpostTypeEnum;
|
type: OutpostTypeEnum = OutpostTypeEnum.Proxy;
|
||||||
|
|
||||||
loadInstance(pk: string): Promise<Outpost> {
|
loadInstance(pk: string): Promise<Outpost> {
|
||||||
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({
|
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesRetrieve({
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import { CryptoApi, FlowsApi, FlowsInstancesListDesignationEnum, ProvidersApi, ProxyProvider } from "authentik-api";
|
import { CryptoApi, FlowsApi, FlowsInstancesListDesignationEnum, ProvidersApi, ProxyMode, ProxyProvider } from "authentik-api";
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import { customElement, property } from "lit-element";
|
import { css, CSSResult, customElement, property } from "lit-element";
|
||||||
import { html, TemplateResult } from "lit-html";
|
import { html, TemplateResult } from "lit-html";
|
||||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
import { ModelForm } from "../../../elements/forms/ModelForm";
|
import { ModelForm } from "../../../elements/forms/ModelForm";
|
||||||
|
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
|
||||||
|
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||||
|
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import "../../../elements/forms/HorizontalFormElement";
|
import "../../../elements/forms/HorizontalFormElement";
|
||||||
|
@ -13,12 +16,20 @@ import { first } from "../../../utils";
|
||||||
@customElement("ak-provider-proxy-form")
|
@customElement("ak-provider-proxy-form")
|
||||||
export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return super.styles.concat(PFToggleGroup, PFContent, PFSpacing, css`
|
||||||
|
.pf-c-toggle-group {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
loadInstance(pk: number): Promise<ProxyProvider> {
|
loadInstance(pk: number): Promise<ProxyProvider> {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({
|
return new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({
|
||||||
id: pk,
|
id: pk,
|
||||||
}).then(provider => {
|
}).then(provider => {
|
||||||
this.showHttpBasic = first(provider.basicAuthEnabled, true);
|
this.showHttpBasic = first(provider.basicAuthEnabled, true);
|
||||||
this.showInternalServer = first(!provider.forwardAuthMode, true);
|
this.mode = first(provider.mode, ProxyMode.Proxy);
|
||||||
return provider;
|
return provider;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -26,8 +37,8 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||||
@property({type: Boolean})
|
@property({type: Boolean})
|
||||||
showHttpBasic = true;
|
showHttpBasic = true;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({attribute: false})
|
||||||
showInternalServer = true;
|
mode: ProxyMode = ProxyMode.Proxy;
|
||||||
|
|
||||||
getSuccessMessage(): string {
|
getSuccessMessage(): string {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
|
@ -38,6 +49,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||||
}
|
}
|
||||||
|
|
||||||
send = (data: ProxyProvider): Promise<ProxyProvider> => {
|
send = (data: ProxyProvider): Promise<ProxyProvider> => {
|
||||||
|
data.mode = this.mode;
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({
|
return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({
|
||||||
id: this.instance.pk || 0,
|
id: this.instance.pk || 0,
|
||||||
|
@ -68,26 +80,94 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||||
</ak-form-element-horizontal>`;
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInternalServer(): TemplateResult {
|
renderModeSelector(): TemplateResult {
|
||||||
if (!this.showInternalServer) {
|
return html`
|
||||||
return html``;
|
<div class="pf-c-toggle-group__item">
|
||||||
|
<button class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy ? "pf-m-selected" : ""}" type="button" @click=${() => {
|
||||||
|
this.mode = ProxyMode.Proxy;
|
||||||
|
}}>
|
||||||
|
<span class="pf-c-toggle-group__text">${t`Proxy`}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||||
|
<div class="pf-c-toggle-group__item">
|
||||||
|
<button class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardSingle ? "pf-m-selected" : ""}" type="button" @click=${() => {
|
||||||
|
this.mode = ProxyMode.ForwardSingle;
|
||||||
|
}}>
|
||||||
|
<span class="pf-c-toggle-group__text">${t`Forward auth (single application)`}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
|
||||||
|
<div class="pf-c-toggle-group__item">
|
||||||
|
<button class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardDomain ? "pf-m-selected" : ""}" type="button" @click=${() => {
|
||||||
|
this.mode = ProxyMode.ForwardDomain;
|
||||||
|
}}>
|
||||||
|
<span class="pf-c-toggle-group__text">${t`Forward auth (domain level)`}</span>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSettings(): TemplateResult {
|
||||||
|
switch (this.mode) {
|
||||||
|
case ProxyMode.Proxy:
|
||||||
|
return html`
|
||||||
|
<p class="pf-u-mb-xl">
|
||||||
|
${t`This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.`}
|
||||||
|
</p>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`External host`}
|
||||||
|
?required=${true}
|
||||||
|
name="externalHost">
|
||||||
|
<input type="text" value="${ifDefined(this.instance?.externalHost)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${t`The external URL you'll access the application at. Include any non-standard port.`}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`Internal host`}
|
||||||
|
?required=${true}
|
||||||
|
name="internalHost">
|
||||||
|
<input type="text" value="${ifDefined(this.instance?.internalHost)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${t`Upstream host that the requests are forwarded to.`}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal name="internalHostSslValidation">
|
||||||
|
<div class="pf-c-check">
|
||||||
|
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.internalHostSslValidation, true)}>
|
||||||
|
<label class="pf-c-check__label">
|
||||||
|
${t`Internal host SSL Validation`}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="pf-c-form__helper-text">${t`Validate SSL Certificates of upstream servers.`}</p>
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
|
case ProxyMode.ForwardSingle:
|
||||||
|
return html`
|
||||||
|
<p class="pf-u-mb-xl">
|
||||||
|
${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you).`}
|
||||||
|
</p>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`External host`}
|
||||||
|
?required=${true}
|
||||||
|
name="externalHost">
|
||||||
|
<input type="text" value="${ifDefined(this.instance?.externalHost)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${t`The external URL you'll access the application at. Include any non-standard port.`}</p>
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
|
case ProxyMode.ForwardDomain:
|
||||||
|
return html`
|
||||||
|
<p class="pf-u-mb-xl">
|
||||||
|
${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.`}
|
||||||
|
</p>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`External host`}
|
||||||
|
?required=${true}
|
||||||
|
name="externalHost">
|
||||||
|
<input type="text" value="${first(this.instance?.externalHost, window.location.origin)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${t`The external URL you'll authenticate at. Can be the same domain as authentik.`}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`Cookie domain`}
|
||||||
|
name="cookieDomain">
|
||||||
|
<input type="text" value="${ifDefined(this.instance?.cookieDomain)}" class="pf-c-form-control" required>
|
||||||
|
<p class="pf-c-form__helper-text">${t`Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.`}</p>
|
||||||
|
</ak-form-element-horizontal>`;
|
||||||
}
|
}
|
||||||
return html`<ak-form-element-horizontal
|
|
||||||
label=${t`Internal host`}
|
|
||||||
?required=${true}
|
|
||||||
name="internalHost">
|
|
||||||
<input type="text" value="${ifDefined(this.instance?.internalHost)}" class="pf-c-form-control" required>
|
|
||||||
<p class="pf-c-form__helper-text">${t`Upstream host that the requests are forwarded to.`}</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal name="internalHostSslValidation">
|
|
||||||
<div class="pf-c-check">
|
|
||||||
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.internalHostSslValidation, true)}>
|
|
||||||
<label class="pf-c-check__label">
|
|
||||||
${t`Internal host SSL Validation`}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<p class="pf-c-form__helper-text">${t`Validate SSL Certificates of upstream servers.`}</p>
|
|
||||||
</ak-form-element-horizontal>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderForm(): TemplateResult {
|
||||||
|
@ -115,35 +195,16 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
|
||||||
<p class="pf-c-form__helper-text">${t`Flow used when authorizing this provider.`}</p>
|
<p class="pf-c-form__helper-text">${t`Flow used when authorizing this provider.`}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-group .expanded=${true}>
|
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||||
<span slot="header">
|
<div class="pf-c-card__body">
|
||||||
${t`Protocol settings`}
|
<div class="pf-c-toggle-group">
|
||||||
</span>
|
${this.renderModeSelector()}
|
||||||
<div slot="body" class="pf-c-form">
|
</div>
|
||||||
<ak-form-element-horizontal
|
|
||||||
label=${t`External host`}
|
|
||||||
?required=${true}
|
|
||||||
name="externalHost">
|
|
||||||
<input type="text" value="${ifDefined(this.instance?.externalHost)}" class="pf-c-form-control" required>
|
|
||||||
<p class="pf-c-form__helper-text">${t`The external URL you'll access the application at. Include any non-standard port.`}</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
<ak-form-element-horizontal name="forwardAuthMode">
|
|
||||||
<div class="pf-c-check">
|
|
||||||
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.forwardAuthMode, false)} @change=${(ev: Event) => {
|
|
||||||
const el = ev.target as HTMLInputElement;
|
|
||||||
this.showInternalServer = !el.checked;
|
|
||||||
}}>
|
|
||||||
<label class="pf-c-check__label">
|
|
||||||
${t`Enable forward-auth mode`}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<p class="pf-c-form__helper-text">
|
|
||||||
${t`Enable this if you don't want to use this provider as a proxy, and want to use it with Traefik's forwardAuth or nginx's auth_request.`}
|
|
||||||
</p>
|
|
||||||
</ak-form-element-horizontal>
|
|
||||||
${this.renderInternalServer()}
|
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
<div class="pf-c-card__footer">
|
||||||
|
${this.renderSettings()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ak-form-group>
|
<ak-form-group>
|
||||||
<span slot="header">
|
<span slot="header">
|
||||||
|
|
|
@ -20,6 +20,7 @@ import "./ProxyProviderForm";
|
||||||
import { ProvidersApi, ProxyProvider } from "authentik-api";
|
import { ProvidersApi, ProxyProvider } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
import { EVENT_REFRESH } from "../../../constants";
|
import { EVENT_REFRESH } from "../../../constants";
|
||||||
|
import { convertToTitle } from "../../../utils";
|
||||||
|
|
||||||
@customElement("ak-provider-proxy-view")
|
@customElement("ak-provider-proxy-view")
|
||||||
export class ProxyProviderViewPage extends LitElement {
|
export class ProxyProviderViewPage extends LitElement {
|
||||||
|
@ -115,18 +116,11 @@ export class ProxyProviderViewPage extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-description-list__group">
|
<div class="pf-c-description-list__group">
|
||||||
<dt class="pf-c-description-list__term">
|
<dt class="pf-c-description-list__term">
|
||||||
<span class="pf-c-description-list__text">${t`Forward auth`}</span>
|
<span class="pf-c-description-list__text">${t`Mode`}</span>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">
|
<div class="pf-c-description-list__text">
|
||||||
${this.provider.forwardAuthMode ?
|
${convertToTitle(this.provider.mode || "")}
|
||||||
html`<span class="pf-c-button__icon pf-m-start">
|
|
||||||
<i class="fas fa-check-circle" aria-hidden="true"></i>
|
|
||||||
</span>${t`Yes`}`:
|
|
||||||
html`<span class="pf-c-button__icon pf-m-start">
|
|
||||||
<i class="fas fa-times-circle" aria-hidden="true"></i>
|
|
||||||
</span>${t`No`}`
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,24 +31,24 @@ ldapsearch \
|
||||||
|
|
||||||
The following fields are currently sent for users:
|
The following fields are currently sent for users:
|
||||||
|
|
||||||
- cn: User's username
|
- `cn`: User's username
|
||||||
- uid: Unique user identifier
|
- `uid`: Unique user identifier
|
||||||
- name: User's name
|
- `name`: User's name
|
||||||
- displayName: User's name
|
- `displayName`: User's name
|
||||||
- mail: User's email address
|
- `mail`: User's email address
|
||||||
- objectClass: A list of these strings:
|
- `objectClass`: A list of these strings:
|
||||||
- "user"
|
- "user"
|
||||||
- "organizationalPerson"
|
- "organizationalPerson"
|
||||||
- "goauthentik.io/ldap/user"
|
- "goauthentik.io/ldap/user"
|
||||||
- accountStatus: "active" if the account is active, otherwise "inactive"
|
- `accountStatus`: "active" if the account is active, otherwise "inactive"
|
||||||
- superuser: "active" if the account is part of a group with superuser permissions, otherwise "inactive"
|
- `superuser`: "active" if the account is part of a group with superuser permissions, otherwise "inactive"
|
||||||
- memberOf: A list of all DNs that the user is a member of
|
- `memberOf`: A list of all DNs that the user is a member of
|
||||||
|
|
||||||
The following fields are current set for groups:
|
The following fields are current set for groups:
|
||||||
|
|
||||||
- cn: The group's name
|
- `cn`: The group's name
|
||||||
- uid: Unique group identifier
|
- `uid`: Unique group identifier
|
||||||
- objectClass: A list of these strings:
|
- `objectClass`: A list of these strings:
|
||||||
- "group"
|
- "group"
|
||||||
- "goauthentik.io/ldap/group"
|
- "goauthentik.io/ldap/group"
|
||||||
|
|
|
@ -1,31 +1,36 @@
|
||||||
---
|
---
|
||||||
title: Proxy Outpost
|
title: Forward auth
|
||||||
---
|
---
|
||||||
|
|
||||||
The proxy outpost sets the following headers:
|
Using forward auth uses your existing reverse proxy to do the proxying, and only uses the
|
||||||
|
authentik outpost to check authentication and authoirzation.
|
||||||
|
|
||||||
```
|
To use forward auth instead of proxying, you have to change a couple of settings.
|
||||||
X-Auth-Username: akadmin # The username of the currently logged in user
|
In the Proxy Provider, make sure to use one of the Forward auth modes.
|
||||||
X-Forwarded-Email: root@localhost # The email address of the currently logged in user
|
|
||||||
X-Forwarded-Preferred-Username: akadmin # The username of the currently logged in user
|
|
||||||
X-Forwarded-User: 900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb # The hashed identifier of the currently logged in user.
|
|
||||||
```
|
|
||||||
|
|
||||||
Additionally, you can set `additionalHeaders` on groups or users to set additional headers.
|
## Single application
|
||||||
|
|
||||||
If you enable *Set HTTP-Basic Authentication* option, the HTTP Authorization header is being set.
|
Single application mode works for a single application hosted on its dedicated subdomain. This
|
||||||
|
has the advantage that you can still do per-application access policies in authentik.
|
||||||
|
|
||||||
# HTTPS
|
## Domain level
|
||||||
|
|
||||||
The outpost listens on both 4180 for HTTP and 4443 for HTTPS.
|
To use forward auth instead of proxying, you have to change a couple of settings.
|
||||||
|
In the Proxy Provider, make sure to use the *Forward auth (domain level)* mode.
|
||||||
|
|
||||||
:::warning
|
This mode differs from the *Forward auth (single application)* mode in the following points:
|
||||||
If your upstream host is HTTPS, and you're not using forward auth, you need to access the outpost over HTTPS too.
|
- You don't have to configure an application in authentik for each domain
|
||||||
:::
|
- Users don't have to authorize multiple times
|
||||||
|
|
||||||
# Forward auth
|
There are however also some downsides, mainly the fact that you **can't** restrict individual
|
||||||
|
applications to different users.
|
||||||
|
|
||||||
To use forward auth instead of proxying, you have to change a couple of settings. In the Proxy Provider, make sure to enable `Enable forward-auth mode` on the provider.
|
The only configuration difference between single application and domain level is the host you specify.
|
||||||
|
|
||||||
|
For single application, you'd use the domain which the application is running on, and only /akprox
|
||||||
|
is redirect to the outpost.
|
||||||
|
|
||||||
|
For domain level, you'd use the same domain as authentik.
|
||||||
|
|
||||||
## Nginx
|
## Nginx
|
||||||
|
|
||||||
|
@ -42,8 +47,8 @@ import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
```
|
```
|
||||||
location /akprox {
|
location /akprox {
|
||||||
proxy_pass http://*ip of your outpost*:4180;
|
proxy_pass http://*ip of your outpost*:4180;
|
||||||
error_page 401 = @akprox_signin;
|
error_page 401 = @akprox_signin;
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||||
add_header Set-Cookie $auth_cookie;
|
add_header Set-Cookie $auth_cookie;
|
24
website/docs/outposts/proxy/proxy.md
Normal file
24
website/docs/outposts/proxy/proxy.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: Proxy Outpost
|
||||||
|
---
|
||||||
|
|
||||||
|
The proxy outpost sets the following headers:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Auth-Username: akadmin # The username of the currently logged in user
|
||||||
|
X-Forwarded-Email: root@localhost # The email address of the currently logged in user
|
||||||
|
X-Forwarded-Preferred-Username: akadmin # The username of the currently logged in user
|
||||||
|
X-Forwarded-User: 900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb # The hashed identifier of the currently logged in user.
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, you can set `additionalHeaders` on groups or users to set additional headers.
|
||||||
|
|
||||||
|
If you enable *Set HTTP-Basic Authentication* option, the HTTP Authorization header is being set.
|
||||||
|
|
||||||
|
# HTTPS
|
||||||
|
|
||||||
|
The outpost listens on both 4180 for HTTP and 4443 for HTTPS.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
If your upstream host is HTTPS, and you're not using forward auth, you need to access the outpost over HTTPS too.
|
||||||
|
:::
|
|
@ -2,6 +2,10 @@
|
||||||
title: Policies
|
title: Policies
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Event-matcher policy
|
||||||
|
|
||||||
|
This policy is used by the events subsystem. You can use this policy to match events by multiple different criteria, to choose when you get notified.
|
||||||
|
|
||||||
## Reputation Policy
|
## Reputation Policy
|
||||||
|
|
||||||
authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one).
|
authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one).
|
||||||
|
@ -12,10 +16,6 @@ This policy can be used, for example, to prompt clients with a low score to pass
|
||||||
|
|
||||||
See [Expression Policy](expression.mdx).
|
See [Expression Policy](expression.mdx).
|
||||||
|
|
||||||
## Password Policies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Password Policy
|
## Password Policy
|
||||||
|
|
||||||
This policy allows you to specify password rules, such as length and required characters.
|
This policy allows you to specify password rules, such as length and required characters.
|
||||||
|
@ -34,3 +34,7 @@ This policy checks the hashed password against the [Have I Been Pwned](https://h
|
||||||
## Password-Expiry Policy
|
## Password-Expiry Policy
|
||||||
|
|
||||||
This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password.
|
This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password.
|
||||||
|
|
||||||
|
## Reputation Policy
|
||||||
|
|
||||||
|
This policy checks the reputation of the client's IP address and the username is attempted to be authenticated as.
|
||||||
|
|
|
@ -20,7 +20,7 @@ This feature is still in technical preview, so please report any Bugs you run in
|
||||||
- Compatibility with forwardAuth/auth_request
|
- Compatibility with forwardAuth/auth_request
|
||||||
|
|
||||||
The authentik proxy is now compatible with forwardAuth (traefik) / auth_request (nginx). All that is required is the latest version of the outpost,
|
The authentik proxy is now compatible with forwardAuth (traefik) / auth_request (nginx). All that is required is the latest version of the outpost,
|
||||||
and the correct config from [here](../outposts/proxy.mdx).
|
and the correct config from [here](../outposts/proxy/forward_auth.mdx).
|
||||||
|
|
||||||
- Docker images for ARM
|
- Docker images for ARM
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,30 @@ module.exports = {
|
||||||
label: "Outposts",
|
label: "Outposts",
|
||||||
items: [
|
items: [
|
||||||
"outposts/outposts",
|
"outposts/outposts",
|
||||||
"outposts/proxy",
|
{
|
||||||
"outposts/ldap",
|
type: "category",
|
||||||
"outposts/upgrading",
|
label: "Running and upgrading",
|
||||||
"outposts/manual-deploy-docker-compose",
|
items: [
|
||||||
"outposts/manual-deploy-kubernetes",
|
"outposts/upgrading",
|
||||||
|
"outposts/manual-deploy-docker-compose",
|
||||||
|
"outposts/manual-deploy-kubernetes",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "Proxy",
|
||||||
|
items: [
|
||||||
|
"outposts/proxy/proxy",
|
||||||
|
"outposts/proxy/forward_auth",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "LDAP",
|
||||||
|
items: [
|
||||||
|
"outposts/ldap/ldap",
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Reference in a new issue