Merge branch 'master' into version-2021.12

This commit is contained in:
Jens Langhammer 2021-12-04 19:55:37 +01:00
commit 639c2f5c2e
93 changed files with 1406 additions and 552 deletions

View file

@ -28,6 +28,7 @@ jobs:
- isort - isort
- bandit - bandit
- pyright - pyright
- pending-migrations
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -34,13 +34,9 @@ WORKDIR /work
COPY --from=web-builder /work/web/robots.txt /work/web/robots.txt COPY --from=web-builder /work/web/robots.txt /work/web/robots.txt
COPY --from=web-builder /work/web/security.txt /work/web/security.txt COPY --from=web-builder /work/web/security.txt /work/web/security.txt
COPY --from=web-builder /work/web/dist/ /work/web/dist/
COPY --from=web-builder /work/web/authentik/ /work/web/authentik/
COPY --from=website-builder /work/website/help/ /work/website/help/
COPY ./cmd /work/cmd COPY ./cmd /work/cmd
COPY ./web/static.go /work/web/static.go COPY ./web/static.go /work/web/static.go
COPY ./website/static.go /work/website/static.go
COPY ./internal /work/internal COPY ./internal /work/internal
COPY ./go.mod /work/go.mod COPY ./go.mod /work/go.mod
COPY ./go.sum /work/go.sum COPY ./go.sum /work/go.sum
@ -78,6 +74,9 @@ COPY ./tests /tests
COPY ./manage.py / COPY ./manage.py /
COPY ./lifecycle/ /lifecycle COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy COPY --from=builder /work/authentik /authentik-proxy
COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/help/ /website/help/
USER authentik USER authentik

View file

@ -68,7 +68,7 @@ gen-outpost:
docker run \ docker run \
--rm -v ${PWD}:/local \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
openapitools/openapi-generator-cli generate \ openapitools/openapi-generator-cli:v5.2.1 generate \
-i /local/schema.yml \ -i /local/schema.yml \
-g go \ -g go \
-o /local/api \ -o /local/api \
@ -113,3 +113,6 @@ ci-bandit:
ci-pyright: ci-pyright:
pyright e2e lifecycle pyright e2e lifecycle
ci-pending-migrations:
./manage.py makemigrations --check

View file

@ -11,12 +11,7 @@ from structlog.stdlib import get_logger
from authentik import ENV_GIT_HASH_KEY, __version__ from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.events.models import Event, EventAction, Notification from authentik.events.models import Event, EventAction, Notification
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -53,9 +48,8 @@ def clear_update_notifications():
notification.delete() notification.delete()
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def update_latest_version(self: PrefilledMonitoredTask):
def update_latest_version(self: MonitoredTask):
"""Update latest version info""" """Update latest version info"""
if CONFIG.y_bool("disable_update_check"): if CONFIG.y_bool("disable_update_check"):
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)

View file

@ -16,21 +16,15 @@ from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, ExpiringModel from authentik.core.models import AuthenticatedSession, ExpiringModel
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def clean_expired_models(self: PrefilledMonitoredTask):
def clean_expired_models(self: MonitoredTask):
"""Remove expired objects""" """Remove expired objects"""
messages = [] messages = []
for cls in ExpiringModel.__subclasses__(): for cls in ExpiringModel.__subclasses__():
@ -68,9 +62,8 @@ def should_backup() -> bool:
return True return True
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def backup_database(self: PrefilledMonitoredTask): # pragma: no cover
def backup_database(self: MonitoredTask): # pragma: no cover
"""Database backup""" """Database backup"""
self.result_timeout_hours = 25 self.result_timeout_hours = 25
if not should_backup(): if not should_backup():

View file

@ -20,6 +20,7 @@ from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.managed import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -141,9 +142,11 @@ class CertificateKeyPairFilter(FilterSet):
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"""CertificateKeyPair Viewset""" """CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False) queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
serializer_class = CertificateKeyPairSerializer serializer_class = CertificateKeyPairSerializer
filterset_class = CertificateKeyPairFilter filterset_class = CertificateKeyPairFilter
ordering = ["name"]
search_fields = ["name"]
@permission_required(None, ["authentik_crypto.add_certificatekeypair"]) @permission_required(None, ["authentik_crypto.add_certificatekeypair"])
@extend_schema( @extend_schema(

View file

@ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig):
def ready(self): def ready(self):
import_module("authentik.crypto.managed") import_module("authentik.crypto.managed")
import_module("authentik.crypto.tasks")

View file

@ -0,0 +1,10 @@
"""Crypto task Settings"""
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"crypto_certificate_discovery": {
"task": "authentik.crypto.tasks.certificate_discovery",
"schedule": crontab(minute="*/5"),
"options": {"queue": "authentik_scheduled"},
},
}

67
authentik/crypto/tasks.py Normal file
View file

@ -0,0 +1,67 @@
"""Crypto tasks"""
from glob import glob
from pathlib import Path
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
def certificate_discovery(self: PrefilledMonitoredTask):
"""Discover and update certificates form 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
# 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 "BEGIN RSA PRIVATE KEY" in body:
private_keys[cert_name] = body
else:
certs[cert_name] = body
except OSError as exc:
LOGGER.warning("Failed to open file", 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})],
)
)

View file

@ -1,5 +1,7 @@
"""Crypto tests""" """Crypto tests"""
import datetime import datetime
from os import makedirs
from tempfile import TemporaryDirectory
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -9,6 +11,8 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_cert,
from authentik.crypto.api import CertificateKeyPairSerializer from authentik.crypto.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_key from authentik.lib.generators import generate_key
from authentik.providers.oauth2.models import OAuth2Provider from authentik.providers.oauth2.models import OAuth2Provider
@ -163,3 +167,33 @@ class TestCrypto(APITestCase):
} }
], ],
) )
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
self.assertTrue(
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo").exists()
)
self.assertTrue(
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
)

View file

@ -112,30 +112,6 @@ class TaskInfo:
cache.set(key, self, timeout=timeout_hours * 60 * 60) cache.set(key, self, timeout=timeout_hours * 60 * 60)
def prefill_task():
"""Ensure a task's details are always in cache, so it can always be triggered via API"""
def inner_wrap(func):
status = TaskInfo.by_name(func.__name__)
if status:
return func
TaskInfo(
task_name=func.__name__,
task_description=func.__doc__,
result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]),
task_call_module=func.__module__,
task_call_func=func.__name__,
# We don't have real values for these attributes but they cannot be null
start_timestamp=default_timer(),
finish_timestamp=default_timer(),
finish_time=datetime.now(),
).save(86400)
LOGGER.debug("prefilled task", task_name=func.__name__)
return func
return inner_wrap
class MonitoredTask(Task): class MonitoredTask(Task):
"""Task which can save its state to the cache""" """Task which can save its state to the cache"""
@ -210,5 +186,31 @@ class MonitoredTask(Task):
raise NotImplementedError raise NotImplementedError
class PrefilledMonitoredTask(MonitoredTask):
"""Subclass of MonitoredTask, but create entry in cache if task hasn't been run
Does not support UID"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
status = TaskInfo.by_name(self.__name__)
if status:
return
TaskInfo(
task_name=self.__name__,
task_description=self.__doc__,
result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]),
task_call_module=self.__module__,
task_call_func=self.__name__,
# We don't have real values for these attributes but they cannot be null
start_timestamp=default_timer(),
finish_timestamp=default_timer(),
finish_time=datetime.now(),
).save(86400)
LOGGER.debug("prefilled task", task_name=self.__name__)
def run(self, *args, **kwargs):
raise NotImplementedError
for task in TaskInfo.all().values(): for task in TaskInfo.all().values():
task.set_prom_metrics() task.set_prom_metrics()

View file

@ -53,6 +53,7 @@ NEXT_ARG_NAME = "next"
SESSION_KEY_PLAN = "authentik_flows_plan" SESSION_KEY_PLAN = "authentik_flows_plan"
SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre" SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
SESSION_KEY_GET = "authentik_flows_get" SESSION_KEY_GET = "authentik_flows_get"
SESSION_KEY_POST = "authentik_flows_post"
SESSION_KEY_HISTORY = "authentik_flows_history" SESSION_KEY_HISTORY = "authentik_flows_history"

View file

@ -47,6 +47,7 @@ error_reporting:
enabled: false enabled: false
environment: customer environment: customer
send_pii: false send_pii: false
sample_rate: 0.5
# Global email settings # Global email settings
email: email:
@ -82,3 +83,4 @@ default_user_change_email: true
default_user_change_username: true default_user_change_username: true
gdpr_compliance: true gdpr_compliance: true
cert_discovery_dir: /certs

View file

@ -68,9 +68,9 @@ class DomainlessURLValidator(URLValidator):
) )
self.schemes = ["http", "https", "blank"] + list(self.schemes) self.schemes = ["http", "https", "blank"] + list(self.schemes)
def __call__(self, value): def __call__(self, value: str):
# Check if the scheme is valid. # Check if the scheme is valid.
scheme = value.split("://")[0].lower() scheme = value.split("://")[0].lower()
if scheme not in self.schemes: if scheme not in self.schemes:
value = "default" + value value = "default" + value
return super().__call__(value) super().__call__(value)

View file

@ -2,18 +2,12 @@
from django.db import DatabaseError from django.db import DatabaseError
from authentik.core.tasks import CELERY_APP from authentik.core.tasks import CELERY_APP
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.managed.manager import ObjectManager from authentik.managed.manager import ObjectManager
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def managed_reconcile(self: PrefilledMonitoredTask):
def managed_reconcile(self: MonitoredTask):
"""Run ObjectManager to ensure objects are up-to-date""" """Run ObjectManager to ensure objects are up-to-date"""
try: try:
ObjectManager().run() ObjectManager().run()

View file

@ -19,8 +19,9 @@ class AuthentikOutpostConfig(AppConfig):
import_module("authentik.outposts.signals") import_module("authentik.outposts.signals")
import_module("authentik.outposts.managed") import_module("authentik.outposts.managed")
try: try:
from authentik.outposts.tasks import outpost_local_connection from authentik.outposts.tasks import outpost_controller_all, outpost_local_connection
outpost_local_connection.delay() outpost_local_connection.delay()
outpost_controller_all.delay()
except ProgrammingError: except ProgrammingError:
pass pass

View file

@ -19,9 +19,9 @@ from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import (
MonitoredTask, MonitoredTask,
PrefilledMonitoredTask,
TaskResult, TaskResult,
TaskResultStatus, TaskResultStatus,
prefill_task,
) )
from authentik.lib.utils.reflection import path_to_class from authentik.lib.utils.reflection import path_to_class
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseController, ControllerException
@ -75,9 +75,8 @@ def outpost_service_connection_state(connection_pk: Any):
cache.set(connection.state_key, state, timeout=None) cache.set(connection.state_key, state, timeout=None)
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def outpost_service_connection_monitor(self: PrefilledMonitoredTask):
def outpost_service_connection_monitor(self: MonitoredTask):
"""Regularly check the state of Outpost Service Connections""" """Regularly check the state of Outpost Service Connections"""
connections = OutpostServiceConnection.objects.all() connections = OutpostServiceConnection.objects.all()
for connection in connections.iterator(): for connection in connections.iterator():
@ -125,9 +124,8 @@ def outpost_controller(
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs)) self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def outpost_token_ensurer(self: PrefilledMonitoredTask):
def outpost_token_ensurer(self: MonitoredTask):
"""Periodically ensure that all Outposts have valid Service Accounts """Periodically ensure that all Outposts have valid Service Accounts
and Tokens""" and Tokens"""
all_outposts = Outpost.objects.all() all_outposts = Outpost.objects.all()

View file

@ -69,8 +69,8 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"), ("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"), ("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.tenants", "authentik Tenants"), ("authentik.tenants", "authentik Tenants"),
("authentik.core", "authentik Core"),
("authentik.managed", "authentik Managed"), ("authentik.managed", "authentik Managed"),
("authentik.core", "authentik Core"),
], ],
default="", default="",
help_text="Match events created by selected application. When left empty, all applications are matched.", help_text="Match events created by selected application. When left empty, all applications are matched.",

View file

@ -2,12 +2,7 @@
from django.core.cache import cache from django.core.cache import cache
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.policies.reputation.models import IPReputation, UserReputation from authentik.policies.reputation.models import IPReputation, UserReputation
from authentik.policies.reputation.signals import CACHE_KEY_IP_PREFIX, CACHE_KEY_USER_PREFIX from authentik.policies.reputation.signals import CACHE_KEY_IP_PREFIX, CACHE_KEY_USER_PREFIX
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -15,9 +10,8 @@ from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def save_ip_reputation(self: PrefilledMonitoredTask):
def save_ip_reputation(self: MonitoredTask):
"""Save currently cached reputation to database""" """Save currently cached reputation to database"""
objects_to_update = [] objects_to_update = []
for key, score in cache.get_many(cache.keys(CACHE_KEY_IP_PREFIX + "*")).items(): for key, score in cache.get_many(cache.keys(CACHE_KEY_IP_PREFIX + "*")).items():
@ -29,9 +23,8 @@ def save_ip_reputation(self: MonitoredTask):
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated IP Reputation"])) self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated IP Reputation"]))
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def save_user_reputation(self: PrefilledMonitoredTask):
def save_user_reputation(self: MonitoredTask):
"""Save currently cached reputation to database""" """Save currently cached reputation to database"""
objects_to_update = [] objects_to_update = []
for key, score in cache.get_many(cache.keys(CACHE_KEY_USER_PREFIX + "*")).items(): for key, score in cache.get_many(cache.keys(CACHE_KEY_USER_PREFIX + "*")).items():

View file

@ -10,7 +10,7 @@ from django.views.generic.base import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User from authentik.core.models import Application, Provider, User
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
@ -84,6 +84,10 @@ class PolicyAccessView(AccessMixin, View):
a hint on the Identification Stage what the user should login for.""" a hint on the Identification Stage what the user should login for."""
if self.application: if self.application:
self.request.session[SESSION_KEY_APPLICATION_PRE] = self.application self.request.session[SESSION_KEY_APPLICATION_PRE] = self.application
# Because this view might get hit with a POST request, we need to preserve that data
# since later views might need it (mostly SAML)
if self.request.method.lower() == "post":
self.request.session[SESSION_KEY_POST] = self.request.POST
return redirect_to_login( return redirect_to_login(
self.request.get_full_path(), self.request.get_full_path(),
self.get_login_url(), self.get_login_url(),

View file

@ -3,7 +3,7 @@ from typing import Any, Optional
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, ReadOnlyField, SerializerMethodField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -109,6 +109,9 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
class ProxyOutpostConfigSerializer(ModelSerializer): class ProxyOutpostConfigSerializer(ModelSerializer):
"""Proxy provider serializer for outposts""" """Proxy provider serializer for outposts"""
assigned_application_slug = ReadOnlyField(source="application.slug")
assigned_application_name = ReadOnlyField(source="application.name")
oidc_configuration = SerializerMethodField() oidc_configuration = SerializerMethodField()
token_validity = SerializerMethodField() token_validity = SerializerMethodField()
scopes_to_request = SerializerMethodField() scopes_to_request = SerializerMethodField()
@ -152,6 +155,8 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
"cookie_domain", "cookie_domain",
"token_validity", "token_validity",
"scopes_to_request", "scopes_to_request",
"assigned_application_slug",
"assigned_application_name",
] ]

View file

@ -20,9 +20,11 @@ class TraefikMiddlewareSpecForwardAuth:
address: str address: str
# pylint: disable=invalid-name # pylint: disable=invalid-name
authResponseHeaders: list[str] authResponseHeadersRegex: str = field(default="")
# pylint: disable=invalid-name # pylint: disable=invalid-name
trustForwardHeader: bool authResponseHeaders: list[str] = field(default_factory=list)
# pylint: disable=invalid-name
trustForwardHeader: bool = field(default=True)
@dataclass @dataclass
@ -108,21 +110,8 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
spec=TraefikMiddlewareSpec( spec=TraefikMiddlewareSpec(
forwardAuth=TraefikMiddlewareSpecForwardAuth( forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik", address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
authResponseHeaders=[ authResponseHeaders=[],
"Set-Cookie", authResponseHeadersRegex="^.*$",
# Legacy headers, remove after 2022.1
"X-Auth-Username",
"X-Auth-Groups",
"X-Forwarded-Email",
"X-Forwarded-Preferred-Username",
"X-Forwarded-User",
# New headers, unique prefix
"X-authentik-username",
"X-authentik-groups",
"X-authentik-email",
"X-authentik-name",
"X-authentik-uid",
],
trustForwardHeader=True, trustForwardHeader=True,
) )
), ),

View file

@ -100,14 +100,13 @@ class AuthNRequestParser:
xmlsec.tree.add_ids(root, ["ID"]) xmlsec.tree.add_ids(root, ["ID"])
signature_nodes = root.xpath("/samlp:AuthnRequest/ds:Signature", namespaces=NS_MAP) signature_nodes = root.xpath("/samlp:AuthnRequest/ds:Signature", namespaces=NS_MAP)
# No signatures, no verifier configured -> decode xml directly # No signatures, no verifier configured -> decode xml directly
if len(signature_nodes) < 1 and not verifier: if len(signature_nodes) < 1:
if not verifier:
return self._parse_xml(decoded_xml, relay_state) return self._parse_xml(decoded_xml, relay_state)
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
signature_node = signature_nodes[0] signature_node = signature_nodes[0]
if verifier and signature_node is None:
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
if signature_node is not None: if signature_node is not None:
if not verifier: if not verifier:
raise CannotHandleAssertion(ERROR_SIGNATURE_EXISTS_BUT_NO_VERIFIER) raise CannotHandleAssertion(ERROR_SIGNATURE_EXISTS_BUT_NO_VERIFIER)

View file

@ -13,7 +13,7 @@ from authentik.core.models import Application
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.models import in_memory_stage from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView from authentik.policies.views import PolicyAccessView
@ -37,7 +37,7 @@ LOGGER = get_logger()
class SAMLSSOView(PolicyAccessView): class SAMLSSOView(PolicyAccessView):
""" "SAML SSO Base View, which plans a flow and injects our final stage. """SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler.""" Calls get/post handler."""
def resolve_provider_application(self): def resolve_provider_application(self):
@ -120,14 +120,20 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
def check_saml_request(self) -> Optional[HttpRequest]: def check_saml_request(self) -> Optional[HttpRequest]:
"""Handle POST bindings""" """Handle POST bindings"""
if REQUEST_KEY_SAML_REQUEST not in self.request.POST: payload = self.request.POST
# Restore the post body from the session
# This happens when using POST bindings but the user isn't logged in
# (user gets redirected and POST body is 'lost')
if SESSION_KEY_POST in self.request.session:
payload = self.request.session[SESSION_KEY_POST]
if REQUEST_KEY_SAML_REQUEST not in payload:
LOGGER.info("check_saml_request: SAML payload missing") LOGGER.info("check_saml_request: SAML payload missing")
return bad_request_message(self.request, "The SAML request payload is missing.") return bad_request_message(self.request, "The SAML request payload is missing.")
try: try:
auth_n_request = AuthNRequestParser(self.provider).parse( auth_n_request = AuthNRequestParser(self.provider).parse(
self.request.POST[REQUEST_KEY_SAML_REQUEST], payload[REQUEST_KEY_SAML_REQUEST],
self.request.POST.get(REQUEST_KEY_RELAY_STATE), payload.get(REQUEST_KEY_RELAY_STATE),
) )
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
except CannotHandleAssertion as exc: except CannotHandleAssertion as exc:

View file

@ -424,7 +424,7 @@ if _ERROR_REPORTING:
], ],
before_send=before_send, before_send=before_send,
release=f"authentik@{__version__}", release=f"authentik@{__version__}",
traces_sample_rate=float(CONFIG.y("error_reporting.sample_rate", 0.4)), traces_sample_rate=float(CONFIG.y("error_reporting.sample_rate", 0.5)),
environment=CONFIG.y("error_reporting.environment", "customer"), environment=CONFIG.y("error_reporting.environment", "customer"),
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False), send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
) )

View file

@ -43,6 +43,7 @@ class LDAPSourceSerializer(SourceSerializer):
model = LDAPSource model = LDAPSource
fields = SourceSerializer.Meta.fields + [ fields = SourceSerializer.Meta.fields + [
"server_uri", "server_uri",
"peer_certificate",
"bind_cn", "bind_cn",
"bind_password", "bind_password",
"start_tls", "start_tls",
@ -73,11 +74,9 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"name", "name",
"slug", "slug",
"enabled", "enabled",
"authentication_flow",
"enrollment_flow",
"policy_engine_mode",
"server_uri", "server_uri",
"bind_cn", "bind_cn",
"peer_certificate",
"start_tls", "start_tls",
"base_dn", "base_dn",
"additional_user_dn", "additional_user_dn",

View file

@ -58,7 +58,7 @@ class LDAPBackend(InbuiltBackend):
LOGGER.debug("Attempting Binding as user", user=user) LOGGER.debug("Attempting Binding as user", user=user)
try: try:
temp_connection = ldap3.Connection( temp_connection = ldap3.Connection(
source.connection.server, source.server,
user=user.attributes.get(LDAP_DISTINGUISHED_NAME), user=user.attributes.get(LDAP_DISTINGUISHED_NAME),
password=password, password=password,
raise_exceptions=True, raise_exceptions=True,

View file

@ -0,0 +1,38 @@
# Generated by Django 3.2.9 on 2021-12-03 09:00
import django.db.models.deletion
from django.db import migrations, models
import authentik.sources.ldap.models
class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0003_certificatekeypair_managed"),
("authentik_sources_ldap", "0001_squashed_0012_auto_20210812_1703"),
]
operations = [
migrations.AddField(
model_name="ldapsource",
name="peer_certificate",
field=models.ForeignKey(
default=None,
help_text="Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_crypto.certificatekeypair",
),
),
migrations.AlterField(
model_name="ldapsource",
name="server_uri",
field=models.TextField(
validators=[
authentik.sources.ldap.models.MultiURLValidator(schemes=["ldap", "ldaps"])
],
verbose_name="Server URI",
),
),
]

View file

@ -1,24 +1,48 @@
"""authentik LDAP Models""" """authentik LDAP Models"""
from typing import Optional, Type from ssl import CERT_REQUIRED
from typing import Type
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, Connection, Server from ldap3 import ALL, RANDOM, Connection, Server, ServerPool, Tls
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from authentik.core.models import Group, PropertyMapping, Source from authentik.core.models import Group, PropertyMapping, Source
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator from authentik.lib.models import DomainlessURLValidator
LDAP_TIMEOUT = 15 LDAP_TIMEOUT = 15
class MultiURLValidator(DomainlessURLValidator):
"""Same as DomainlessURLValidator but supports multiple URLs separated with a comma."""
def __call__(self, value: str):
if "," in value:
for url in value.split(","):
super().__call__(url)
else:
super().__call__(value)
class LDAPSource(Source): class LDAPSource(Source):
"""Federate LDAP Directory with authentik, or create new accounts in LDAP.""" """Federate LDAP Directory with authentik, or create new accounts in LDAP."""
server_uri = models.TextField( server_uri = models.TextField(
validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])], validators=[MultiURLValidator(schemes=["ldap", "ldaps"])],
verbose_name=_("Server URI"), verbose_name=_("Server URI"),
) )
peer_certificate = models.ForeignKey(
CertificateKeyPair,
on_delete=models.SET_DEFAULT,
default=None,
null=True,
help_text=_(
"Optionally verify the LDAP Server's Certificate "
"against the CA Chain in this keypair."
),
)
bind_cn = models.TextField(verbose_name=_("Bind CN"), blank=True) bind_cn = models.TextField(verbose_name=_("Bind CN"), blank=True)
bind_password = models.TextField(blank=True) bind_password = models.TextField(blank=True)
start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS")) start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS"))
@ -82,25 +106,40 @@ class LDAPSource(Source):
return LDAPSourceSerializer return LDAPSourceSerializer
_connection: Optional[Connection] = None @property
def server(self) -> Server:
"""Get LDAP Server/ServerPool"""
servers = []
tls = Tls()
if self.peer_certificate:
tls = Tls(ca_certs_data=self.peer_certificate.certificate_data, validate=CERT_REQUIRED)
kwargs = {
"get_info": ALL,
"connect_timeout": LDAP_TIMEOUT,
"tls": tls,
}
if "," in self.server_uri:
for server in self.server_uri.split(","):
servers.append(Server(server, **kwargs))
else:
servers = [Server(self.server_uri, **kwargs)]
return ServerPool(servers, RANDOM, active=True, exhaust=True)
@property @property
def connection(self) -> Connection: def connection(self) -> Connection:
"""Get a fully connected and bound LDAP Connection""" """Get a fully connected and bound LDAP Connection"""
if not self._connection: connection = Connection(
server = Server(self.server_uri, get_info=ALL, connect_timeout=LDAP_TIMEOUT) self.server,
self._connection = Connection(
server,
raise_exceptions=True, raise_exceptions=True,
user=self.bind_cn, user=self.bind_cn,
password=self.bind_password, password=self.bind_password,
receive_timeout=LDAP_TIMEOUT, receive_timeout=LDAP_TIMEOUT,
) )
self._connection.bind() connection.bind()
if self.start_tls: if self.start_tls:
self._connection.start_tls() connection.start_tls()
return self._connection return connection
class Meta: class Meta:

View file

@ -51,7 +51,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
}, },
defaults, defaults,
) )
except (IntegrityError, FieldError) as exc: except (IntegrityError, FieldError, TypeError) as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=( message=(

View file

@ -45,7 +45,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
ak_user, created = self.update_or_create_attributes( ak_user, created = self.update_or_create_attributes(
User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults
) )
except (IntegrityError, FieldError) as exc: except (IntegrityError, FieldError, TypeError) as exc:
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=( message=(

View file

@ -39,7 +39,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
# to set the state with # to set the state with
return return
sync = path_to_class(sync_class) sync = path_to_class(sync_class)
self.set_uid(f"{slugify(source.name)}-{sync.__name__}") self.set_uid(f"{slugify(source.name)}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
try: try:
sync_inst = sync(source) sync_inst = sync(source)
count = sync_inst.sync() count = sync_inst.sync()

View file

@ -120,9 +120,9 @@ class LDAPSyncTests(TestCase):
self.source.property_mappings_group.set( self.source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name") LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
) )
self.source.save()
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD)) connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
group_sync = GroupLDAPSynchronizer(self.source) group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync() group_sync.sync()
membership_sync = MembershipLDAPSynchronizer(self.source) membership_sync = MembershipLDAPSynchronizer(self.source)
@ -143,9 +143,9 @@ class LDAPSyncTests(TestCase):
self.source.property_mappings_group.set( self.source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn") LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
) )
self.source.save()
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
group_sync = GroupLDAPSynchronizer(self.source) group_sync = GroupLDAPSynchronizer(self.source)
group_sync.sync() group_sync.sync()
membership_sync = MembershipLDAPSynchronizer(self.source) membership_sync = MembershipLDAPSynchronizer(self.source)
@ -168,9 +168,9 @@ class LDAPSyncTests(TestCase):
self.source.property_mappings_group.set( self.source.property_mappings_group.set(
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn") LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
) )
self.source.save()
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD)) connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
self.source.save()
user_sync = UserLDAPSynchronizer(self.source) user_sync = UserLDAPSynchronizer(self.source)
user_sync.sync() user_sync.sync()
group_sync = GroupLDAPSynchronizer(self.source) group_sync = GroupLDAPSynchronizer(self.source)

View file

@ -29,14 +29,15 @@ def check_plex_token(self: MonitoredTask, source_slug: int):
auth.get_user_info() auth.get_user_info()
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Plex token is valid."])) self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Plex token is valid."]))
except RequestException as exc: except RequestException as exc:
error = exception_to_string(exc).replace(source.plex_token, "$PLEX_TOKEN")
self.set_status( self.set_status(
TaskResult( TaskResult(
TaskResultStatus.ERROR, TaskResultStatus.ERROR,
["Plex token is invalid/an error occurred:", exception_to_string(exc)], ["Plex token is invalid/an error occurred:", error],
) )
) )
Event.new( Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=f"Plex token invalid, please re-authenticate source.\n{str(exc)}", message=f"Plex token invalid, please re-authenticate source.\n{error}",
source=source, source=source,
).save() ).save()

View file

@ -3,12 +3,7 @@ from django.utils.timezone import now
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User from authentik.core.models import AuthenticatedSession, User
from authentik.events.monitored_tasks import ( from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
MonitoredTask,
TaskResult,
TaskResultStatus,
prefill_task,
)
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
@ -16,9 +11,8 @@ from authentik.sources.saml.models import SAMLSource
LOGGER = get_logger() LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
@prefill_task() def clean_temporary_users(self: PrefilledMonitoredTask):
def clean_temporary_users(self: MonitoredTask):
"""Remove temporary users created by SAML Sources""" """Remove temporary users created by SAML Sources"""
_now = now() _now = now()
messages = [] messages = []

View file

@ -0,0 +1,43 @@
# Generated by Django 3.2.9 on 2021-12-03 09:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_prompt", "0005_alter_prompt_field_key"),
]
operations = [
migrations.AlterField(
model_name="prompt",
name="type",
field=models.CharField(
choices=[
("text", "Text: Simple Text input"),
(
"text_read_only",
"Text (read-only): Simple Text input, but cannot be edited.",
),
(
"username",
"Username: Same as Text input, but checks for and prevents duplicate usernames.",
),
("email", "Email: Text field with Email type."),
(
"password",
"Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.",
),
("number", "Number"),
("checkbox", "Checkbox"),
("date", "Date"),
("date-time", "Date Time"),
("separator", "Separator: Static Separator Line"),
("hidden", "Hidden: Hidden field, can be used to insert data into form."),
("static", "Static: Static value, displayed as-is."),
],
max_length=100,
),
),
]

View file

@ -113,6 +113,9 @@ class Prompt(SerializerModel):
kwargs["label"] = "" kwargs["label"] = ""
if default: if default:
kwargs["default"] = default kwargs["default"] = default
# May not set both `required` and `default`
if "default" in kwargs:
kwargs.pop("required", None)
return field_class(**kwargs) return field_class(**kwargs)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View file

@ -55,6 +55,7 @@ services:
volumes: volumes:
- ./backups:/backups - ./backups:/backups
- ./media:/media - ./media:/media
- ./certs:/certs
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./custom-templates:/templates - ./custom-templates:/templates
- geoip:/geoip - geoip:/geoip

3
go.mod
View file

@ -29,10 +29,11 @@ require (
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/recws-org/recws v1.3.1 github.com/recws-org/recws v1.3.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
goauthentik.io/api v0.2021104.6 goauthentik.io/api v0.2021104.7
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect

6
go.sum
View file

@ -561,8 +561,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
goauthentik.io/api v0.2021104.6 h1:1Vyw1gnVm9D7htUXWTcy7Gg7ldU0V0vIhT8RFo9G/Iw= goauthentik.io/api v0.2021104.7 h1:JWKypuvYWWPqq8c8xLN8qVv5ny8TqsfmLdqNwJM9bZk=
goauthentik.io/api v0.2021104.6/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE= goauthentik.io/api v0.2021104.7/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -672,6 +672,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View file

@ -1,5 +1,11 @@
package constants package constants
const (
OCTop = "top"
OCDomain = "domain"
OCNSContainer = "nsContainer"
)
const ( const (
OCGroup = "group" OCGroup = "group"
OCGroupOfUniqueNames = "groupOfUniqueNames" OCGroupOfUniqueNames = "groupOfUniqueNames"
@ -19,3 +25,42 @@ const (
OUGroups = "groups" OUGroups = "groups"
OUVirtualGroups = "virtual-groups" OUVirtualGroups = "virtual-groups"
) )
func GetDomainOCs() map[string]bool {
return map[string]bool{
OCTop: true,
OCDomain: true,
}
}
func GetContainerOCs() map[string]bool {
return map[string]bool{
OCTop: true,
OCNSContainer: true,
}
}
func GetUserOCs() map[string]bool {
return map[string]bool{
OCUser: true,
OCOrgPerson: true,
OCInetOrgPerson: true,
OCAKUser: true,
}
}
func GetGroupOCs() map[string]bool {
return map[string]bool{
OCGroup: true,
OCGroupOfUniqueNames: true,
OCAKGroup: true,
}
}
func GetVirtualGroupOCs() map[string]bool {
return map[string]bool{
OCGroup: true,
OCGroupOfUniqueNames: true,
OCAKVirtualGroup: true,
}
}

View file

@ -2,14 +2,20 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"strings"
"sync" "sync"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ldap/bind" "goauthentik.io/internal/outpost/ldap/bind"
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/flags" "goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/search" "goauthentik.io/internal/outpost/ldap/search"
"goauthentik.io/internal/outpost/ldap/utils"
) )
type ProviderInstance struct { type ProviderInstance struct {
@ -50,6 +56,10 @@ func (pi *ProviderInstance) GetBaseGroupDN() string {
return pi.GroupDN return pi.GroupDN
} }
func (pi *ProviderInstance) GetBaseVirtualGroupDN() string {
return pi.VirtualGroupDN
}
func (pi *ProviderInstance) GetBaseUserDN() string { func (pi *ProviderInstance) GetBaseUserDN() string {
return pi.UserDN return pi.UserDN
} }
@ -82,3 +92,77 @@ func (pi *ProviderInstance) GetFlowSlug() string {
func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID { func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
return pi.searchAllowedGroups return pi.searchAllowedGroups
} }
func (pi *ProviderInstance) GetBaseEntry() *ldap.Entry {
return &ldap.Entry{
DN: pi.GetBaseDN(),
Attributes: []*ldap.EntryAttribute{
{
Name: "distinguishedName",
Values: []string{pi.GetBaseDN()},
},
{
Name: "objectClass",
Values: []string{ldapConstants.OCTop, ldapConstants.OCDomain},
},
{
Name: "supportedLDAPVersion",
Values: []string{"3"},
},
{
Name: "namingContexts",
Values: []string{
pi.GetBaseDN(),
pi.GetBaseUserDN(),
pi.GetBaseGroupDN(),
pi.GetBaseVirtualGroupDN(),
},
},
{
Name: "vendorName",
Values: []string{"goauthentik.io"},
},
{
Name: "vendorVersion",
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
},
},
}
}
func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool) {
needUsers := false
needGroups := false
// We only want to load users/groups if we're actually going to be asked
// for at least one user or group based on the search's base DN and scope.
//
// If our requested base DN doesn't match any of the container DNs, then
// we're probably loading a user or group. If it does, then make sure our
// scope will eventually take us to users or groups.
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) {
if baseDN != pi.UserDN && baseDN != pi.BaseDN ||
baseDN == pi.BaseDN && scope > 1 ||
baseDN == pi.UserDN && scope > 0 {
needUsers = true
}
}
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) {
if baseDN != pi.GroupDN && baseDN != pi.BaseDN ||
baseDN == pi.BaseDN && scope > 1 ||
baseDN == pi.GroupDN && scope > 0 {
needGroups = true
}
}
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) {
if baseDN != pi.VirtualGroupDN && baseDN != pi.BaseDN ||
baseDN == pi.BaseDN && scope > 1 ||
baseDN == pi.VirtualGroupDN && scope > 0 {
needUsers = true
}
}
return needUsers, needGroups
}

View file

@ -4,16 +4,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/outpost/ldap/constants" "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/group" "goauthentik.io/internal/outpost/ldap/group"
"goauthentik.io/internal/outpost/ldap/metrics" "goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/outpost/ldap/search" "goauthentik.io/internal/outpost/ldap/search"
@ -35,26 +34,11 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
return ds return ds
} }
func (ds *DirectSearcher) SearchMe(req *search.Request, f flags.UserFlags) (ldap.ServerSearchResult, error) {
if f.UserInfo == nil {
u, _, err := ds.si.GetAPIClient().CoreApi.CoreUsersRetrieve(req.Context(), f.UserPk).Execute()
if err != nil {
req.Log().WithError(err).Warning("Failed to get user info")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
}
f.UserInfo = &u
}
entries := make([]*ldap.Entry, 1)
entries[0] = ds.si.UserEntry(*f.UserInfo)
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
baseDN := strings.ToLower("," + ds.si.GetBaseDN()) baseDN := strings.ToLower(ds.si.GetBaseDN())
entries := []*ldap.Entry{} filterOC, err := ldap.GetFilterObjectClass(req.Filter)
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil { if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{ metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(), "outpost_name": ds.si.GetOutpostName(),
@ -75,7 +59,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
}).Inc() }).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
} }
if !strings.HasSuffix(req.BindDN, baseDN) { if !strings.HasSuffix(req.BindDN, ","+baseDN) {
metrics.RequestsRejected.With(prometheus.Labels{ metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(), "outpost_name": ds.si.GetOutpostName(),
"type": "search", "type": "search",
@ -98,15 +82,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
}).Inc() }).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
} }
if req.Scope == ldap.ScopeBaseObject {
req.Log().Debug("base scope, showing domain info")
return ds.SearchBase(req, flags.CanSearch)
}
if !flags.CanSearch {
req.Log().Debug("User can't search, showing info about user")
return ds.SearchMe(req, flags)
}
accsp.Finish() accsp.Finish()
parsedFilter, err := ldap.CompileFilter(req.Filter) parsedFilter, err := ldap.CompileFilter(req.Filter)
@ -121,99 +96,176 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
} }
entries := make([]*ldap.Entry, 0)
// Create a custom client to set additional headers // Create a custom client to set additional headers
c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig()) c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig())
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter) c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
switch filterEntity { scope := req.SearchRequest.Scope
default: needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC)
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
"reason": "unhandled_filter_type",
"dn": req.BindDN,
"client": req.RemoteAddr(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
case constants.OCGroupOfUniqueNames:
fallthrough
case constants.OCAKGroup:
fallthrough
case constants.OCAKVirtualGroup:
fallthrough
case constants.OCGroup:
wg := sync.WaitGroup{}
wg.Add(2)
gEntries := make([]*ldap.Entry, 0) if scope >= 0 && req.BaseDN == baseDN {
uEntries := make([]*ldap.Entry, 0) if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
entries = append(entries, ds.si.GetBaseEntry())
}
go func() { scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
defer wg.Done() }
var users *[]api.User
var groups *[]api.Group
errs, _ := errgroup.WithContext(req.Context())
if needUsers {
errs.Go(func() error {
if flags.CanSearch {
uapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_user")
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
if skip {
req.Log().Trace("Skip backend request")
return nil
}
u, _, e := searchReq.Execute()
uapisp.Finish()
if err != nil {
req.Log().WithError(err).Warning("failed to get users")
return e
}
users = &u.Results
} else {
if flags.UserInfo == nil {
uapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_user")
u, _, err := c.CoreApi.CoreUsersRetrieve(req.Context(), flags.UserPk).Execute()
uapisp.Finish()
if err != nil {
req.Log().WithError(err).Warning("Failed to get user info")
return fmt.Errorf("failed to get userinfo")
}
flags.UserInfo = &u
}
u := make([]api.User, 1)
u[0] = *flags.UserInfo
users = &u
}
return nil
})
}
if needGroups {
errs.Go(func() error {
gapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_group") gapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_group")
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false) searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
if skip { if skip {
req.Log().Trace("Skip backend request") req.Log().Trace("Skip backend request")
return return nil
} }
groups, _, err := searchReq.Execute()
if !flags.CanSearch {
// If they can't search, filter all groups by those they're a member of
searchReq = searchReq.MembersByPk([]int32{flags.UserPk})
}
g, _, err := searchReq.Execute()
gapisp.Finish() gapisp.Finish()
if err != nil { if err != nil {
req.Log().WithError(err).Warning("failed to get groups") req.Log().WithError(err).Warning("failed to get groups")
return return err
} }
req.Log().WithField("count", len(groups.Results)).Trace("Got results from API") req.Log().WithField("count", len(g.Results)).Trace("Got results from API")
for _, g := range groups.Results { if !flags.CanSearch {
gEntries = append(gEntries, group.FromAPIGroup(g, ds.si).Entry()) for i, results := range g.Results {
// If they can't search, remove any users from the group results except the one we're looking for.
g.Results[i].Users = []int32{flags.UserPk}
for _, u := range results.UsersObj {
if u.Pk == flags.UserPk {
g.Results[i].UsersObj = []api.GroupMember{u}
break
}
} }
}()
go func() {
defer wg.Done()
uapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_user")
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
if skip {
req.Log().Trace("Skip backend request")
return
} }
users, _, err := searchReq.Execute()
uapisp.Finish()
if err != nil {
req.Log().WithError(err).Warning("failed to get users")
return
} }
for _, u := range users.Results { groups = &g.Results
uEntries = append(uEntries, group.FromAPIUser(u, ds.si).Entry())
return nil
})
} }
}()
wg.Wait() err = errs.Wait()
entries = append(gEntries, uEntries...)
case "":
fallthrough
case constants.OCOrgPerson:
fallthrough
case constants.OCInetOrgPerson:
fallthrough
case constants.OCAKUser:
fallthrough
case constants.OCUser:
uapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_user")
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
if skip {
req.Log().Trace("Skip backend request")
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
users, _, err := searchReq.Execute()
uapisp.Finish()
if err != nil { if err != nil {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
} }
for _, u := range users.Results {
entries = append(entries, ds.si.UserEntry(u)) if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseUserDN())) {
singleu := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers))
scope -= 1
}
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
for _, u := range *users {
entry := ds.si.UserEntry(u)
if req.BaseDN == entry.DN || !singleu {
entries = append(entries, entry)
} }
} }
}
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseGroupDN())) {
singleg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups))
scope -= 1
}
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
for _, g := range *groups {
entry := group.FromAPIGroup(g, ds.si).Entry()
if req.BaseDN == entry.DN || !singleg {
entries = append(entries, entry)
}
}
}
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
singlevg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
if !singlevg || utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
scope -= 1
}
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
for _, u := range *users {
entry := group.FromAPIUser(u, ds.si).Entry()
if req.BaseDN == entry.DN || !singlevg {
entries = append(entries, entry)
}
}
}
}
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
} }

View file

@ -1,54 +0,0 @@
package memory
import (
"fmt"
"github.com/nmcclain/ldap"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ldap/search"
)
func (ms *MemorySearcher) SearchBase(req *search.Request, authz bool) (ldap.ServerSearchResult, error) {
dn := ""
if authz {
dn = req.SearchRequest.BaseDN
}
return ldap.ServerSearchResult{
Entries: []*ldap.Entry{
{
DN: dn,
Attributes: []*ldap.EntryAttribute{
{
Name: "distinguishedName",
Values: []string{ms.si.GetBaseDN()},
},
{
Name: "objectClass",
Values: []string{"top", "domain"},
},
{
Name: "supportedLDAPVersion",
Values: []string{"3"},
},
{
Name: "namingContexts",
Values: []string{
ms.si.GetBaseDN(),
ms.si.GetBaseUserDN(),
ms.si.GetBaseGroupDN(),
},
},
{
Name: "vendorName",
Values: []string{"goauthentik.io"},
},
{
Name: "vendorVersion",
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
},
},
},
},
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
}, nil
}

View file

@ -11,11 +11,11 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/outpost/ldap/constants" "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/group" "goauthentik.io/internal/outpost/ldap/group"
"goauthentik.io/internal/outpost/ldap/metrics" "goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/outpost/ldap/search" "goauthentik.io/internal/outpost/ldap/search"
"goauthentik.io/internal/outpost/ldap/server" "goauthentik.io/internal/outpost/ldap/server"
"goauthentik.io/internal/outpost/ldap/utils"
) )
type MemorySearcher struct { type MemorySearcher struct {
@ -37,29 +37,11 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
return ms return ms
} }
func (ms *MemorySearcher) SearchMe(req *search.Request, f flags.UserFlags) (ldap.ServerSearchResult, error) {
if f.UserInfo == nil {
for _, u := range ms.users {
if u.Pk == f.UserPk {
f.UserInfo = &u
}
}
if f.UserInfo == nil {
req.Log().WithField("pk", f.UserPk).Warning("User with pk is not in local cache")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
}
}
entries := make([]*ldap.Entry, 1)
entries[0] = ms.si.UserEntry(*f.UserInfo)
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
baseDN := strings.ToLower("," + ms.si.GetBaseDN()) baseDN := strings.ToLower(ms.si.GetBaseDN())
entries := []*ldap.Entry{} filterOC, err := ldap.GetFilterObjectClass(req.Filter)
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil { if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{ metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(), "outpost_name": ms.si.GetOutpostName(),
@ -80,7 +62,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
}).Inc() }).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
} }
if !strings.HasSuffix(req.BindDN, baseDN) { if !strings.HasSuffix(req.BindDN, ","+baseDN) {
metrics.RequestsRejected.With(prometheus.Labels{ metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(), "outpost_name": ms.si.GetOutpostName(),
"type": "search", "type": "search",
@ -103,52 +85,132 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
}).Inc() }).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
} }
if req.Scope == ldap.ScopeBaseObject {
req.Log().Debug("base scope, showing domain info")
return ms.SearchBase(req, flags.CanSearch)
}
if !flags.CanSearch {
req.Log().Debug("User can't search, showing info about user")
return ms.SearchMe(req, flags)
}
accsp.Finish() accsp.Finish()
switch filterEntity { entries := make([]*ldap.Entry, 0)
default:
metrics.RequestsRejected.With(prometheus.Labels{ scope := req.SearchRequest.Scope
"outpost_name": ms.si.GetOutpostName(), needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC)
"type": "search",
"reason": "unhandled_filter_type", if scope >= 0 && req.BaseDN == baseDN {
"dn": req.BindDN, if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
"client": req.RemoteAddr(), entries = append(entries, ms.si.GetBaseEntry())
}).Inc() }
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
case constants.OCGroupOfUniqueNames: scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
fallthrough }
case constants.OCAKGroup:
fallthrough var users *[]api.User
case constants.OCAKVirtualGroup: var groups []*group.LDAPGroup
fallthrough
case constants.OCGroup: if needUsers {
if flags.CanSearch {
users = &ms.users
} else {
if flags.UserInfo == nil {
for i, u := range ms.users {
if u.Pk == flags.UserPk {
flags.UserInfo = &ms.users[i]
}
}
if flags.UserInfo == nil {
req.Log().WithField("pk", flags.UserPk).Warning("User with pk is not in local cache")
err = fmt.Errorf("failed to get userinfo")
}
}
u := make([]api.User, 1)
u[0] = *flags.UserInfo
users = &u
}
}
if needGroups {
groups = make([]*group.LDAPGroup, 0)
for _, g := range ms.groups { for _, g := range ms.groups {
entries = append(entries, group.FromAPIGroup(g, ms.si).Entry()) if flags.CanSearch {
} groups = append(groups, group.FromAPIGroup(g, ms.si))
for _, u := range ms.users { } else {
entries = append(entries, group.FromAPIUser(u, ms.si).Entry()) // If the user cannot search, we're going to only return
} // the groups they're in _and_ only return themselves
case "": // as a member.
fallthrough for _, u := range g.UsersObj {
case constants.OCOrgPerson: if flags.UserPk == u.Pk {
fallthrough // TODO: Is there a better way to clone this object?
case constants.OCInetOrgPerson: fg := api.NewGroup(g.Pk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u})
fallthrough fg.SetAttributes(*g.Attributes)
case constants.OCAKUser: fg.SetIsSuperuser(*g.IsSuperuser)
fallthrough groups = append(groups, group.FromAPIGroup(*fg, ms.si))
case constants.OCUser: break
for _, u := range ms.users {
entries = append(entries, ms.si.UserEntry(u))
} }
} }
}
}
}
if err != nil {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
}
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseUserDN())) {
singleu := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers))
scope -= 1
}
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
for _, u := range *users {
entry := ms.si.UserEntry(u)
if req.BaseDN == entry.DN || !singleu {
entries = append(entries, entry)
}
}
}
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseGroupDN())) {
singleg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups))
scope -= 1
}
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
for _, g := range groups {
if req.BaseDN == g.DN || !singleg {
entries = append(entries, g.Entry())
}
}
}
scope += 1 // Return the scope to what it was before we descended
}
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
singlevg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
scope -= 1
}
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
for _, u := range *users {
entry := group.FromAPIUser(u, ms.si).Entry()
if req.BaseDN == entry.DN || !singlevg {
entries = append(entries, entry)
}
}
}
}
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
} }

View file

@ -1,6 +1,8 @@
package search package search
import "github.com/nmcclain/ldap" import (
"github.com/nmcclain/ldap"
)
type Searcher interface { type Searcher interface {
Search(req *Request) (ldap.ServerSearchResult, error) Search(req *Request) (ldap.ServerSearchResult, error)

View file

@ -19,6 +19,7 @@ type LDAPServerInstance interface {
GetBaseDN() string GetBaseDN() string
GetBaseGroupDN() string GetBaseGroupDN() string
GetBaseVirtualGroupDN() string
GetBaseUserDN() string GetBaseUserDN() string
GetUserDN(string) string GetUserDN(string) string
@ -32,4 +33,7 @@ type LDAPServerInstance interface {
GetFlags(string) (flags.UserFlags, bool) GetFlags(string) (flags.UserFlags, bool)
SetFlags(string, flags.UserFlags) SetFlags(string, flags.UserFlags)
GetBaseEntry() *ldap.Entry
GetNeededObjects(int, string, string) (bool, bool)
} }

View file

@ -5,6 +5,7 @@ import (
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
) )
func BoolToString(in bool) string { func BoolToString(in bool) string {
@ -84,3 +85,35 @@ func MustHaveAttribute(attrs []*ldap.EntryAttribute, name string, value []string
} }
return attrs return attrs
} }
func IncludeObjectClass(searchOC string, ocs map[string]bool) bool {
if searchOC == "" {
return true
}
return ocs[searchOC]
}
func GetContainerEntry(filterOC string, dn string, ou string) *ldap.Entry {
if IncludeObjectClass(filterOC, ldapConstants.GetContainerOCs()) {
return &ldap.Entry{
DN: dn,
Attributes: []*ldap.EntryAttribute{
{
Name: "distinguishedName",
Values: []string{dn},
},
{
Name: "objectClass",
Values: []string{"top", "nsContainer"},
},
{
Name: "commonName",
Values: []string{ou},
},
},
}
}
return nil
}

View file

@ -1,6 +1,7 @@
package application package application
import ( import (
"context"
"crypto/tls" "crypto/tls"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
@ -52,11 +53,17 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
return nil, fmt.Errorf("failed to parse URL, skipping provider") return nil, fmt.Errorf("failed to parse URL, skipping provider")
} }
ks := hs256.NewKeySet(*p.ClientSecret) var ks oidc.KeySet
if contains(p.OidcConfiguration.IdTokenSigningAlgValuesSupported, "HS256") {
ks = hs256.NewKeySet(*p.ClientSecret)
} else {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
ks = oidc.NewRemoteKeySet(ctx, p.OidcConfiguration.JwksUri)
}
var verifier = oidc.NewVerifier(p.OidcConfiguration.Issuer, ks, &oidc.Config{ var verifier = oidc.NewVerifier(p.OidcConfiguration.Issuer, ks, &oidc.Config{
ClientID: *p.ClientId, ClientID: *p.ClientId,
SupportedSigningAlgs: []string{"HS256"}, SupportedSigningAlgs: []string{"RS256", "HS256"},
}) })
// Configure an OpenID Connect aware OAuth2 client. // Configure an OpenID Connect aware OAuth2 client.
@ -94,14 +101,14 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
if !ok { if !ok {
return l return l
} }
return l.WithField("request_username", c.Email) return l.WithField("request_username", c.PreferredUsername)
})) }))
mux.Use(func(inner http.Handler) http.Handler { mux.Use(func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
c, _ := a.getClaims(r) c, _ := a.getClaims(r)
user := "" user := ""
if c != nil { if c != nil {
user = c.Email user = c.PreferredUsername
} }
before := time.Now() before := time.Now()
inner.ServeHTTP(rw, r) inner.ServeHTTP(rw, r)

View file

@ -13,4 +13,6 @@ type Claims struct {
Name string `json:"name"` Name string `json:"name"`
PreferredUsername string `json:"preferred_username"` PreferredUsername string `json:"preferred_username"`
Groups []string `json:"groups"` Groups []string `json:"groups"`
RawToken string
} }

View file

@ -5,24 +5,34 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"goauthentik.io/internal/constants"
) )
func (a *Application) addHeaders(r *http.Request, c *Claims) { func (a *Application) addHeaders(headers http.Header, c *Claims) {
// https://goauthentik.io/docs/providers/proxy/proxy // https://goauthentik.io/docs/providers/proxy/proxy
// Legacy headers, remove after 2022.1 // Legacy headers, remove after 2022.1
r.Header.Set("X-Auth-Username", c.PreferredUsername) headers.Set("X-Auth-Username", c.PreferredUsername)
r.Header.Set("X-Auth-Groups", strings.Join(c.Groups, "|")) headers.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
r.Header.Set("X-Forwarded-Email", c.Email) headers.Set("X-Forwarded-Email", c.Email)
r.Header.Set("X-Forwarded-Preferred-Username", c.PreferredUsername) headers.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
r.Header.Set("X-Forwarded-User", c.Sub) headers.Set("X-Forwarded-User", c.Sub)
// New headers, unique prefix // New headers, unique prefix
r.Header.Set("X-authentik-username", c.PreferredUsername) headers.Set("X-authentik-username", c.PreferredUsername)
r.Header.Set("X-authentik-groups", strings.Join(c.Groups, "|")) headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
r.Header.Set("X-authentik-email", c.Email) headers.Set("X-authentik-email", c.Email)
r.Header.Set("X-authentik-name", c.Name) headers.Set("X-authentik-name", c.Name)
r.Header.Set("X-authentik-uid", c.Sub) headers.Set("X-authentik-uid", c.Sub)
headers.Set("X-authentik-jwt", c.RawToken)
// System headers
headers.Set("X-authentik-meta-jwks", a.proxyConfig.OidcConfiguration.JwksUri)
headers.Set("X-authentik-meta-outpost", a.outpostName)
headers.Set("X-authentik-meta-provider", a.proxyConfig.Name)
headers.Set("X-authentik-meta-app", a.proxyConfig.AssignedApplicationSlug)
headers.Set("X-authentik-meta-version", constants.OutpostUserAgent())
userAttributes := c.Proxy.UserAttributes userAttributes := c.Proxy.UserAttributes
// Attempt to set basic auth based on user's attributes // Attempt to set basic auth based on user's attributes
@ -39,7 +49,7 @@ func (a *Application) addHeaders(r *http.Request, c *Claims) {
} }
authVal := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) authVal := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
a.log.WithField("username", username).Trace("setting http basic auth") a.log.WithField("username", username).Trace("setting http basic auth")
r.Header["Authorization"] = []string{fmt.Sprintf("Basic %s", authVal)} headers.Set("Authorization", fmt.Sprintf("Basic %s", authVal))
} }
// Check if user has additional headers set that we should sent // Check if user has additional headers set that we should sent
if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]interface{}); ok { if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]interface{}); ok {
@ -48,15 +58,7 @@ func (a *Application) addHeaders(r *http.Request, c *Claims) {
return return
} }
for key, value := range additionalHeaders { for key, value := range additionalHeaders {
r.Header.Set(key, toString(value)) headers.Set(key, toString(value))
}
}
}
func copyHeadersToResponse(rw http.ResponseWriter, r *http.Request) {
for headerKey, headers := range r.Header {
for _, value := range headers {
rw.Header().Set(headerKey, value)
} }
} }
} }

View file

@ -26,8 +26,9 @@ func (a *Application) configureForward() error {
func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) { func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) {
claims, err := a.getClaims(r) claims, err := a.getClaims(r)
if claims != nil && err == nil { if claims != nil && err == nil {
a.addHeaders(r, claims) a.addHeaders(rw.Header(), claims)
copyHeadersToResponse(rw, r) rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return return
} else if claims == nil && a.IsAllowlisted(r) { } else if claims == nil && a.IsAllowlisted(r) {
a.log.Trace("path can be accessed without authentication") a.log.Trace("path can be accessed without authentication")
@ -69,9 +70,10 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) { func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
claims, err := a.getClaims(r) claims, err := a.getClaims(r)
if claims != nil && err == nil { if claims != nil && err == nil {
a.addHeaders(r, claims) a.addHeaders(rw.Header(), claims)
copyHeadersToResponse(rw, r) rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
rw.WriteHeader(200) rw.WriteHeader(200)
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return return
} else if claims == nil && a.IsAllowlisted(r) { } else if claims == nil && a.IsAllowlisted(r) {
a.log.Trace("path can be accessed without authentication") a.log.Trace("path can be accessed without authentication")

View file

@ -39,7 +39,7 @@ func (a *Application) configureProxy() error {
a.redirectToStart(rw, r) a.redirectToStart(rw, r)
return return
} else { } else {
a.addHeaders(r, claims) a.addHeaders(r.Header, claims)
} }
before := time.Now() before := time.Now()
rp.ServeHTTP(rw, r) rp.ServeHTTP(rw, r)

View file

@ -45,5 +45,6 @@ func (a *Application) redeemCallback(r *http.Request, shouldState string) (*Clai
if err := idToken.Claims(&claims); err != nil { if err := idToken.Claims(&claims); err != nil {
return nil, err return nil, err
} }
claims.RawToken = rawIDToken
return claims, nil return claims, nil
} }

View file

@ -56,3 +56,12 @@ func toString(in interface{}) string {
} }
return "" return ""
} }
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

View file

@ -10,7 +10,6 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"goauthentik.io/internal/outpost/proxyv2/metrics" "goauthentik.io/internal/outpost/proxyv2/metrics"
"goauthentik.io/internal/utils/web" "goauthentik.io/internal/utils/web"
staticWeb "goauthentik.io/web"
) )
func (ps *ProxyServer) HandlePing(rw http.ResponseWriter, r *http.Request) { func (ps *ProxyServer) HandlePing(rw http.ResponseWriter, r *http.Request) {
@ -29,9 +28,9 @@ func (ps *ProxyServer) HandlePing(rw http.ResponseWriter, r *http.Request) {
} }
func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) { func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) {
staticFs := http.FileServer(http.FS(staticWeb.StaticDist)) staticFs := http.FileServer(http.Dir("./web/dist/"))
before := time.Now() before := time.Now()
web.DisableIndex(http.StripPrefix("/akprox/static", staticFs)).ServeHTTP(rw, r) web.DisableIndex(http.StripPrefix("/akprox/static/dist", staticFs)).ServeHTTP(rw, r)
after := time.Since(before) after := time.Since(before)
metrics.Requests.With(prometheus.Labels{ metrics.Requests.With(prometheus.Labels{
"outpost_name": ps.akAPI.Outpost.Name, "outpost_name": ps.akAPI.Outpost.Name,

View file

@ -9,33 +9,19 @@ import (
"goauthentik.io/internal/constants" "goauthentik.io/internal/constants"
"goauthentik.io/internal/utils/web" "goauthentik.io/internal/utils/web"
staticWeb "goauthentik.io/web" staticWeb "goauthentik.io/web"
staticDocs "goauthentik.io/website"
) )
func (ws *WebServer) configureStatic() { func (ws *WebServer) configureStatic() {
statRouter := ws.lh.NewRoute().Subrouter() statRouter := ws.lh.NewRoute().Subrouter()
statRouter.Use(ws.staticHeaderMiddleware)
indexLessRouter := statRouter.NewRoute().Subrouter() indexLessRouter := statRouter.NewRoute().Subrouter()
indexLessRouter.Use(web.DisableIndex) indexLessRouter.Use(web.DisableIndex)
// Media files, always local // Media files, always local
fs := http.FileServer(http.Dir(config.G.Paths.Media)) fs := http.FileServer(http.Dir(config.G.Paths.Media))
var distHandler http.Handler distFs := http.FileServer(http.Dir("./web/dist"))
var distFs http.Handler distHandler := http.StripPrefix("/static/dist/", distFs)
var authentikHandler http.Handler authentikHandler := http.StripPrefix("/static/authentik/", http.FileServer(http.Dir("./web/authentik")))
var helpHandler http.Handler helpHandler := http.FileServer(http.Dir("./website/help/"))
if config.G.Debug || config.G.Web.LoadLocalFiles {
ws.log.Debug("Using local static files")
distFs = http.FileServer(http.Dir("./web/dist"))
distHandler = http.StripPrefix("/static/dist/", distFs)
authentikHandler = http.StripPrefix("/static/authentik/", http.FileServer(http.Dir("./web/authentik")))
helpHandler = http.FileServer(http.Dir("./website/help/"))
} else {
statRouter.Use(ws.staticHeaderMiddleware)
ws.log.Debug("Using packaged static files with aggressive caching")
distFs = http.FileServer(http.FS(staticWeb.StaticDist))
distHandler = http.StripPrefix("/static", distFs)
authentikHandler = http.StripPrefix("/static", http.FileServer(http.FS(staticWeb.StaticAuthentik)))
helpHandler = http.FileServer(http.FS(staticDocs.Help))
}
indexLessRouter.PathPrefix("/static/dist/").Handler(distHandler) indexLessRouter.PathPrefix("/static/dist/").Handler(distHandler)
indexLessRouter.PathPrefix("/static/authentik/").Handler(authentikHandler) indexLessRouter.PathPrefix("/static/authentik/").Handler(authentikHandler)

View file

@ -28,7 +28,7 @@ function check_if_root {
GROUP="authentik:${GROUP_NAME}" GROUP="authentik:${GROUP_NAME}"
fi fi
# Fix permissions of backups and media # Fix permissions of backups and media
chown -R authentik:authentik /media /backups chown -R authentik:authentik /media /backups /certs
chpst -u authentik:$GROUP env HOME=/authentik $1 chpst -u authentik:$GROUP env HOME=/authentik $1
} }

View file

@ -12,10 +12,6 @@ FROM docker.io/golang:1.17.3-bullseye AS builder
WORKDIR /go/src/goauthentik.io WORKDIR /go/src/goauthentik.io
COPY . . COPY . .
COPY --from=web-builder /static/robots.txt /work/web/robots.txt
COPY --from=web-builder /static/security.txt /work/web/security.txt
COPY --from=web-builder /static/dist/ /work/web/dist/
COPY --from=web-builder /static/authentik/ /work/web/authentik/
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
RUN go build -o /go/proxy ./cmd/proxy RUN go build -o /go/proxy ./cmd/proxy
@ -27,6 +23,10 @@ ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
COPY --from=builder /go/proxy / COPY --from=builder /go/proxy /
COPY --from=web-builder /static/robots.txt /web/robots.txt
COPY --from=web-builder /static/security.txt /web/security.txt
COPY --from=web-builder /static/dist/ /web/dist/
COPY --from=web-builder /static/authentik/ /web/authentik/
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/akprox/ping" ] HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/akprox/ping" ]

View file

@ -12058,11 +12058,6 @@ paths:
name: additional_user_dn name: additional_user_dn
schema: schema:
type: string type: string
- in: query
name: authentication_flow
schema:
type: string
format: uuid
- in: query - in: query
name: base_dn name: base_dn
schema: schema:
@ -12075,11 +12070,6 @@ paths:
name: enabled name: enabled
schema: schema:
type: boolean type: boolean
- in: query
name: enrollment_flow
schema:
type: string
format: uuid
- in: query - in: query
name: group_membership_field name: group_membership_field
schema: schema:
@ -12115,12 +12105,10 @@ paths:
schema: schema:
type: integer type: integer
- in: query - in: query
name: policy_engine_mode name: peer_certificate
schema: schema:
type: string type: string
enum: format: uuid
- all
- any
- in: query - in: query
name: property_mappings name: property_mappings
schema: schema:
@ -22461,6 +22449,12 @@ components:
server_uri: server_uri:
type: string type: string
format: uri format: uri
peer_certificate:
type: string
format: uuid
nullable: true
description: Optionally verify the LDAP Server's Certificate against the
CA Chain in this keypair.
bind_cn: bind_cn:
type: string type: string
start_tls: start_tls:
@ -22558,6 +22552,12 @@ components:
type: string type: string
minLength: 1 minLength: 1
format: uri format: uri
peer_certificate:
type: string
format: uuid
nullable: true
description: Optionally verify the LDAP Server's Certificate against the
CA Chain in this keypair.
bind_cn: bind_cn:
type: string type: string
bind_password: bind_password:
@ -27181,6 +27181,12 @@ components:
type: string type: string
minLength: 1 minLength: 1
format: uri format: uri
peer_certificate:
type: string
format: uuid
nullable: true
description: Optionally verify the LDAP Server's Certificate against the
CA Chain in this keypair.
bind_cn: bind_cn:
type: string type: string
bind_password: bind_password:
@ -28984,7 +28990,17 @@ components:
items: items:
type: string type: string
readOnly: true readOnly: true
assigned_application_slug:
type: string
description: Internal application name, used in URLs.
readOnly: true
assigned_application_name:
type: string
description: Application's display Name.
readOnly: true
required: required:
- assigned_application_name
- assigned_application_slug
- external_host - external_host
- name - name
- oidc_configuration - oidc_configuration

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
web/icons/icon_discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

14
web/package-lock.json generated
View file

@ -15,7 +15,7 @@
"@babel/preset-env": "^7.16.4", "@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0", "@babel/preset-typescript": "^7.16.0",
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@goauthentik/api": "^2021.10.4-1638190705", "@goauthentik/api": "^2021.10.4-1638522576",
"@jackfranklin/rollup-plugin-markdown": "^0.3.0", "@jackfranklin/rollup-plugin-markdown": "^0.3.0",
"@lingui/cli": "^3.13.0", "@lingui/cli": "^3.13.0",
"@lingui/core": "^3.13.0", "@lingui/core": "^3.13.0",
@ -1708,9 +1708,9 @@
} }
}, },
"node_modules/@goauthentik/api": { "node_modules/@goauthentik/api": {
"version": "2021.10.4-1638190705", "version": "2021.10.4-1638522576",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638190705.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638522576.tgz",
"integrity": "sha512-fEtKGX8F9BDnYWIF9vTxLEkqGkABRl+0M2sgCOd4XqiflNveDEQYMVZAK5yvNzCK8L4wIcbn7y8s/lCncEKJ2Q==" "integrity": "sha512-ojnhGFPnEHXPeMULMtRUBoRVB8k0B73l3O5UL8NSipaY2ZC7jSscIQKDZWz7yvvx9NPMV34kKJ9NK8N+/jzfgw=="
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.6.0", "version": "0.6.0",
@ -9895,9 +9895,9 @@
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg=="
}, },
"@goauthentik/api": { "@goauthentik/api": {
"version": "2021.10.4-1638190705", "version": "2021.10.4-1638522576",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638190705.tgz", "resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638522576.tgz",
"integrity": "sha512-fEtKGX8F9BDnYWIF9vTxLEkqGkABRl+0M2sgCOd4XqiflNveDEQYMVZAK5yvNzCK8L4wIcbn7y8s/lCncEKJ2Q==" "integrity": "sha512-ojnhGFPnEHXPeMULMtRUBoRVB8k0B73l3O5UL8NSipaY2ZC7jSscIQKDZWz7yvvx9NPMV34kKJ9NK8N+/jzfgw=="
}, },
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.6.0", "version": "0.6.0",

View file

@ -51,7 +51,7 @@
"@babel/preset-env": "^7.16.4", "@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0", "@babel/preset-typescript": "^7.16.0",
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@goauthentik/api": "^2021.10.4-1638190705", "@goauthentik/api": "^2021.10.4-1638522576",
"@jackfranklin/rollup-plugin-markdown": "^0.3.0", "@jackfranklin/rollup-plugin-markdown": "^0.3.0",
"@lingui/cli": "^3.13.0", "@lingui/cli": "^3.13.0",
"@lingui/core": "^3.13.0", "@lingui/core": "^3.13.0",

View file

@ -1,4 +1,4 @@
Contact: mailto:security@beryju.org Contact: mailto:security@beryju.org
Expires: Sat, 1 Jan 2022 00:00 +0200 Expires: Sat, 1 Jan 2023 00:00 +0200
Preferred-Languages: en, de Preferred-Languages: en, de
Policy: https://github.com/goauthentik/authentik/blob/master/SECURITY.md Policy: https://github.com/goauthentik/authentik/blob/master/SECURITY.md

View file

@ -17,6 +17,7 @@ import { AKResponse } from "../../api/Client";
import { EVENT_REFRESH } from "../../constants"; import { EVENT_REFRESH } from "../../constants";
import { groupBy } from "../../utils"; import { groupBy } from "../../utils";
import "../EmptyState"; import "../EmptyState";
import "../buttons/SpinnerButton";
import "../chips/Chip"; import "../chips/Chip";
import "../chips/ChipGroup"; import "../chips/ChipGroup";
import { getURLParam, updateURLParams } from "../router/RouteMatch"; import { getURLParam, updateURLParams } from "../router/RouteMatch";
@ -162,12 +163,12 @@ export abstract class Table<T> extends LitElement {
}); });
} }
public fetch(): void { public async fetch(): Promise<void> {
if (this.isLoading) { if (this.isLoading) {
return; return;
} }
this.isLoading = true; this.isLoading = true;
this.apiEndpoint(this.page) return this.apiEndpoint(this.page)
.then((r) => { .then((r) => {
this.data = r; this.data = r;
this.page = r.pagination.current; this.page = r.pagination.current;
@ -319,19 +320,14 @@ export abstract class Table<T> extends LitElement {
} }
renderToolbar(): TemplateResult { renderToolbar(): TemplateResult {
return html`<button return html` <ak-spinner-button
@click=${() => { .callAction=${() => {
this.dispatchEvent( return this.fetch();
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
}} }}
class="pf-c-button pf-m-secondary" class="pf-m-secondary"
> >
${t`Refresh`} ${t`Refresh`}</ak-spinner-button
</button>`; >`;
} }
renderToolbarSelected(): TemplateResult { renderToolbarSelected(): TemplateResult {
@ -350,12 +346,7 @@ export abstract class Table<T> extends LitElement {
value=${ifDefined(this.search)} value=${ifDefined(this.search)}
.onSearch=${(value: string) => { .onSearch=${(value: string) => {
this.search = value; this.search = value;
this.dispatchEvent( this.fetch();
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
updateURLParams({ updateURLParams({
search: value, search: value,
}); });
@ -382,12 +373,7 @@ export abstract class Table<T> extends LitElement {
.pages=${this.data?.pagination} .pages=${this.data?.pagination}
.pageChangeHandler=${(page: number) => { .pageChangeHandler=${(page: number) => {
this.page = page; this.page = page;
this.dispatchEvent( this.fetch();
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
}} }}
> >
</ak-table-pagination>` </ak-table-pagination>`
@ -442,12 +428,7 @@ export abstract class Table<T> extends LitElement {
.pages=${this.data?.pagination} .pages=${this.data?.pagination}
.pageChangeHandler=${(page: number) => { .pageChangeHandler=${(page: number) => {
this.page = page; this.page = page;
this.dispatchEvent( this.fetch();
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
}} }}
> >
</ak-table-pagination> </ak-table-pagination>

View file

@ -100,9 +100,7 @@ export class AuthenticatorValidateStage
return html`<i class="fas fa-mobile-alt"></i> return html`<i class="fas fa-mobile-alt"></i>
<div class="right"> <div class="right">
<p>${t`Duo push-notifications`}</p> <p>${t`Duo push-notifications`}</p>
<small <small>${t`Receive a push notification on your device.`}</small>
>${t`Receive a push notification on your phone to prove your identity.`}</small
>
</div>`; </div>`;
case DeviceClassesEnum.Webauthn: case DeviceClassesEnum.Webauthn:
return html`<i class="fas fa-mobile-alt"></i> return html`<i class="fas fa-mobile-alt"></i>

View file

@ -2387,6 +2387,14 @@ msgstr "Internal host"
msgid "Internal host SSL Validation" msgid "Internal host SSL Validation"
msgstr "Internal host SSL Validation" msgstr "Internal host SSL Validation"
#: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid ""
"Invalid login attempts will decrease the score for the client's IP, and the\n"
"username they are attempting to login as, by one."
msgstr ""
"Invalid login attempts will decrease the score for the client's IP, and the\n"
"username they are attempting to login as, by one."
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
msgid "Invalid response action" msgid "Invalid response action"
msgstr "Invalid response action" msgstr "Invalid response action"
@ -2608,6 +2616,7 @@ msgstr "Loading"
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.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
@ -2704,6 +2713,14 @@ msgstr "MFA Devices"
msgid "Make sure to keep these tokens in a safe place." msgid "Make sure to keep these tokens in a safe place."
msgstr "Make sure to keep these tokens in a safe place." msgstr "Make sure to keep these tokens in a safe place."
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Managed by authentik"
msgstr "Managed by authentik"
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Managed by authentik (Discovered)"
msgstr "Managed by authentik (Discovered)"
#: src/pages/stages/user_write/UserWriteStageForm.ts #: src/pages/stages/user_write/UserWriteStageForm.ts
msgid "Mark newly created users as inactive." msgid "Mark newly created users as inactive."
msgstr "Mark newly created users as inactive." msgstr "Mark newly created users as inactive."
@ -3612,8 +3629,8 @@ msgid "Re-evaluate policies"
msgstr "Re-evaluate policies" msgstr "Re-evaluate policies"
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts #: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Receive a push notification on your phone to prove your identity." msgid "Receive a push notification on your device."
msgstr "Receive a push notification on your phone to prove your identity." msgstr "Receive a push notification on your device."
#: src/pages/flows/utils.ts #: src/pages/flows/utils.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
@ -4199,6 +4216,10 @@ msgstr "Sources"
msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves."
msgstr "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgstr "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves."
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Specify multiple server URIs by separating them with a comma."
msgstr "Specify multiple server URIs by separating them with a comma."
#: src/pages/flows/BoundStagesList.ts #: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
msgid "Stage" msgid "Stage"
@ -4739,6 +4760,7 @@ msgstr "TLS Authentication Certificate"
#~ msgstr "TLS Server name" #~ msgstr "TLS Server name"
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "TLS Verification Certificate" msgid "TLS Verification Certificate"
msgstr "TLS Verification Certificate" msgstr "TLS Verification Certificate"
@ -4830,14 +4852,24 @@ msgstr "The external URL you'll authenticate at. Can be the same domain as authe
msgid "The following objects use {objName}" msgid "The following objects use {objName}"
msgstr "The following objects use {objName}" msgstr "The following objects use {objName}"
#: src/pages/policies/reputation/ReputationPolicyForm.ts
#~ msgid ""
#~ "The policy passes when the reputation score is above the threshold, and\n"
#~ "doesn't pass when either or both of the selected options are equal or less than the\n"
#~ "threshold."
#~ msgstr ""
#~ "The policy passes when the reputation score is above the threshold, and\n"
#~ "doesn't pass when either or both of the selected options are equal or less than the\n"
#~ "threshold."
#: src/pages/policies/reputation/ReputationPolicyForm.ts #: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid "" msgid ""
"The policy passes when the reputation score is above the threshold, and\n" "The policy passes when the reputation score is below the threshold, and\n"
"doesn't pass when either or both of the selected options are equal or less than the\n" "doesn't pass when either or both of the selected options are equal or above the\n"
"threshold." "threshold."
msgstr "" msgstr ""
"The policy passes when the reputation score is above the threshold, and\n" "The policy passes when the reputation score is below the threshold, and\n"
"doesn't pass when either or both of the selected options are equal or less than the\n" "doesn't pass when either or both of the selected options are equal or above the\n"
"threshold." "threshold."
#: src/pages/policies/dummy/DummyPolicyForm.ts #: src/pages/policies/dummy/DummyPolicyForm.ts
@ -5647,6 +5679,10 @@ msgstr "When a user returns from the email successfully, their account will be a
msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown." msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
msgstr "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown." msgstr "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
msgstr "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored."

View file

@ -2370,6 +2370,12 @@ msgstr "Hôte interne"
msgid "Internal host SSL Validation" msgid "Internal host SSL Validation"
msgstr "Validation SSL de l'hôte interne" msgstr "Validation SSL de l'hôte interne"
#: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid ""
"Invalid login attempts will decrease the score for the client's IP, and the\n"
"username they are attempting to login as, by one."
msgstr ""
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
msgid "Invalid response action" msgid "Invalid response action"
msgstr "Action de réponse invalide" msgstr "Action de réponse invalide"
@ -2589,6 +2595,7 @@ msgstr "Chargement en cours"
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.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
@ -2685,6 +2692,14 @@ msgstr ""
msgid "Make sure to keep these tokens in a safe place." msgid "Make sure to keep these tokens in a safe place."
msgstr "" msgstr ""
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Managed by authentik"
msgstr ""
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Managed by authentik (Discovered)"
msgstr ""
#: src/pages/stages/user_write/UserWriteStageForm.ts #: src/pages/stages/user_write/UserWriteStageForm.ts
msgid "Mark newly created users as inactive." msgid "Mark newly created users as inactive."
msgstr "Marquer les utilisateurs nouvellements créés comme inactifs." msgstr "Marquer les utilisateurs nouvellements créés comme inactifs."
@ -3582,8 +3597,12 @@ msgid "Re-evaluate policies"
msgstr "Ré-évaluer les politiques" msgstr "Ré-évaluer les politiques"
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts #: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Receive a push notification on your phone to prove your identity." msgid "Receive a push notification on your device."
msgstr "Recevez une notification push sur votre téléphone pour prouver votre identité." msgstr ""
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
#~ msgid "Receive a push notification on your phone to prove your identity."
#~ msgstr "Recevez une notification push sur votre téléphone pour prouver votre identité."
#: src/pages/flows/utils.ts #: src/pages/flows/utils.ts
#: src/pages/tokens/TokenListPage.ts #: src/pages/tokens/TokenListPage.ts
@ -4158,6 +4177,10 @@ msgstr "Sources"
msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves."
msgstr "Sources d'identités, qui peuvent soit être synchronisées dans la base de données d'Authentik, soit être utilisées par les utilisateurs pour s'authentifier et s'inscrire." msgstr "Sources d'identités, qui peuvent soit être synchronisées dans la base de données d'Authentik, soit être utilisées par les utilisateurs pour s'authentifier et s'inscrire."
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Specify multiple server URIs by separating them with a comma."
msgstr ""
#: src/pages/flows/BoundStagesList.ts #: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
msgid "Stage" msgid "Stage"
@ -4691,6 +4714,7 @@ msgstr "Certificat TLS d'authentification"
#~ msgstr "Nom TLS du serveur" #~ msgstr "Nom TLS du serveur"
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "TLS Verification Certificate" msgid "TLS Verification Certificate"
msgstr "Certificat de vérification TLS" msgstr "Certificat de vérification TLS"
@ -4781,12 +4805,19 @@ msgstr "L'URL externe sur laquelle vous vous authentifierez. Cela peut être le
msgid "The following objects use {objName}" msgid "The following objects use {objName}"
msgstr "Les objets suivants utilisent {objName}" msgstr "Les objets suivants utilisent {objName}"
#: src/pages/policies/reputation/ReputationPolicyForm.ts
#~ msgid ""
#~ "The policy passes when the reputation score is above the threshold, and\n"
#~ "doesn't pass when either or both of the selected options are equal or less than the\n"
#~ "threshold."
#~ msgstr "La politique est réussie si la note de réputation est au-dessus du seuil, et échoue si au moins l'une des options sélectionnées sont inférieures ou égales au seuil."
#: src/pages/policies/reputation/ReputationPolicyForm.ts #: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid "" msgid ""
"The policy passes when the reputation score is above the threshold, and\n" "The policy passes when the reputation score is below the threshold, and\n"
"doesn't pass when either or both of the selected options are equal or less than the\n" "doesn't pass when either or both of the selected options are equal or above the\n"
"threshold." "threshold."
msgstr "La politique est réussie si la note de réputation est au-dessus du seuil, et échoue si au moins l'une des options sélectionnées sont inférieures ou égales au seuil." msgstr ""
#: 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."
@ -5586,6 +5617,10 @@ msgstr "Lorsqu'un utilisateur revient de l'e-mail avec succès, son compte sera
msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown." msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
msgstr "Lorsqu'un nom d'utilisateur/email valide a été saisi, et si cette option est active, le nom d'utilisateur et l'avatar de l'utilisateur seront affichés. Sinon, le texte que l'utilisateur a saisi sera affiché." msgstr "Lorsqu'un nom d'utilisateur/email valide a été saisi, et si cette option est active, le nom d'utilisateur et l'avatar de l'utilisateur seront affichés. Sinon, le texte que l'utilisateur a saisi sera affiché."
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
msgstr ""
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
msgstr "Si activé, les paramètres globaux de connexion courriel seront utilisés et les paramètres de connexion ci-dessous seront ignorés." msgstr "Si activé, les paramètres globaux de connexion courriel seront utilisés et les paramètres de connexion ci-dessous seront ignorés."

View file

@ -2379,6 +2379,12 @@ msgstr ""
msgid "Internal host SSL Validation" msgid "Internal host SSL Validation"
msgstr "" msgstr ""
#: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid ""
"Invalid login attempts will decrease the score for the client's IP, and the\n"
"username they are attempting to login as, by one."
msgstr ""
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
msgid "Invalid response action" msgid "Invalid response action"
msgstr "" msgstr ""
@ -2600,6 +2606,7 @@ msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts #: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts
#: src/pages/sources/oauth/OAuthSourceForm.ts #: src/pages/sources/oauth/OAuthSourceForm.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
@ -2696,6 +2703,14 @@ msgstr ""
msgid "Make sure to keep these tokens in a safe place." msgid "Make sure to keep these tokens in a safe place."
msgstr "" msgstr ""
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Managed by authentik"
msgstr ""
#: src/pages/crypto/CertificateKeyPairListPage.ts
msgid "Managed by authentik (Discovered)"
msgstr ""
#: src/pages/stages/user_write/UserWriteStageForm.ts #: src/pages/stages/user_write/UserWriteStageForm.ts
msgid "Mark newly created users as inactive." msgid "Mark newly created users as inactive."
msgstr "" msgstr ""
@ -3604,7 +3619,7 @@ msgid "Re-evaluate policies"
msgstr "" msgstr ""
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts #: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Receive a push notification on your phone to prove your identity." msgid "Receive a push notification on your device."
msgstr "" msgstr ""
#: src/pages/flows/utils.ts #: src/pages/flows/utils.ts
@ -4191,6 +4206,10 @@ msgstr ""
msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves."
msgstr "" msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "Specify multiple server URIs by separating them with a comma."
msgstr ""
#: src/pages/flows/BoundStagesList.ts #: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts #: src/pages/flows/StageBindingForm.ts
msgid "Stage" msgid "Stage"
@ -4731,6 +4750,7 @@ msgstr ""
#~ msgstr "" #~ msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "TLS Verification Certificate" msgid "TLS Verification Certificate"
msgstr "" msgstr ""
@ -4822,10 +4842,17 @@ msgstr ""
msgid "The following objects use {objName}" msgid "The following objects use {objName}"
msgstr "" msgstr ""
#: src/pages/policies/reputation/ReputationPolicyForm.ts
#~ msgid ""
#~ "The policy passes when the reputation score is above the threshold, and\n"
#~ "doesn't pass when either or both of the selected options are equal or less than the\n"
#~ "threshold."
#~ msgstr ""
#: src/pages/policies/reputation/ReputationPolicyForm.ts #: src/pages/policies/reputation/ReputationPolicyForm.ts
msgid "" msgid ""
"The policy passes when the reputation score is above the threshold, and\n" "The policy passes when the reputation score is below the threshold, and\n"
"doesn't pass when either or both of the selected options are equal or less than the\n" "doesn't pass when either or both of the selected options are equal or above the\n"
"threshold." "threshold."
msgstr "" msgstr ""
@ -5632,6 +5659,10 @@ msgstr ""
msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown." msgid "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown."
msgstr "" msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
msgstr ""
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
msgstr "" msgstr ""

View file

@ -91,8 +91,13 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
} }
row(item: CertificateKeyPair): TemplateResult[] { row(item: CertificateKeyPair): TemplateResult[] {
let managedSubText = t`Managed by authentik`;
if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) {
managedSubText = t`Managed by authentik (Discovered)`;
}
return [ return [
html`${item.name}`, html`<div>${item.name}</div>
${item.managed ? html`<small>${managedSubText}</small>` : html``}`,
html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}> html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}>
${item.privateKeyAvailable ? t`Yes` : t`No`} ${item.privateKeyAvailable ? t`Yes` : t`No`}
</ak-label>`, </ak-label>`,

View file

@ -47,8 +47,12 @@ export class ReputationPolicyForm extends ModelForm<ReputationPolicy, string> {
${t`Allows/denys requests based on the users and/or the IPs reputation.`} ${t`Allows/denys requests based on the users and/or the IPs reputation.`}
</div> </div>
<div class="form-help-text"> <div class="form-help-text">
${t`The policy passes when the reputation score is above the threshold, and ${t`Invalid login attempts will decrease the score for the client's IP, and the
doesn't pass when either or both of the selected options are equal or less than the username they are attempting to login as, by one.`}
</div>
<div class="form-help-text">
${t`The policy passes when the reputation score is below the threshold, and
doesn't pass when either or both of the selected options are equal or above the
threshold.`} threshold.`}
</div> </div>
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name"> <ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">

View file

@ -176,7 +176,7 @@ export class ProxyProviderViewPage extends LitElement {
<ak-label <ak-label
color=${this.provider.basicAuthEnabled color=${this.provider.basicAuthEnabled
? PFColor.Green ? PFColor.Green
: PFColor.Red} : PFColor.Grey}
> >
${this.provider.basicAuthEnabled ? t`Yes` : t`No`} ${this.provider.basicAuthEnabled ? t`Yes` : t`No`}
</ak-label> </ak-label>

View file

@ -7,6 +7,7 @@ import { until } from "lit/directives/until.js";
import { import {
CoreApi, CoreApi,
CryptoApi,
LDAPSource, LDAPSource,
LDAPSourceRequest, LDAPSourceRequest,
PropertymappingsApi, PropertymappingsApi,
@ -124,6 +125,9 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
class="pf-c-form-control" class="pf-c-form-control"
required required
/> />
<p class="pf-c-form__helper-text">
${t`Specify multiple server URIs by separating them with a comma.`}
</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal name="startTls"> <ak-form-element-horizontal name="startTls">
<div class="pf-c-check"> <div class="pf-c-check">
@ -138,6 +142,44 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
${t`To use SSL instead, use 'ldaps://' and disable this option.`} ${t`To use SSL instead, use 'ldaps://' and disable this option.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`TLS Verification Certificate`}
name="peerCertificate"
>
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.peerCertificate === undefined}
>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
})
.then((keys) => {
return keys.results.map((key) => {
let selected =
this.instance?.peerCertificate === key.pk;
if (keys.results.length === 1) {
selected = true;
}
return html`<option
value=${ifDefined(key.pk)}
?selected=${selected}
>
${key.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Bind CN`} name="bindCn"> <ak-form-element-horizontal label=${t`Bind CN`} name="bindCn">
<input <input
type="text" type="text"

View file

@ -1,12 +1,6 @@
package web package web
import "embed" import _ "embed"
//go:embed dist/*
var StaticDist embed.FS
//go:embed authentik
var StaticAuthentik embed.FS
//go:embed robots.txt //go:embed robots.txt
var RobotsTxt []byte var RobotsTxt []byte

View file

@ -1,5 +1,6 @@
--- ---
title: Applications title: Applications
slug: /applications
--- ---
Applications in authentik are the counterpart of providers. They exist in a 1-to-1 relationship, each application needs a provider and every provider can be used with one application. Applications in authentik are the counterpart of providers. They exist in a 1-to-1 relationship, each application needs a provider and every provider can be used with one application.

View file

@ -0,0 +1,57 @@
---
title: Certificates
---
Certificates in authentik are used for the following use cases:
- Signing and verifying SAML Requests and Responses
- Signing JSON Web Tokens for OAuth and OIDC
- Connecting to remote docker hosts using the Docker integration
- Verifying LDAP Servers' certificates
- Encrypting outposts's endpoints
## Default certificate
Every authentik install generates a self-signed certificate on the first start. The certificate is called *authentik Self-signed Certificate* and is valid for 1 year.
This certificate is generated to be used as a default for all OAuth2/OIDC providers, as these don't require the certificate to be configured on both sides (the signature of a JWT is validated using the [JWKS](https://auth0.com/docs/security/tokens/json-web-tokens/json-web-key-sets) URL).
This certificate can also be used for SAML Providers/Sources, just keep in mind that the certificate is only valid for a year. Some SAML applications require the certificate to be valid, so they might need to be rotated regularly.
For SAML use-cases, you can generate a Certificate that's valid for longer than 1 year, on your own risk.
## External certificates
To use externally managed certificates, for example generated with certbot or HashiCorp Vault, you can use the discovery feature.
The docker-compose installation maps a `certs` directory to `/certs`, you can simply use this as an output directory for certbot.
For Kubernetes, you can map custom secrets/volumes under `/certs`.
You can also bind mount single files into the folder, as long as they fall under this naming schema.
- Files in the root directory will be imported based on their filename.
`/foo.pem` Will be imported as the keypair `foo`. Based on its content its either imported as certificate or private key.
Currently, only RSA Keys are supported, so if the file contains `BEGIN RSA PRIVATE KEY` it will imported as private key.
Otherwise it will be imported as certificate.
- If the file is called `fullchain.pem` or `privkey.pem` (the output naming of certbot), they will get the name of the parent folder.
- Files can be in any arbitrary file structure, and can have extension.
```
certs/
├── baz
│   └── bar.baz
│   ├── fullchain.pem
│   └── privkey.key
├── foo.bar
│   ├── fullchain.pem
│   └── privkey.key
├── foo.key
└── foo.pem
```
Files are checked every 5 minutes, and will trigger an Outpost refresh if the files differ.

View file

@ -1,5 +1,6 @@
--- ---
title: Tenants title: Tenants
slug: /tenants
--- ---
authentik support soft multi-tenancy. This means that you can configure several options depending on domain, but all the objects like applications, providers, etc, are still global. This can be handy to use the same authentik instance, but branded differently for different domains. authentik support soft multi-tenancy. This means that you can configure several options depending on domain, but all the objects like applications, providers, etc, are still global. This can be handy to use the same authentik instance, but branded differently for different domains.

View file

@ -1,6 +1,7 @@
--- ---
id: terminology id: terminology
title: Terminology title: Terminology
slug: /terminology
--- ---
![](/img/authentik_objects.svg) ![](/img/authentik_objects.svg)

View file

@ -3,3 +3,27 @@ title: Password stage
--- ---
This is a generic password prompt which authenticates the current `pending_user`. This stage allows the selection of the source the user is authenticated against. This is a generic password prompt which authenticates the current `pending_user`. This stage allows the selection of the source the user is authenticated against.
## Passwordless login
To achieve a "passwordless" experience; authenticating users based only on TOTP/WebAuthn/Duo, create an expression policy and optionally skip the password stage.
Depending on what kind of device you want to require the user to have:
#### WebAuthn
```python
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
return WebAuthnDevice.objects.filter(user=request.user, active=True).exists()
```
#### Duo
```python
from authentik.stages.authenticator_duo.models import DuoDevice
return DuoDevice.objects.filter(user=request.user, active=True).exists()
```
Afterwards, bind the policy you've created to the stage binding of the password stage.
Make sure to uncheck *Evaluate on plan* and check *Re-evaluate policies*, otherwise an invalid result will be cached.

View file

@ -73,3 +73,8 @@ Starting with 2021.9.1, custom attributes will override the inbuilt attributes.
You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings. You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings.
This enables you to bind on port 636 using LDAPS, StartTLS is not supported. This enables you to bind on port 636 using LDAPS, StartTLS is not supported.
## Integrations
See the integration guide for [sssd](../../integrations/services/sssd/index) for
an example guide.

View file

@ -50,7 +50,7 @@ services:
traefik.http.routers.authentik.tls: true traefik.http.routers.authentik.tls: true
traefik.http.middlewares.authentik.forwardauth.address: http://outpost.company:9000/akprox/auth/traefik traefik.http.middlewares.authentik.forwardauth.address: http://outpost.company:9000/akprox/auth/traefik
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true
traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid traefik.http.middlewares.authentik.forwardauth.authResponseHeadersRegex: ^.*$
restart: unless-stopped restart: unless-stopped
whoami: whoami:

View file

@ -9,13 +9,7 @@ spec:
forwardAuth: forwardAuth:
address: http://outpost.company:9000/akprox/auth/traefik address: http://outpost.company:9000/akprox/auth/traefik
trustForwardHeader: true trustForwardHeader: true
authResponseHeaders: authResponseHeadersRegex: ^.*$
- Set-Cookie
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
``` ```
Add the following settings to your IngressRoute Add the following settings to your IngressRoute

View file

@ -5,13 +5,7 @@ http:
forwardAuth: forwardAuth:
address: http://outpost.company:9000/akprox/auth/traefik address: http://outpost.company:9000/akprox/auth/traefik
trustForwardHeader: true trustForwardHeader: true
authResponseHeaders: authResponseHeadersRegex: ^.*$
- Set-Cookie
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
routers: routers:
default-router: default-router:
rule: "Host(`app.company`)" rule: "Host(`app.company`)"

View file

@ -2,20 +2,50 @@
title: Overview title: Overview
--- ---
The proxy outpost sets the following headers: The proxy outpost sets the following user-specific headers:
``` - X-authentik-username: `akadmin`
X-authentik-username: akadmin # The username of the currently logged in user
X-authentik-groups: foo|bar|baz # The groups the user is member of, separated by a pipe The username of the currently logged in user
X-authentik-email: root@localhost # The email address of the currently logged in user
X-authentik-name: authentik Default Admin # Full name of the current user - X-authentik-groups: `foo|bar|baz`
X-authentik-uid: 900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb # The hashed identifier of the currently logged in user.
``` The groups the user is member of, separated by a pipe
- X-authentik-email: `root@localhost`
The email address of the currently logged in user
- X-authentik-name: `authentik Default Admin`
Full name of the current user
- X-authentik-uid: `900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb`
The hashed identifier of the currently logged in user.
Additionally, you can set `additionalHeaders` on groups or users to set additional headers. 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. If you enable *Set HTTP-Basic Authentication* option, the HTTP Authorization header is being set.
Besides these user-specific headers, some application specific headers are also set:
- X-authentik-meta-outpost: `authentik Embedded Outpost`
The authentik outpost's name.
- X-authentik-meta-provider: `test`
The authentik provider's name.
- X-authentik-meta-app: `test`
The authentik application's slug.
- X-authentik-meta-version: `authentik-outpost@1.2.3 (build=tagged)`
The authentik outpost's version.
# HTTPS # HTTPS
The outpost listens on both 9000 for HTTP and 9443 for HTTPS. The outpost listens on both 9000 for HTTP and 9443 for HTTPS.

View file

@ -0,0 +1,141 @@
---
title: sssd
---
:::info
This feature is still in technical preview, so please report any
Bugs you run into on [GitHub](https://github.com/goauthentik/authentik/issues)
:::
## What is sssd
From https://sssd.io/
:::note
**SSSD** is an acronym for System Security Services Daemon. It is the client component of centralized identity management solutions such as FreeIPA, 389 Directory Server, Microsoft Active Directory, OpenLDAP and other directory servers. The client serves and caches the information stored in the remote directory server and provides identity, authentication and authorization services to the host machine.
:::
Note that Authentik supports _only_ user and group objects. As
a consequence, it cannot be used to provide automount or sudo
configuration nor can it provide netgroups or services to `nss`.
Kerberos is also not supported.
## Preperation
The following placeholders will be used:
- `authentik.company` is the FQDN of the authentik install.
- `ldap.baseDN` is the Base DN you configure in the LDAP provider.
- `ldap.domain` is (typically) an FQDN for your domain. Usually
it is just the components of your base DN. For example, if
`ldap.baseDN` is `dc=ldap,dc=goauthentik,dc=io` then the domain
might be `ldap.goauthentik.io`.
- `ldap.searchGroup` is the "Search Group" that can can see all
users and groups in Authentik.
- `sssd.serviceAccount` is a service account created in Authentik
- `sssd.serviceAccountToken` is the service account token generated
by Authentik.
Create an LDAP Provider if you don't already have one setup.
This guide assumes you will be running with TLS and that you've
correctly setup certificates both in Authentik and on the host
running sssd. See the [ldap provider docs](../../../docs/providers/ldap) for setting up SSL on the Authentik side.
Remember the Base DN you have configured for the provider as you'll
need it in the sssd configuration.
Create a new service account for all of your hosts to use to connect
to LDAP and perform searches. Make sure this service account is added
to `ldap.searchGroup`.
## Deployment
Create an outpost deployment for the provider you've created above, as described [here](../../../docs/outposts/outposts). Deploy this Outpost either on the same host or a different host that your
host(s) running sssd can access.
The outpost will connect to authentik and configure itself.
## Client Configuration
First, install the necessary sssd packages on your host. Very likely
the package is just `sssd`.
:::note
This guide well help you configure the `sssd.conf` for LDAP only. You
will likely need to perform other tasks for a usable setup
like setting up automounted or autocreated home directories that
are beyond the scope of this guide. See the "additional resources"
section for some help.
:::
Create a file at `/etc/sssd/sssd.conf` with contents similar to
the following:
```ini
[nss]
filter_groups = root
filter_users = root
reconnection_retries = 3
[sssd]
config_file_version = 2
reconnection_retries = 3
sbus_timeout = 30
domains = ${ldap.domain}
services = nss, pam, ssh
[pam]
reconnection_retries = 3
[domain/${ldap.domain}]
cache_credentials = True
id_provider = ldap
chpass_provider = ldap
auth_provider = ldap
access_provider = ldap
ldap_uri = ldaps://${authentik.company}:636
ldap_schema = rfc2307bis
ldap_search_base = ${ldap.baseDN}
ldap_user_search_base = ou=users,${ldap.baseDN}
ldap_group_search_base = ${ldap.baseDN}
ldap_user_object_class = user
ldap_user_name = cn
ldap_group_object_class = group
ldap_group_name = cn
# Optionally, filter logins to only a specific group
#ldap_access_order = filter
#ldap_access_filter = memberOf=cn=authentik Admins,ou=groups,${ldap.baseDN}
ldap_default_bind_dn = cn=${sssd.serviceAccount},ou=users,${ldap.baseDN}
ldap_default_authtok = ${sssd.serviceAccountToken}
```
You should now be able to start sssd; however, the system may not
yet be setup to use it. Depending on your platform, you may need to
use `authconfig` or `pam-auth-update` to configure your system. See
the additional resources section for details.
:::note
You can store SSH authorized keys in LDAP by adding the
`sshPublicKey` attribute to any user with their public key as
the value.
:::
## Additional Resources
The setup of sssd may vary based on Linux distribution and version,
here are some resources that can help you get this setup:
:::note
Authentik is providing a simple LDAP server, not an Active Directory
domain. Be sure you're looking at the correct sections in these guides.
:::
- https://sssd.io/docs/quick-start.html#quick-start-ldap
- https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system-level_authentication_guide/configuring_services
- https://ubuntu.com/server/docs/service-sssd
- https://manpages.debian.org/unstable/sssd-ldap/sssd-ldap.5.en.html
- https://wiki.archlinux.org/title/LDAP_authentication

View file

@ -43,6 +43,10 @@ Use these settings:
For authentik to be able to write passwords back to Active Directory, make sure to use `ldaps://` For authentik to be able to write passwords back to Active Directory, make sure to use `ldaps://`
You can specify multiple servers by separating URIs with a comma, like `ldap://dc1.ad.company,ldap://dc2.ad.company`.
When using a DNS entry with multiple Records, authentik will select a random entry when first connecting.
- Bind CN: `<name of your service user>@ad.company` - Bind CN: `<name of your service user>@ad.company`
- Bind Password: The password you've given the user above - Bind Password: The password you've given the user above
- Base DN: The base DN which you want authentik to sync - Base DN: The base DN which you want authentik to sync

View file

@ -45,6 +45,11 @@ In authentik, create a new LDAP Source in Resources -> Sources.
Use these settings: Use these settings:
- Server URI: `ldaps://ipa1.freeipa.company` - Server URI: `ldaps://ipa1.freeipa.company`
You can specify multiple servers by separating URIs with a comma, like `ldap://ipa1.freeipa.company,ldap://ipa2.freeipa.company`.
When using a DNS entry with multiple Records, authentik will select a random entry when first connecting.
- Bind CN: `uid=svc_authentik,cn=users,cn=accounts,dc=freeipa,dc=company` - Bind CN: `uid=svc_authentik,cn=users,cn=accounts,dc=freeipa,dc=company`
- Bind Password: The password you've given the user above - Bind Password: The password you've given the user above
- Base DN: `dc=freeipa,dc=company` - Base DN: `dc=freeipa,dc=company`

View file

@ -15,6 +15,11 @@ For FreeIPA, follow the [FreeIPA Integration](../freeipa/index.md)
::: :::
- Server URI: URI to your LDAP server/Domain Controller. - Server URI: URI to your LDAP server/Domain Controller.
You can specify multiple servers by separating URIs with a comma, like `ldap://ldap1.company,ldap://ldap2.company`.
When using a DNS entry with multiple Records, authentik will select a random entry when first connecting.
- Bind CN: CN of the bind user. This can also be a UPN in the format of `user@domain.tld`. - Bind CN: CN of the bind user. This can also be a UPN in the format of `user@domain.tld`.
- Bind password: Password used during the bind process. - Bind password: Password used during the bind process.
- Enable StartTLS: Enables StartTLS functionality. To use LDAPS instead, use port `636`. - Enable StartTLS: Enables StartTLS functionality. To use LDAPS instead, use port `636`.

View file

@ -4,10 +4,6 @@ module.exports = {
type: "doc", type: "doc",
id: "index", id: "index",
}, },
{
type: "doc",
id: "terminology",
},
{ {
type: "category", type: "category",
label: "Installation", label: "Installation",
@ -23,8 +19,15 @@ module.exports = {
], ],
}, },
{ {
type: "doc", type: "category",
id: "applications", label: "Core Concepts",
collapsed: false,
items: [
"core/terminology",
"core/applications",
"core/tenants",
"core/certificates",
],
}, },
{ {
type: "category", type: "category",
@ -121,10 +124,6 @@ module.exports = {
label: "Users & Groups", label: "Users & Groups",
items: ["user-group/user", "user-group/group"], items: ["user-group/user", "user-group/group"],
}, },
{
type: "doc",
id: "tenants",
},
{ {
type: "category", type: "category",
label: "Maintenance", label: "Maintenance",

View file

@ -47,6 +47,7 @@ module.exports = {
"services/proxmox-ve/index", "services/proxmox-ve/index",
"services/rancher/index", "services/rancher/index",
"services/sentry/index", "services/sentry/index",
"services/sssd/index",
"services/sonarr/index", "services/sonarr/index",
"services/tautulli/index", "services/tautulli/index",
"services/ubuntu-landscape/index", "services/ubuntu-landscape/index",

View file

@ -1,6 +0,0 @@
package web
import "embed"
//go:embed help/*
var Help embed.FS