Merge branch 'master' into version-2021.12
This commit is contained in:
commit
639c2f5c2e
1
.github/workflows/ci-main.yml
vendored
1
.github/workflows/ci-main.yml
vendored
|
@ -28,6 +28,7 @@ jobs:
|
|||
- isort
|
||||
- bandit
|
||||
- pyright
|
||||
- pending-migrations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -34,13 +34,9 @@ WORKDIR /work
|
|||
|
||||
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/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 ./web/static.go /work/web/static.go
|
||||
COPY ./website/static.go /work/website/static.go
|
||||
COPY ./internal /work/internal
|
||||
COPY ./go.mod /work/go.mod
|
||||
COPY ./go.sum /work/go.sum
|
||||
|
@ -78,6 +74,9 @@ COPY ./tests /tests
|
|||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
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
|
||||
|
||||
|
|
5
Makefile
5
Makefile
|
@ -68,7 +68,7 @@ gen-outpost:
|
|||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli generate \
|
||||
openapitools/openapi-generator-cli:v5.2.1 generate \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/api \
|
||||
|
@ -113,3 +113,6 @@ ci-bandit:
|
|||
|
||||
ci-pyright:
|
||||
pyright e2e lifecycle
|
||||
|
||||
ci-pending-migrations:
|
||||
./manage.py makemigrations --check
|
||||
|
|
|
@ -11,12 +11,7 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
@ -53,9 +48,8 @@ def clear_update_notifications():
|
|||
notification.delete()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def update_latest_version(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def update_latest_version(self: PrefilledMonitoredTask):
|
||||
"""Update latest version info"""
|
||||
if CONFIG.y_bool("disable_update_check"):
|
||||
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||
|
|
|
@ -16,21 +16,15 @@ from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, ExpiringModel
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
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()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def clean_expired_models(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def clean_expired_models(self: PrefilledMonitoredTask):
|
||||
"""Remove expired objects"""
|
||||
messages = []
|
||||
for cls in ExpiringModel.__subclasses__():
|
||||
|
@ -68,9 +62,8 @@ def should_backup() -> bool:
|
|||
return True
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def backup_database(self: PrefilledMonitoredTask): # pragma: no cover
|
||||
"""Database backup"""
|
||||
self.result_timeout_hours = 25
|
||||
if not should_backup():
|
||||
|
|
|
@ -20,6 +20,7 @@ from authentik.api.decorators import permission_required
|
|||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.managed import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
@ -141,9 +142,11 @@ class CertificateKeyPairFilter(FilterSet):
|
|||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
"""CertificateKeyPair Viewset"""
|
||||
|
||||
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False)
|
||||
queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
|
||||
serializer_class = CertificateKeyPairSerializer
|
||||
filterset_class = CertificateKeyPairFilter
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
|
||||
@extend_schema(
|
||||
|
|
|
@ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
import_module("authentik.crypto.managed")
|
||||
import_module("authentik.crypto.tasks")
|
||||
|
|
10
authentik/crypto/settings.py
Normal file
10
authentik/crypto/settings.py
Normal 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
67
authentik/crypto/tasks.py
Normal 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})],
|
||||
)
|
||||
)
|
|
@ -1,5 +1,7 @@
|
|||
"""Crypto tests"""
|
||||
import datetime
|
||||
from os import makedirs
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from django.urls import reverse
|
||||
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.builder import CertificateBuilder
|
||||
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.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()
|
||||
)
|
||||
|
|
|
@ -112,30 +112,6 @@ class TaskInfo:
|
|||
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):
|
||||
"""Task which can save its state to the cache"""
|
||||
|
||||
|
@ -210,5 +186,31 @@ class MonitoredTask(Task):
|
|||
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():
|
||||
task.set_prom_metrics()
|
||||
|
|
|
@ -53,6 +53,7 @@ NEXT_ARG_NAME = "next"
|
|||
SESSION_KEY_PLAN = "authentik_flows_plan"
|
||||
SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
|
||||
SESSION_KEY_GET = "authentik_flows_get"
|
||||
SESSION_KEY_POST = "authentik_flows_post"
|
||||
SESSION_KEY_HISTORY = "authentik_flows_history"
|
||||
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ error_reporting:
|
|||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
sample_rate: 0.5
|
||||
|
||||
# Global email settings
|
||||
email:
|
||||
|
@ -82,3 +83,4 @@ default_user_change_email: true
|
|||
default_user_change_username: true
|
||||
|
||||
gdpr_compliance: true
|
||||
cert_discovery_dir: /certs
|
||||
|
|
|
@ -68,9 +68,9 @@ class DomainlessURLValidator(URLValidator):
|
|||
)
|
||||
self.schemes = ["http", "https", "blank"] + list(self.schemes)
|
||||
|
||||
def __call__(self, value):
|
||||
def __call__(self, value: str):
|
||||
# Check if the scheme is valid.
|
||||
scheme = value.split("://")[0].lower()
|
||||
if scheme not in self.schemes:
|
||||
value = "default" + value
|
||||
return super().__call__(value)
|
||||
super().__call__(value)
|
||||
|
|
|
@ -2,18 +2,12 @@
|
|||
from django.db import DatabaseError
|
||||
|
||||
from authentik.core.tasks import CELERY_APP
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.managed.manager import ObjectManager
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def managed_reconcile(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def managed_reconcile(self: PrefilledMonitoredTask):
|
||||
"""Run ObjectManager to ensure objects are up-to-date"""
|
||||
try:
|
||||
ObjectManager().run()
|
||||
|
|
|
@ -19,8 +19,9 @@ class AuthentikOutpostConfig(AppConfig):
|
|||
import_module("authentik.outposts.signals")
|
||||
import_module("authentik.outposts.managed")
|
||||
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_controller_all.delay()
|
||||
except ProgrammingError:
|
||||
pass
|
||||
|
|
|
@ -19,9 +19,9 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
PrefilledMonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.lib.utils.reflection import path_to_class
|
||||
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)
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def outpost_service_connection_monitor(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def outpost_service_connection_monitor(self: PrefilledMonitoredTask):
|
||||
"""Regularly check the state of Outpost Service Connections"""
|
||||
connections = OutpostServiceConnection.objects.all()
|
||||
for connection in connections.iterator():
|
||||
|
@ -125,9 +124,8 @@ def outpost_controller(
|
|||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def outpost_token_ensurer(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def outpost_token_ensurer(self: PrefilledMonitoredTask):
|
||||
"""Periodically ensure that all Outposts have valid Service Accounts
|
||||
and Tokens"""
|
||||
all_outposts = Outpost.objects.all()
|
||||
|
|
|
@ -69,8 +69,8 @@ class Migration(migrations.Migration):
|
|||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.tenants", "authentik Tenants"),
|
||||
("authentik.core", "authentik Core"),
|
||||
("authentik.managed", "authentik Managed"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
default="",
|
||||
help_text="Match events created by selected application. When left empty, all applications are matched.",
|
||||
|
|
|
@ -2,12 +2,7 @@
|
|||
from django.core.cache import cache
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.policies.reputation.models import IPReputation, UserReputation
|
||||
from authentik.policies.reputation.signals import CACHE_KEY_IP_PREFIX, CACHE_KEY_USER_PREFIX
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
@ -15,9 +10,8 @@ from authentik.root.celery import CELERY_APP
|
|||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def save_ip_reputation(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def save_ip_reputation(self: PrefilledMonitoredTask):
|
||||
"""Save currently cached reputation to database"""
|
||||
objects_to_update = []
|
||||
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"]))
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def save_user_reputation(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def save_user_reputation(self: PrefilledMonitoredTask):
|
||||
"""Save currently cached reputation to database"""
|
||||
objects_to_update = []
|
||||
for key, score in cache.get_many(cache.keys(CACHE_KEY_USER_PREFIX + "*")).items():
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.views.generic.base import View
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
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.policies.denied import AccessDeniedResponse
|
||||
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."""
|
||||
if 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(
|
||||
self.request.get_full_path(),
|
||||
self.get_login_url(),
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any, Optional
|
|||
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
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.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
|
@ -109,6 +109,9 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
|||
class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"""Proxy provider serializer for outposts"""
|
||||
|
||||
assigned_application_slug = ReadOnlyField(source="application.slug")
|
||||
assigned_application_name = ReadOnlyField(source="application.name")
|
||||
|
||||
oidc_configuration = SerializerMethodField()
|
||||
token_validity = SerializerMethodField()
|
||||
scopes_to_request = SerializerMethodField()
|
||||
|
@ -152,6 +155,8 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
|||
"cookie_domain",
|
||||
"token_validity",
|
||||
"scopes_to_request",
|
||||
"assigned_application_slug",
|
||||
"assigned_application_name",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -20,9 +20,11 @@ class TraefikMiddlewareSpecForwardAuth:
|
|||
|
||||
address: str
|
||||
# pylint: disable=invalid-name
|
||||
authResponseHeaders: list[str]
|
||||
authResponseHeadersRegex: str = field(default="")
|
||||
# pylint: disable=invalid-name
|
||||
trustForwardHeader: bool
|
||||
authResponseHeaders: list[str] = field(default_factory=list)
|
||||
# pylint: disable=invalid-name
|
||||
trustForwardHeader: bool = field(default=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -108,21 +110,8 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
|||
spec=TraefikMiddlewareSpec(
|
||||
forwardAuth=TraefikMiddlewareSpecForwardAuth(
|
||||
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
|
||||
authResponseHeaders=[
|
||||
"Set-Cookie",
|
||||
# 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",
|
||||
],
|
||||
authResponseHeaders=[],
|
||||
authResponseHeadersRegex="^.*$",
|
||||
trustForwardHeader=True,
|
||||
)
|
||||
),
|
||||
|
|
|
@ -100,14 +100,13 @@ class AuthNRequestParser:
|
|||
xmlsec.tree.add_ids(root, ["ID"])
|
||||
signature_nodes = root.xpath("/samlp:AuthnRequest/ds:Signature", namespaces=NS_MAP)
|
||||
# No signatures, no verifier configured -> decode xml directly
|
||||
if len(signature_nodes) < 1 and not verifier:
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
if len(signature_nodes) < 1:
|
||||
if not verifier:
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
|
||||
|
||||
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 not verifier:
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_EXISTS_BUT_NO_VERIFIER)
|
||||
|
|
|
@ -13,7 +13,7 @@ from authentik.core.models import Application
|
|||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import in_memory_stage
|
||||
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.views import bad_request_message
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
|
@ -37,7 +37,7 @@ LOGGER = get_logger()
|
|||
|
||||
|
||||
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."""
|
||||
|
||||
def resolve_provider_application(self):
|
||||
|
@ -120,14 +120,20 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
|
|||
|
||||
def check_saml_request(self) -> Optional[HttpRequest]:
|
||||
"""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")
|
||||
return bad_request_message(self.request, "The SAML request payload is missing.")
|
||||
|
||||
try:
|
||||
auth_n_request = AuthNRequestParser(self.provider).parse(
|
||||
self.request.POST[REQUEST_KEY_SAML_REQUEST],
|
||||
self.request.POST.get(REQUEST_KEY_RELAY_STATE),
|
||||
payload[REQUEST_KEY_SAML_REQUEST],
|
||||
payload.get(REQUEST_KEY_RELAY_STATE),
|
||||
)
|
||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
||||
except CannotHandleAssertion as exc:
|
||||
|
|
|
@ -424,7 +424,7 @@ if _ERROR_REPORTING:
|
|||
],
|
||||
before_send=before_send,
|
||||
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"),
|
||||
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
|
||||
)
|
||||
|
|
|
@ -43,6 +43,7 @@ class LDAPSourceSerializer(SourceSerializer):
|
|||
model = LDAPSource
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
"server_uri",
|
||||
"peer_certificate",
|
||||
"bind_cn",
|
||||
"bind_password",
|
||||
"start_tls",
|
||||
|
@ -73,11 +74,9 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
|||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"policy_engine_mode",
|
||||
"server_uri",
|
||||
"bind_cn",
|
||||
"peer_certificate",
|
||||
"start_tls",
|
||||
"base_dn",
|
||||
"additional_user_dn",
|
||||
|
|
|
@ -58,7 +58,7 @@ class LDAPBackend(InbuiltBackend):
|
|||
LOGGER.debug("Attempting Binding as user", user=user)
|
||||
try:
|
||||
temp_connection = ldap3.Connection(
|
||||
source.connection.server,
|
||||
source.server,
|
||||
user=user.attributes.get(LDAP_DISTINGUISHED_NAME),
|
||||
password=password,
|
||||
raise_exceptions=True,
|
||||
|
|
38
authentik/sources/ldap/migrations/0002_auto_20211203_0900.py
Normal file
38
authentik/sources/ldap/migrations/0002_auto_20211203_0900.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,24 +1,48 @@
|
|||
"""authentik LDAP Models"""
|
||||
from typing import Optional, Type
|
||||
from ssl import CERT_REQUIRED
|
||||
from typing import Type
|
||||
|
||||
from django.db import models
|
||||
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 authentik.core.models import Group, PropertyMapping, Source
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.models import DomainlessURLValidator
|
||||
|
||||
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):
|
||||
"""Federate LDAP Directory with authentik, or create new accounts in LDAP."""
|
||||
|
||||
server_uri = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])],
|
||||
validators=[MultiURLValidator(schemes=["ldap", "ldaps"])],
|
||||
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_password = models.TextField(blank=True)
|
||||
start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS"))
|
||||
|
@ -82,25 +106,40 @@ class LDAPSource(Source):
|
|||
|
||||
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
|
||||
def connection(self) -> Connection:
|
||||
"""Get a fully connected and bound LDAP Connection"""
|
||||
if not self._connection:
|
||||
server = Server(self.server_uri, get_info=ALL, connect_timeout=LDAP_TIMEOUT)
|
||||
self._connection = Connection(
|
||||
server,
|
||||
raise_exceptions=True,
|
||||
user=self.bind_cn,
|
||||
password=self.bind_password,
|
||||
receive_timeout=LDAP_TIMEOUT,
|
||||
)
|
||||
connection = Connection(
|
||||
self.server,
|
||||
raise_exceptions=True,
|
||||
user=self.bind_cn,
|
||||
password=self.bind_password,
|
||||
receive_timeout=LDAP_TIMEOUT,
|
||||
)
|
||||
|
||||
self._connection.bind()
|
||||
if self.start_tls:
|
||||
self._connection.start_tls()
|
||||
return self._connection
|
||||
connection.bind()
|
||||
if self.start_tls:
|
||||
connection.start_tls()
|
||||
return connection
|
||||
|
||||
class Meta:
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||
},
|
||||
defaults,
|
||||
)
|
||||
except (IntegrityError, FieldError) as exc:
|
||||
except (IntegrityError, FieldError, TypeError) as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
|
|
|
@ -45,7 +45,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||
ak_user, created = self.update_or_create_attributes(
|
||||
User, {f"attributes__{LDAP_UNIQUENESS}": uniq}, defaults
|
||||
)
|
||||
except (IntegrityError, FieldError) as exc:
|
||||
except (IntegrityError, FieldError, TypeError) as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
|
|
|
@ -39,7 +39,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
|
|||
# to set the state with
|
||||
return
|
||||
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:
|
||||
sync_inst = sync(source)
|
||||
count = sync_inst.sync()
|
||||
|
|
|
@ -120,9 +120,9 @@ class LDAPSyncTests(TestCase):
|
|||
self.source.property_mappings_group.set(
|
||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/default-name")
|
||||
)
|
||||
self.source.save()
|
||||
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
self.source.save()
|
||||
group_sync = GroupLDAPSynchronizer(self.source)
|
||||
group_sync.sync()
|
||||
membership_sync = MembershipLDAPSynchronizer(self.source)
|
||||
|
@ -143,9 +143,9 @@ class LDAPSyncTests(TestCase):
|
|||
self.source.property_mappings_group.set(
|
||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
|
||||
)
|
||||
self.source.save()
|
||||
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
self.source.save()
|
||||
group_sync = GroupLDAPSynchronizer(self.source)
|
||||
group_sync.sync()
|
||||
membership_sync = MembershipLDAPSynchronizer(self.source)
|
||||
|
@ -168,9 +168,9 @@ class LDAPSyncTests(TestCase):
|
|||
self.source.property_mappings_group.set(
|
||||
LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
|
||||
)
|
||||
self.source.save()
|
||||
connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
self.source.save()
|
||||
user_sync = UserLDAPSynchronizer(self.source)
|
||||
user_sync.sync()
|
||||
group_sync = GroupLDAPSynchronizer(self.source)
|
||||
|
|
|
@ -29,14 +29,15 @@ def check_plex_token(self: MonitoredTask, source_slug: int):
|
|||
auth.get_user_info()
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Plex token is valid."]))
|
||||
except RequestException as exc:
|
||||
error = exception_to_string(exc).replace(source.plex_token, "$PLEX_TOKEN")
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.ERROR,
|
||||
["Plex token is invalid/an error occurred:", exception_to_string(exc)],
|
||||
["Plex token is invalid/an error occurred:", error],
|
||||
)
|
||||
)
|
||||
Event.new(
|
||||
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,
|
||||
).save()
|
||||
|
|
|
@ -3,12 +3,7 @@ from django.utils.timezone import now
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
TaskResult,
|
||||
TaskResultStatus,
|
||||
prefill_task,
|
||||
)
|
||||
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
@ -16,9 +11,8 @@ from authentik.sources.saml.models import SAMLSource
|
|||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
@prefill_task()
|
||||
def clean_temporary_users(self: MonitoredTask):
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def clean_temporary_users(self: PrefilledMonitoredTask):
|
||||
"""Remove temporary users created by SAML Sources"""
|
||||
_now = now()
|
||||
messages = []
|
||||
|
|
43
authentik/stages/prompt/migrations/0006_alter_prompt_type.py
Normal file
43
authentik/stages/prompt/migrations/0006_alter_prompt_type.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -113,6 +113,9 @@ class Prompt(SerializerModel):
|
|||
kwargs["label"] = ""
|
||||
if default:
|
||||
kwargs["default"] = default
|
||||
# May not set both `required` and `default`
|
||||
if "default" in kwargs:
|
||||
kwargs.pop("required", None)
|
||||
return field_class(**kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
|
@ -55,6 +55,7 @@ services:
|
|||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./media:/media
|
||||
- ./certs:/certs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./custom-templates:/templates
|
||||
- geoip:/geoip
|
||||
|
|
3
go.mod
3
go.mod
|
@ -29,10 +29,11 @@ require (
|
|||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/recws-org/recws v1.3.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/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||
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
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -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.3/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.6/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
|
||||
goauthentik.io/api v0.2021104.7 h1:JWKypuvYWWPqq8c8xLN8qVv5ny8TqsfmLdqNwJM9bZk=
|
||||
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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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-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-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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
package constants
|
||||
|
||||
const (
|
||||
OCTop = "top"
|
||||
OCDomain = "domain"
|
||||
OCNSContainer = "nsContainer"
|
||||
)
|
||||
|
||||
const (
|
||||
OCGroup = "group"
|
||||
OCGroupOfUniqueNames = "groupOfUniqueNames"
|
||||
|
@ -19,3 +25,42 @@ const (
|
|||
OUGroups = "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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,20 @@ package ldap
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/bind"
|
||||
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
)
|
||||
|
||||
type ProviderInstance struct {
|
||||
|
@ -50,6 +56,10 @@ func (pi *ProviderInstance) GetBaseGroupDN() string {
|
|||
return pi.GroupDN
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetBaseVirtualGroupDN() string {
|
||||
return pi.VirtualGroupDN
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetBaseUserDN() string {
|
||||
return pi.UserDN
|
||||
}
|
||||
|
@ -82,3 +92,77 @@ func (pi *ProviderInstance) GetFlowSlug() string {
|
|||
func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -4,16 +4,15 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/group"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
|
@ -35,26 +34,11 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
|
|||
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) {
|
||||
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{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
|
@ -75,7 +59,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
|||
}).Inc()
|
||||
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{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
|
@ -98,15 +82,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
|||
}).Inc()
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
entries := make([]*ldap.Entry, 0)
|
||||
|
||||
// Create a custom client to set additional headers
|
||||
c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig())
|
||||
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
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)
|
||||
scope := req.SearchRequest.Scope
|
||||
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC)
|
||||
|
||||
gEntries := make([]*ldap.Entry, 0)
|
||||
uEntries := make([]*ldap.Entry, 0)
|
||||
if scope >= 0 && req.BaseDN == baseDN {
|
||||
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
|
||||
entries = append(entries, ds.si.GetBaseEntry())
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
|
||||
}
|
||||
|
||||
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")
|
||||
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
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()
|
||||
if err != nil {
|
||||
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 {
|
||||
gEntries = append(gEntries, group.FromAPIGroup(g, ds.si).Entry())
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
if !flags.CanSearch {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range users.Results {
|
||||
uEntries = append(uEntries, group.FromAPIUser(u, ds.si).Entry())
|
||||
}
|
||||
}()
|
||||
wg.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
|
||||
groups = &g.Results
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err = errs.Wait()
|
||||
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
users, _, err := searchReq.Execute()
|
||||
uapisp.Finish()
|
||||
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, u := range users.Results {
|
||||
entries = append(entries, ds.si.UserEntry(u))
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -11,11 +11,11 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/group"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
)
|
||||
|
||||
type MemorySearcher struct {
|
||||
|
@ -37,29 +37,11 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
|
|||
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) {
|
||||
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{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
|
@ -80,7 +62,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
|||
}).Inc()
|
||||
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{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
|
@ -103,52 +85,132 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
|||
}).Inc()
|
||||
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()
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.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:
|
||||
for _, g := range ms.groups {
|
||||
entries = append(entries, group.FromAPIGroup(g, ms.si).Entry())
|
||||
entries := make([]*ldap.Entry, 0)
|
||||
|
||||
scope := req.SearchRequest.Scope
|
||||
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC)
|
||||
|
||||
if scope >= 0 && req.BaseDN == baseDN {
|
||||
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
|
||||
entries = append(entries, ms.si.GetBaseEntry())
|
||||
}
|
||||
for _, u := range ms.users {
|
||||
entries = append(entries, group.FromAPIUser(u, ms.si).Entry())
|
||||
}
|
||||
case "":
|
||||
fallthrough
|
||||
case constants.OCOrgPerson:
|
||||
fallthrough
|
||||
case constants.OCInetOrgPerson:
|
||||
fallthrough
|
||||
case constants.OCAKUser:
|
||||
fallthrough
|
||||
case constants.OCUser:
|
||||
for _, u := range ms.users {
|
||||
entries = append(entries, ms.si.UserEntry(u))
|
||||
|
||||
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
|
||||
}
|
||||
|
||||
var users *[]api.User
|
||||
var groups []*group.LDAPGroup
|
||||
|
||||
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 {
|
||||
if flags.CanSearch {
|
||||
groups = append(groups, group.FromAPIGroup(g, ms.si))
|
||||
} else {
|
||||
// If the user cannot search, we're going to only return
|
||||
// the groups they're in _and_ only return themselves
|
||||
// as a member.
|
||||
for _, u := range g.UsersObj {
|
||||
if flags.UserPk == u.Pk {
|
||||
// TODO: Is there a better way to clone this object?
|
||||
fg := api.NewGroup(g.Pk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u})
|
||||
fg.SetAttributes(*g.Attributes)
|
||||
fg.SetIsSuperuser(*g.IsSuperuser)
|
||||
groups = append(groups, group.FromAPIGroup(*fg, ms.si))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package search
|
||||
|
||||
import "github.com/nmcclain/ldap"
|
||||
import (
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
type Searcher interface {
|
||||
Search(req *Request) (ldap.ServerSearchResult, error)
|
||||
|
|
|
@ -19,6 +19,7 @@ type LDAPServerInstance interface {
|
|||
|
||||
GetBaseDN() string
|
||||
GetBaseGroupDN() string
|
||||
GetBaseVirtualGroupDN() string
|
||||
GetBaseUserDN() string
|
||||
|
||||
GetUserDN(string) string
|
||||
|
@ -32,4 +33,7 @@ type LDAPServerInstance interface {
|
|||
|
||||
GetFlags(string) (flags.UserFlags, bool)
|
||||
SetFlags(string, flags.UserFlags)
|
||||
|
||||
GetBaseEntry() *ldap.Entry
|
||||
GetNeededObjects(int, string, string) (bool, bool)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
||||
)
|
||||
|
||||
func BoolToString(in bool) string {
|
||||
|
@ -84,3 +85,35 @@ func MustHaveAttribute(attrs []*ldap.EntryAttribute, name string, value []string
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/gob"
|
||||
"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")
|
||||
}
|
||||
|
||||
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{
|
||||
ClientID: *p.ClientId,
|
||||
SupportedSigningAlgs: []string{"HS256"},
|
||||
SupportedSigningAlgs: []string{"RS256", "HS256"},
|
||||
})
|
||||
|
||||
// Configure an OpenID Connect aware OAuth2 client.
|
||||
|
@ -94,14 +101,14 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
|||
if !ok {
|
||||
return l
|
||||
}
|
||||
return l.WithField("request_username", c.Email)
|
||||
return l.WithField("request_username", c.PreferredUsername)
|
||||
}))
|
||||
mux.Use(func(inner http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
c, _ := a.getClaims(r)
|
||||
user := ""
|
||||
if c != nil {
|
||||
user = c.Email
|
||||
user = c.PreferredUsername
|
||||
}
|
||||
before := time.Now()
|
||||
inner.ServeHTTP(rw, r)
|
||||
|
|
|
@ -13,4 +13,6 @@ type Claims struct {
|
|||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
|
||||
RawToken string
|
||||
}
|
||||
|
|
|
@ -5,24 +5,34 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"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
|
||||
|
||||
// Legacy headers, remove after 2022.1
|
||||
r.Header.Set("X-Auth-Username", c.PreferredUsername)
|
||||
r.Header.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
|
||||
r.Header.Set("X-Forwarded-Email", c.Email)
|
||||
r.Header.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
|
||||
r.Header.Set("X-Forwarded-User", c.Sub)
|
||||
headers.Set("X-Auth-Username", c.PreferredUsername)
|
||||
headers.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
|
||||
headers.Set("X-Forwarded-Email", c.Email)
|
||||
headers.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
|
||||
headers.Set("X-Forwarded-User", c.Sub)
|
||||
|
||||
// New headers, unique prefix
|
||||
r.Header.Set("X-authentik-username", c.PreferredUsername)
|
||||
r.Header.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
|
||||
r.Header.Set("X-authentik-email", c.Email)
|
||||
r.Header.Set("X-authentik-name", c.Name)
|
||||
r.Header.Set("X-authentik-uid", c.Sub)
|
||||
headers.Set("X-authentik-username", c.PreferredUsername)
|
||||
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
|
||||
headers.Set("X-authentik-email", c.Email)
|
||||
headers.Set("X-authentik-name", c.Name)
|
||||
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
|
||||
// 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))
|
||||
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
|
||||
if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]interface{}); ok {
|
||||
|
@ -48,15 +58,7 @@ func (a *Application) addHeaders(r *http.Request, c *Claims) {
|
|||
return
|
||||
}
|
||||
for key, value := range additionalHeaders {
|
||||
r.Header.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)
|
||||
headers.Set(key, toString(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,9 @@ func (a *Application) configureForward() error {
|
|||
func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := a.getClaims(r)
|
||||
if claims != nil && err == nil {
|
||||
a.addHeaders(r, claims)
|
||||
copyHeadersToResponse(rw, r)
|
||||
a.addHeaders(rw.Header(), claims)
|
||||
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
|
||||
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
|
||||
return
|
||||
} else if claims == nil && a.IsAllowlisted(r) {
|
||||
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) {
|
||||
claims, err := a.getClaims(r)
|
||||
if claims != nil && err == nil {
|
||||
a.addHeaders(r, claims)
|
||||
copyHeadersToResponse(rw, r)
|
||||
a.addHeaders(rw.Header(), claims)
|
||||
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
|
||||
rw.WriteHeader(200)
|
||||
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
|
||||
return
|
||||
} else if claims == nil && a.IsAllowlisted(r) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
|
|
|
@ -39,7 +39,7 @@ func (a *Application) configureProxy() error {
|
|||
a.redirectToStart(rw, r)
|
||||
return
|
||||
} else {
|
||||
a.addHeaders(r, claims)
|
||||
a.addHeaders(r.Header, claims)
|
||||
}
|
||||
before := time.Now()
|
||||
rp.ServeHTTP(rw, r)
|
||||
|
|
|
@ -45,5 +45,6 @@ func (a *Application) redeemCallback(r *http.Request, shouldState string) (*Clai
|
|||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims.RawToken = rawIDToken
|
||||
return claims, nil
|
||||
}
|
||||
|
|
|
@ -56,3 +56,12 @@ func toString(in interface{}) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus"
|
||||
"goauthentik.io/internal/outpost/proxyv2/metrics"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
staticWeb "goauthentik.io/web"
|
||||
)
|
||||
|
||||
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) {
|
||||
staticFs := http.FileServer(http.FS(staticWeb.StaticDist))
|
||||
staticFs := http.FileServer(http.Dir("./web/dist/"))
|
||||
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)
|
||||
metrics.Requests.With(prometheus.Labels{
|
||||
"outpost_name": ps.akAPI.Outpost.Name,
|
||||
|
|
|
@ -9,33 +9,19 @@ import (
|
|||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
staticWeb "goauthentik.io/web"
|
||||
staticDocs "goauthentik.io/website"
|
||||
)
|
||||
|
||||
func (ws *WebServer) configureStatic() {
|
||||
statRouter := ws.lh.NewRoute().Subrouter()
|
||||
statRouter.Use(ws.staticHeaderMiddleware)
|
||||
indexLessRouter := statRouter.NewRoute().Subrouter()
|
||||
indexLessRouter.Use(web.DisableIndex)
|
||||
// Media files, always local
|
||||
fs := http.FileServer(http.Dir(config.G.Paths.Media))
|
||||
var distHandler http.Handler
|
||||
var distFs http.Handler
|
||||
var authentikHandler http.Handler
|
||||
var helpHandler http.Handler
|
||||
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))
|
||||
}
|
||||
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/"))
|
||||
indexLessRouter.PathPrefix("/static/dist/").Handler(distHandler)
|
||||
indexLessRouter.PathPrefix("/static/authentik/").Handler(authentikHandler)
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ function check_if_root {
|
|||
GROUP="authentik:${GROUP_NAME}"
|
||||
fi
|
||||
# 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
|
||||
}
|
||||
|
||||
|
|
|
@ -12,10 +12,6 @@ FROM docker.io/golang:1.17.3-bullseye AS builder
|
|||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
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
|
||||
RUN go build -o /go/proxy ./cmd/proxy
|
||||
|
@ -27,6 +23,10 @@ ARG GIT_BUILD_HASH
|
|||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
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" ]
|
||||
|
||||
|
|
44
schema.yml
44
schema.yml
|
@ -12058,11 +12058,6 @@ paths:
|
|||
name: additional_user_dn
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: authentication_flow
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: base_dn
|
||||
schema:
|
||||
|
@ -12075,11 +12070,6 @@ paths:
|
|||
name: enabled
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: enrollment_flow
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: group_membership_field
|
||||
schema:
|
||||
|
@ -12115,12 +12105,10 @@ paths:
|
|||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: policy_engine_mode
|
||||
name: peer_certificate
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
format: uuid
|
||||
- in: query
|
||||
name: property_mappings
|
||||
schema:
|
||||
|
@ -22461,6 +22449,12 @@ components:
|
|||
server_uri:
|
||||
type: string
|
||||
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:
|
||||
type: string
|
||||
start_tls:
|
||||
|
@ -22558,6 +22552,12 @@ components:
|
|||
type: string
|
||||
minLength: 1
|
||||
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:
|
||||
type: string
|
||||
bind_password:
|
||||
|
@ -27181,6 +27181,12 @@ components:
|
|||
type: string
|
||||
minLength: 1
|
||||
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:
|
||||
type: string
|
||||
bind_password:
|
||||
|
@ -28984,7 +28990,17 @@ components:
|
|||
items:
|
||||
type: string
|
||||
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:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
- external_host
|
||||
- name
|
||||
- oidc_configuration
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
BIN
web/icons/icon_discord.png
Normal file
BIN
web/icons/icon_discord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
BIN
web/icons/icon_discord_christmas.png
Normal file
BIN
web/icons/icon_discord_christmas.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
14
web/package-lock.json
generated
14
web/package-lock.json
generated
|
@ -15,7 +15,7 @@
|
|||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@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",
|
||||
"@lingui/cli": "^3.13.0",
|
||||
"@lingui/core": "^3.13.0",
|
||||
|
@ -1708,9 +1708,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2021.10.4-1638190705",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638190705.tgz",
|
||||
"integrity": "sha512-fEtKGX8F9BDnYWIF9vTxLEkqGkABRl+0M2sgCOd4XqiflNveDEQYMVZAK5yvNzCK8L4wIcbn7y8s/lCncEKJ2Q=="
|
||||
"version": "2021.10.4-1638522576",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638522576.tgz",
|
||||
"integrity": "sha512-ojnhGFPnEHXPeMULMtRUBoRVB8k0B73l3O5UL8NSipaY2ZC7jSscIQKDZWz7yvvx9NPMV34kKJ9NK8N+/jzfgw=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.6.0",
|
||||
|
@ -9895,9 +9895,9 @@
|
|||
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg=="
|
||||
},
|
||||
"@goauthentik/api": {
|
||||
"version": "2021.10.4-1638190705",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638190705.tgz",
|
||||
"integrity": "sha512-fEtKGX8F9BDnYWIF9vTxLEkqGkABRl+0M2sgCOd4XqiflNveDEQYMVZAK5yvNzCK8L4wIcbn7y8s/lCncEKJ2Q=="
|
||||
"version": "2021.10.4-1638522576",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638522576.tgz",
|
||||
"integrity": "sha512-ojnhGFPnEHXPeMULMtRUBoRVB8k0B73l3O5UL8NSipaY2ZC7jSscIQKDZWz7yvvx9NPMV34kKJ9NK8N+/jzfgw=="
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.6.0",
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@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",
|
||||
"@lingui/cli": "^3.13.0",
|
||||
"@lingui/core": "^3.13.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
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
|
||||
Policy: https://github.com/goauthentik/authentik/blob/master/SECURITY.md
|
||||
|
|
|
@ -17,6 +17,7 @@ import { AKResponse } from "../../api/Client";
|
|||
import { EVENT_REFRESH } from "../../constants";
|
||||
import { groupBy } from "../../utils";
|
||||
import "../EmptyState";
|
||||
import "../buttons/SpinnerButton";
|
||||
import "../chips/Chip";
|
||||
import "../chips/ChipGroup";
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.apiEndpoint(this.page)
|
||||
return this.apiEndpoint(this.page)
|
||||
.then((r) => {
|
||||
this.data = r;
|
||||
this.page = r.pagination.current;
|
||||
|
@ -319,19 +320,14 @@ export abstract class Table<T> extends LitElement {
|
|||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`<button
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
return html` <ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.fetch();
|
||||
}}
|
||||
class="pf-c-button pf-m-secondary"
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${t`Refresh`}
|
||||
</button>`;
|
||||
${t`Refresh`}</ak-spinner-button
|
||||
>`;
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
|
@ -350,12 +346,7 @@ export abstract class Table<T> extends LitElement {
|
|||
value=${ifDefined(this.search)}
|
||||
.onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.fetch();
|
||||
updateURLParams({
|
||||
search: value,
|
||||
});
|
||||
|
@ -382,12 +373,7 @@ export abstract class Table<T> extends LitElement {
|
|||
.pages=${this.data?.pagination}
|
||||
.pageChangeHandler=${(page: number) => {
|
||||
this.page = page;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.fetch();
|
||||
}}
|
||||
>
|
||||
</ak-table-pagination>`
|
||||
|
@ -442,12 +428,7 @@ export abstract class Table<T> extends LitElement {
|
|||
.pages=${this.data?.pagination}
|
||||
.pageChangeHandler=${(page: number) => {
|
||||
this.page = page;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.fetch();
|
||||
}}
|
||||
>
|
||||
</ak-table-pagination>
|
||||
|
|
|
@ -100,9 +100,7 @@ export class AuthenticatorValidateStage
|
|||
return html`<i class="fas fa-mobile-alt"></i>
|
||||
<div class="right">
|
||||
<p>${t`Duo push-notifications`}</p>
|
||||
<small
|
||||
>${t`Receive a push notification on your phone to prove your identity.`}</small
|
||||
>
|
||||
<small>${t`Receive a push notification on your device.`}</small>
|
||||
</div>`;
|
||||
case DeviceClassesEnum.Webauthn:
|
||||
return html`<i class="fas fa-mobile-alt"></i>
|
||||
|
|
|
@ -2387,6 +2387,14 @@ msgstr "Internal host"
|
|||
msgid "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
|
||||
msgid "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/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/oauth/OAuthSourceForm.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."
|
||||
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
|
||||
msgid "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"
|
||||
|
||||
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
|
||||
msgid "Receive a push notification on your phone to prove your identity."
|
||||
msgstr "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 device."
|
||||
|
||||
#: src/pages/flows/utils.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."
|
||||
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/StageBindingForm.ts
|
||||
msgid "Stage"
|
||||
|
@ -4739,6 +4760,7 @@ msgstr "TLS Authentication Certificate"
|
|||
#~ msgstr "TLS Server name"
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
||||
msgid "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}"
|
||||
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
|
||||
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"
|
||||
"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 above 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"
|
||||
"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 above the\n"
|
||||
"threshold."
|
||||
|
||||
#: 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."
|
||||
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
|
||||
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."
|
||||
|
|
|
@ -2370,6 +2370,12 @@ msgstr "Hôte interne"
|
|||
msgid "Internal host SSL Validation"
|
||||
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
|
||||
msgid "Invalid response action"
|
||||
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/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||
|
@ -2685,6 +2692,14 @@ msgstr ""
|
|||
msgid "Make sure to keep these tokens in a safe place."
|
||||
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
|
||||
msgid "Mark newly created users as inactive."
|
||||
msgstr "Marquer les utilisateurs nouvellements créés comme inactifs."
|
||||
|
@ -3582,8 +3597,12 @@ msgid "Re-evaluate policies"
|
|||
msgstr "Ré-évaluer les politiques"
|
||||
|
||||
#: 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é."
|
||||
msgid "Receive a push notification on your device."
|
||||
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/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."
|
||||
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/StageBindingForm.ts
|
||||
msgid "Stage"
|
||||
|
@ -4691,6 +4714,7 @@ msgstr "Certificat TLS d'authentification"
|
|||
#~ msgstr "Nom TLS du serveur"
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
||||
msgid "TLS Verification Certificate"
|
||||
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}"
|
||||
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
|
||||
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"
|
||||
"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 above 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."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/policies/dummy/DummyPolicyForm.ts
|
||||
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."
|
||||
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
|
||||
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."
|
||||
|
|
|
@ -2379,6 +2379,12 @@ msgstr ""
|
|||
msgid "Internal host SSL Validation"
|
||||
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
|
||||
msgid "Invalid response action"
|
||||
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/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||
|
@ -2696,6 +2703,14 @@ msgstr ""
|
|||
msgid "Make sure to keep these tokens in a safe place."
|
||||
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
|
||||
msgid "Mark newly created users as inactive."
|
||||
msgstr ""
|
||||
|
@ -3604,7 +3619,7 @@ msgid "Re-evaluate policies"
|
|||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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."
|
||||
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/StageBindingForm.ts
|
||||
msgid "Stage"
|
||||
|
@ -4731,6 +4750,7 @@ msgstr ""
|
|||
#~ msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
||||
msgid "TLS Verification Certificate"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4822,10 +4842,17 @@ msgstr ""
|
|||
msgid "The following objects use {objName}"
|
||||
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
|
||||
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"
|
||||
"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 above the\n"
|
||||
"threshold."
|
||||
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."
|
||||
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
|
||||
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
|
||||
msgstr ""
|
||||
|
|
|
@ -91,8 +91,13 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
|||
}
|
||||
|
||||
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 [
|
||||
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}>
|
||||
${item.privateKeyAvailable ? t`Yes` : t`No`}
|
||||
</ak-label>`,
|
||||
|
|
|
@ -47,8 +47,12 @@ export class ReputationPolicyForm extends ModelForm<ReputationPolicy, string> {
|
|||
${t`Allows/denys requests based on the users and/or the IPs reputation.`}
|
||||
</div>
|
||||
<div class="form-help-text">
|
||||
${t`The policy passes when the reputation score is above the threshold, and
|
||||
doesn't pass when either or both of the selected options are equal or less than the
|
||||
${t`Invalid login attempts will decrease the score for the client's IP, and 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.`}
|
||||
</div>
|
||||
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||
|
|
|
@ -176,7 +176,7 @@ export class ProxyProviderViewPage extends LitElement {
|
|||
<ak-label
|
||||
color=${this.provider.basicAuthEnabled
|
||||
? PFColor.Green
|
||||
: PFColor.Red}
|
||||
: PFColor.Grey}
|
||||
>
|
||||
${this.provider.basicAuthEnabled ? t`Yes` : t`No`}
|
||||
</ak-label>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { until } from "lit/directives/until.js";
|
|||
|
||||
import {
|
||||
CoreApi,
|
||||
CryptoApi,
|
||||
LDAPSource,
|
||||
LDAPSourceRequest,
|
||||
PropertymappingsApi,
|
||||
|
@ -124,6 +125,9 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
|||
class="pf-c-form-control"
|
||||
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 name="startTls">
|
||||
<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.`}
|
||||
</p>
|
||||
</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">
|
||||
<input
|
||||
type="text"
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed dist/*
|
||||
var StaticDist embed.FS
|
||||
|
||||
//go:embed authentik
|
||||
var StaticAuthentik embed.FS
|
||||
import _ "embed"
|
||||
|
||||
//go:embed robots.txt
|
||||
var RobotsTxt []byte
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
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.
|
57
website/docs/core/certificates.md
Normal file
57
website/docs/core/certificates.md
Normal 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.
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
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.
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
id: terminology
|
||||
title: Terminology
|
||||
slug: /terminology
|
||||
---
|
||||
|
||||
![](/img/authentik_objects.svg)
|
|
@ -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.
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
@ -50,7 +50,7 @@ services:
|
|||
traefik.http.routers.authentik.tls: true
|
||||
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.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
|
||||
|
||||
whoami:
|
||||
|
|
|
@ -9,13 +9,7 @@ spec:
|
|||
forwardAuth:
|
||||
address: http://outpost.company:9000/akprox/auth/traefik
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- Set-Cookie
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
authResponseHeadersRegex: ^.*$
|
||||
```
|
||||
|
||||
Add the following settings to your IngressRoute
|
||||
|
|
|
@ -5,13 +5,7 @@ http:
|
|||
forwardAuth:
|
||||
address: http://outpost.company:9000/akprox/auth/traefik
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- Set-Cookie
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
authResponseHeadersRegex: ^.*$
|
||||
routers:
|
||||
default-router:
|
||||
rule: "Host(`app.company`)"
|
||||
|
|
|
@ -2,20 +2,50 @@
|
|||
title: Overview
|
||||
---
|
||||
|
||||
The proxy outpost sets the following headers:
|
||||
The proxy outpost sets the following user-specific headers:
|
||||
|
||||
```
|
||||
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
|
||||
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.
|
||||
```
|
||||
- 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
|
||||
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
The outpost listens on both 9000 for HTTP and 9443 for HTTPS.
|
||||
|
|
141
website/integrations/services/sssd/index.md
Normal file
141
website/integrations/services/sssd/index.md
Normal 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
|
|
@ -43,6 +43,10 @@ Use these settings:
|
|||
|
||||
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 Password: The password you've given the user above
|
||||
- Base DN: The base DN which you want authentik to sync
|
||||
|
|
|
@ -45,6 +45,11 @@ In authentik, create a new LDAP Source in Resources -> Sources.
|
|||
Use these settings:
|
||||
|
||||
- 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 Password: The password you've given the user above
|
||||
- Base DN: `dc=freeipa,dc=company`
|
||||
|
|
|
@ -15,6 +15,11 @@ For FreeIPA, follow the [FreeIPA Integration](../freeipa/index.md)
|
|||
:::
|
||||
|
||||
- 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 password: Password used during the bind process.
|
||||
- Enable StartTLS: Enables StartTLS functionality. To use LDAPS instead, use port `636`.
|
||||
|
|
|
@ -4,10 +4,6 @@ module.exports = {
|
|||
type: "doc",
|
||||
id: "index",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "terminology",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Installation",
|
||||
|
@ -23,8 +19,15 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "applications",
|
||||
type: "category",
|
||||
label: "Core Concepts",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"core/terminology",
|
||||
"core/applications",
|
||||
"core/tenants",
|
||||
"core/certificates",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
|
@ -121,10 +124,6 @@ module.exports = {
|
|||
label: "Users & Groups",
|
||||
items: ["user-group/user", "user-group/group"],
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "tenants",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Maintenance",
|
||||
|
|
|
@ -47,6 +47,7 @@ module.exports = {
|
|||
"services/proxmox-ve/index",
|
||||
"services/rancher/index",
|
||||
"services/sentry/index",
|
||||
"services/sssd/index",
|
||||
"services/sonarr/index",
|
||||
"services/tautulli/index",
|
||||
"services/ubuntu-landscape/index",
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed help/*
|
||||
var Help embed.FS
|
Reference in a new issue