providers/ldap: Remove search group (#10639)
* remove search_group Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make api operations cleaerer Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * actually use get Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use correct api client for ldap Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix migration Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix migration warning Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix styling issue in dark mode Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated-ish fix button order in wizard Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix missing css import Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Optimised images with calibre/image-actions * Update index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * Update index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * Apply suggestions from code review Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * update release notes based on new template Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
@ -2,15 +2,25 @@
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.query import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters.filters import BooleanFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, ListField, SerializerMethodField
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.policies.types import PolicyResult
|
||||
from authentik.providers.ldap.models import LDAPProvider
|
||||
|
||||
|
||||
@ -23,7 +33,6 @@ class LDAPProviderSerializer(ProviderSerializer):
|
||||
model = LDAPProvider
|
||||
fields = ProviderSerializer.Meta.fields + [
|
||||
"base_dn",
|
||||
"search_group",
|
||||
"certificate",
|
||||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
@ -55,8 +64,6 @@ class LDAPProviderFilter(FilterSet):
|
||||
"name": ["iexact"],
|
||||
"authorization_flow__slug": ["iexact"],
|
||||
"base_dn": ["iexact"],
|
||||
"search_group__group_uuid": ["iexact"],
|
||||
"search_group__name": ["iexact"],
|
||||
"certificate__kp_uuid": ["iexact"],
|
||||
"certificate__name": ["iexact"],
|
||||
"tls_server_name": ["iexact"],
|
||||
@ -95,7 +102,6 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
"base_dn",
|
||||
"bind_flow_slug",
|
||||
"application_slug",
|
||||
"search_group",
|
||||
"certificate",
|
||||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
@ -116,3 +122,33 @@ class LDAPOutpostConfigViewSet(ListModelMixin, GenericViewSet):
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
|
||||
class LDAPCheckAccessSerializer(PassiveSerializer):
|
||||
has_search_permission = BooleanField(required=False)
|
||||
access = PolicyTestResultSerializer()
|
||||
|
||||
@extend_schema(
|
||||
request=None,
|
||||
parameters=[OpenApiParameter("app_slug", OpenApiTypes.STR)],
|
||||
responses={
|
||||
200: LDAPCheckAccessSerializer(),
|
||||
},
|
||||
operation_id="outposts_ldap_access_check",
|
||||
)
|
||||
@action(detail=True)
|
||||
def check_access(self, request: Request, pk) -> Response:
|
||||
"""Check access to a single application by slug"""
|
||||
provider = get_object_or_404(LDAPProvider, pk=pk)
|
||||
application = get_object_or_404(Application, slug=request.query_params["app_slug"])
|
||||
engine = PolicyEngine(application, request.user, request)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
result = engine.result
|
||||
access_response = PolicyResult(result.passing)
|
||||
response = self.LDAPCheckAccessSerializer(
|
||||
instance={
|
||||
"has_search_permission": request.user.has_perm("search_full_directory", provider),
|
||||
"access": access_response,
|
||||
}
|
||||
)
|
||||
return Response(response.data)
|
||||
|
@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-25 14:59
|
||||
from django.apps.registry import Apps
|
||||
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from django.db import migrations
|
||||
from django.contrib.auth.management import create_permissions
|
||||
|
||||
|
||||
def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from guardian.shortcuts import assign_perm
|
||||
from authentik.core.models import User
|
||||
from django.apps import apps as real_apps
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Permissions are only created _after_ migrations are run
|
||||
# - https://github.com/django/django/blob/43cdfa8b20e567a801b7d0a09ec67ddd062d5ea4/django/contrib/auth/apps.py#L19
|
||||
# - https://stackoverflow.com/a/72029063/1870445
|
||||
create_permissions(real_apps.get_app_config("authentik_providers_ldap"), using=db_alias)
|
||||
|
||||
LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider")
|
||||
|
||||
for provider in LDAPProvider.objects.using(db_alias).all():
|
||||
for user_pk in (
|
||||
provider.search_group.users.using(db_alias).all().values_list("pk", flat=True)
|
||||
):
|
||||
# We need the correct user model instance to assign the permission
|
||||
assign_perm("search_full_directory", User.objects.get(pk=user_pk), provider)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_ldap", "0003_ldapprovider_mfa_support_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="ldapprovider",
|
||||
options={
|
||||
"permissions": [("search_full_directory", "Search full LDAP directory")],
|
||||
"verbose_name": "LDAP Provider",
|
||||
"verbose_name_plural": "LDAP Providers",
|
||||
},
|
||||
),
|
||||
migrations.RunPython(migrate_search_group),
|
||||
migrations.RemoveField(
|
||||
model_name="ldapprovider",
|
||||
name="search_group",
|
||||
),
|
||||
]
|
@ -7,7 +7,7 @@ from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import BackchannelProvider, Group
|
||||
from authentik.core.models import BackchannelProvider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
@ -27,17 +27,6 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
help_text=_("DN under which objects are accessible."),
|
||||
)
|
||||
|
||||
search_group = models.ForeignKey(
|
||||
Group,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
help_text=_(
|
||||
"Users in this group can do search queries. "
|
||||
"If not set, every user can execute search queries."
|
||||
),
|
||||
)
|
||||
|
||||
tls_server_name = models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
@ -113,3 +102,6 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
class Meta:
|
||||
verbose_name = _("LDAP Provider")
|
||||
verbose_name_plural = _("LDAP Providers")
|
||||
permissions = [
|
||||
("search_full_directory", _("Search full LDAP directory")),
|
||||
]
|
||||
|
@ -154,6 +154,7 @@ class RadiusOutpostConfigViewSet(ListModelMixin, GenericViewSet):
|
||||
responses={
|
||||
200: RadiusCheckAccessSerializer(),
|
||||
},
|
||||
operation_id="outposts_radius_access_check",
|
||||
)
|
||||
@action(detail=True)
|
||||
def check_access(self, request: Request, pk) -> Response:
|
||||
|
@ -14,7 +14,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="duodevice",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)),
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.UTC)
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
|
@ -14,7 +14,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="smsdevice",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)),
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.UTC)
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
|
@ -14,7 +14,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="staticdevice",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)),
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.UTC)
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
|
@ -14,7 +14,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="totpdevice",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)),
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.UTC)
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
|
@ -14,7 +14,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="webauthndevice",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)),
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0, tzinfo=datetime.UTC)
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
|
@ -5131,12 +5131,6 @@
|
||||
"title": "Base dn",
|
||||
"description": "DN under which objects are accessible."
|
||||
},
|
||||
"search_group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Search group",
|
||||
"description": "Users in this group can do search queries. If not set, every user can execute search queries."
|
||||
},
|
||||
"certificate": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
|
@ -120,21 +120,6 @@ func (fe *FlowExecutor) DelegateClientIP(a string) {
|
||||
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, fe.cip)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) {
|
||||
acsp := sentry.StartSpan(fe.Context, "authentik.outposts.flow_executor.check_access")
|
||||
defer acsp.Finish()
|
||||
p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(acsp.Context(), appSlug).Execute()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check access: %w", err)
|
||||
}
|
||||
if !p.Passing {
|
||||
fe.log.Info("Access denied for user")
|
||||
return false, nil
|
||||
}
|
||||
fe.log.Debug("User has access")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) getAnswer(stage StageComponent) string {
|
||||
if v, o := fe.Answers[stage]; o {
|
||||
return v
|
||||
|
@ -58,8 +58,10 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
access, err := fe.CheckApplicationAccess(db.si.GetAppSlug())
|
||||
if !access {
|
||||
access, _, err := fe.ApiClient().OutpostsApi.OutpostsLdapAccessCheck(
|
||||
req.Context(), db.si.GetProviderID(),
|
||||
).AppSlug(db.si.GetAppSlug()).Execute()
|
||||
if !access.Access.Passing {
|
||||
req.Log().Info("Access denied for user")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": db.si.GetOutpostName(),
|
||||
@ -93,12 +95,11 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
|
||||
req.Log().WithError(err).Warning("failed to get user info")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
cs := db.SearchAccessCheck(userInfo.User)
|
||||
flags.UserPk = userInfo.User.Pk
|
||||
flags.CanSearch = cs != nil
|
||||
flags.CanSearch = access.HasSearchPermission != nil
|
||||
db.si.SetFlags(req.BindDN, &flags)
|
||||
if flags.CanSearch {
|
||||
req.Log().WithField("group", cs).Info("Allowed access to search")
|
||||
req.Log().Debug("Allowed access to search")
|
||||
}
|
||||
uisp.Finish()
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/outpost/flow"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
@ -47,22 +46,6 @@ func (db *DirectBinder) GetUsername(dn string) (string, error) {
|
||||
return "", errors.New("failed to find cn")
|
||||
}
|
||||
|
||||
// SearchAccessCheck Check if the current user is allowed to search
|
||||
func (db *DirectBinder) SearchAccessCheck(user api.UserSelf) *string {
|
||||
for _, group := range user.Groups {
|
||||
for _, allowedGroup := range db.si.GetSearchAllowedGroups() {
|
||||
if allowedGroup == nil {
|
||||
continue
|
||||
}
|
||||
db.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
||||
if group.Pk == allowedGroup.String() {
|
||||
return &group.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DirectBinder) TimerFlowCacheExpiry(ctx context.Context) {
|
||||
fe := flow.NewFlowExecutor(ctx, db.si.GetAuthenticationFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{})
|
||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/api/v3"
|
||||
@ -31,14 +30,13 @@ type ProviderInstance struct {
|
||||
s *LDAPServer
|
||||
log *log.Entry
|
||||
|
||||
tlsServerName *string
|
||||
cert *tls.Certificate
|
||||
certUUID string
|
||||
outpostName string
|
||||
outpostPk int32
|
||||
searchAllowedGroups []*strfmt.UUID
|
||||
boundUsersMutex *sync.RWMutex
|
||||
boundUsers map[string]*flags.UserFlags
|
||||
tlsServerName *string
|
||||
cert *tls.Certificate
|
||||
certUUID string
|
||||
outpostName string
|
||||
providerPk int32
|
||||
boundUsersMutex *sync.RWMutex
|
||||
boundUsers map[string]*flags.UserFlags
|
||||
|
||||
uidStartNumber int32
|
||||
gidStartNumber int32
|
||||
@ -105,8 +103,8 @@ func (pi *ProviderInstance) GetInvalidationFlowSlug() string {
|
||||
return pi.invalidationFlowSlug
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
|
||||
return pi.searchAllowedGroups
|
||||
func (pi *ProviderInstance) GetProviderID() int32 {
|
||||
return pi.providerPk
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool) {
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/api/v3"
|
||||
@ -23,7 +22,7 @@ import (
|
||||
|
||||
func (ls *LDAPServer) getCurrentProvider(pk int32) *ProviderInstance {
|
||||
for _, p := range ls.providers {
|
||||
if p.outpostPk == pk {
|
||||
if p.providerPk == pk {
|
||||
return p
|
||||
}
|
||||
}
|
||||
@ -77,7 +76,6 @@ func (ls *LDAPServer) Refresh() error {
|
||||
appSlug: provider.ApplicationSlug,
|
||||
authenticationFlowSlug: provider.BindFlowSlug,
|
||||
invalidationFlowSlug: invalidationFlow,
|
||||
searchAllowedGroups: []*strfmt.UUID{(*strfmt.UUID)(provider.SearchGroup.Get())},
|
||||
boundUsersMutex: usersMutex,
|
||||
boundUsers: users,
|
||||
s: ls,
|
||||
@ -87,7 +85,7 @@ func (ls *LDAPServer) Refresh() error {
|
||||
gidStartNumber: provider.GetGidStartNumber(),
|
||||
mfaSupport: provider.GetMfaSupport(),
|
||||
outpostName: ls.ac.Outpost.Name,
|
||||
outpostPk: provider.Pk,
|
||||
providerPk: provider.Pk,
|
||||
}
|
||||
if kp := provider.Certificate.Get(); kp != nil {
|
||||
err := ls.cs.AddKeypair(*kp)
|
||||
|
@ -2,7 +2,6 @@ package server
|
||||
|
||||
import (
|
||||
"beryju.io/ldap"
|
||||
"github.com/go-openapi/strfmt"
|
||||
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
@ -15,7 +14,7 @@ type LDAPServerInstance interface {
|
||||
GetAuthenticationFlowSlug() string
|
||||
GetInvalidationFlowSlug() string
|
||||
GetAppSlug() string
|
||||
GetSearchAllowedGroups() []*strfmt.UUID
|
||||
GetProviderID() int32
|
||||
|
||||
UserEntry(u api.User) *ldap.Entry
|
||||
|
||||
|
@ -45,7 +45,9 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
access, _, err := fe.ApiClient().OutpostsApi.OutpostsRadiusCheckAccessRetrieve(r.Context(), r.pi.providerId).AppSlug(r.pi.appSlug).Execute()
|
||||
access, _, err := fe.ApiClient().OutpostsApi.OutpostsRadiusAccessCheck(
|
||||
r.Context(), r.pi.providerId,
|
||||
).AppSlug(r.pi.appSlug).Execute()
|
||||
if err != nil {
|
||||
r.Log().WithField("username", username).WithError(err).Warning("failed to check access")
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
|
83
schema.yml
@ -9641,6 +9641,44 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/outposts/ldap/{id}/check_access/:
|
||||
get:
|
||||
operationId: outposts_ldap_access_check
|
||||
description: Check access to a single application by slug
|
||||
parameters:
|
||||
- in: query
|
||||
name: app_slug
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this LDAP Provider.
|
||||
required: true
|
||||
tags:
|
||||
- outposts
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LDAPCheckAccess'
|
||||
description: ''
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/outposts/proxy/:
|
||||
get:
|
||||
operationId: outposts_proxy_list
|
||||
@ -9755,7 +9793,7 @@ paths:
|
||||
description: ''
|
||||
/outposts/radius/{id}/check_access/:
|
||||
get:
|
||||
operationId: outposts_radius_check_access_retrieve
|
||||
operationId: outposts_radius_access_check
|
||||
description: Check access to a single application by slug
|
||||
parameters:
|
||||
- in: query
|
||||
@ -18342,15 +18380,6 @@ paths:
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: search_group__group_uuid__iexact
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: search_group__name__iexact
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: tls_server_name__iexact
|
||||
schema:
|
||||
@ -40705,6 +40734,16 @@ components:
|
||||
- direct
|
||||
- cached
|
||||
type: string
|
||||
LDAPCheckAccess:
|
||||
type: object
|
||||
description: Base serializer class which doesn't implement create/update methods
|
||||
properties:
|
||||
has_search_permission:
|
||||
type: boolean
|
||||
access:
|
||||
$ref: '#/components/schemas/PolicyTestResult'
|
||||
required:
|
||||
- access
|
||||
LDAPDebug:
|
||||
type: object
|
||||
properties:
|
||||
@ -40749,12 +40788,6 @@ components:
|
||||
type: string
|
||||
description: Prioritise backchannel slug over direct application slug
|
||||
readOnly: true
|
||||
search_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Users in this group can do search queries. If not set, every
|
||||
user can execute search queries.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
@ -40852,12 +40885,6 @@ components:
|
||||
base_dn:
|
||||
type: string
|
||||
description: DN under which objects are accessible.
|
||||
search_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Users in this group can do search queries. If not set, every
|
||||
user can execute search queries.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
@ -40934,12 +40961,6 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: DN under which objects are accessible.
|
||||
search_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Users in this group can do search queries. If not set, every
|
||||
user can execute search queries.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
@ -45706,12 +45727,6 @@ components:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: DN under which objects are accessible.
|
||||
search_group:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Users in this group can do search queries. If not set, every
|
||||
user can execute search queries.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
|
@ -5,6 +5,7 @@ from time import sleep
|
||||
|
||||
from docker.client import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from guardian.shortcuts import assign_perm
|
||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
||||
|
||||
@ -54,9 +55,9 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
ldap: LDAPProvider = LDAPProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
|
||||
search_group=self.user.ak_groups.first(),
|
||||
search_mode=APIAccessMode.CACHED,
|
||||
)
|
||||
assign_perm("search_full_directory", self.user, ldap)
|
||||
# we need to create an application to actually access the ldap
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=ldap)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
|
@ -43,10 +43,6 @@ export const mfaSupportHelp = msg(
|
||||
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
|
||||
);
|
||||
|
||||
export const groupHelp = msg(
|
||||
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
);
|
||||
|
||||
export const cryptoCertificateHelp = msg(
|
||||
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-core-group-search";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
@ -24,7 +23,6 @@ import {
|
||||
bindModeOptions,
|
||||
cryptoCertificateHelp,
|
||||
gidStartNumberHelp,
|
||||
groupHelp,
|
||||
mfaSupportHelp,
|
||||
searchModeOptions,
|
||||
tlsServerNameHelp,
|
||||
@ -65,18 +63,6 @@ export class ApplicationWizardApplicationDetails extends WithBrandConfig(BasePro
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Search group")}
|
||||
name="searchGroup"
|
||||
.errorMessages=${errors?.searchGroup ?? []}
|
||||
>
|
||||
<ak-core-group-search
|
||||
name="searchGroup"
|
||||
group=${ifDefined(provider?.searchGroup ?? nothing)}
|
||||
></ak-core-group-search>
|
||||
<p class="pf-c-form__helper-text">${groupHelp}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Bind mode")}
|
||||
name="bindMode"
|
||||
|
@ -5,19 +5,25 @@ import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
|
||||
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
static get styles() {
|
||||
return super.styles.concat(PFList);
|
||||
}
|
||||
|
||||
renderModeDescription() {
|
||||
return html`<p class="pf-u-mb-xl">
|
||||
return html`<p>
|
||||
${msg(
|
||||
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
|
||||
)}
|
||||
</p>
|
||||
<div class="pf-u-mb-xl">
|
||||
<div>
|
||||
${msg("An example setup can look like this:")}
|
||||
<ul class="pf-c-list">
|
||||
<li>${msg("authentik running on auth.example.com")}</li>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-core-group-search";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
|
@ -42,7 +42,7 @@ class ProviderMethodStep implements ApplicationStepType {
|
||||
valid = false;
|
||||
|
||||
get buttons() {
|
||||
return [BackStep, this.valid ? NextStep : DisabledNextStep, CancelWizard];
|
||||
return [this.valid ? NextStep : DisabledNextStep, BackStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -58,7 +58,7 @@ class ProviderStepDetails implements ApplicationStepType {
|
||||
disabled = true;
|
||||
valid = false;
|
||||
get buttons() {
|
||||
return [BackStep, this.valid ? SubmitStep : DisabledNextStep, CancelWizard];
|
||||
return [this.valid ? SubmitStep : DisabledNextStep, BackStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -15,10 +15,7 @@ import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
Group,
|
||||
LDAPAPIAccessMode,
|
||||
LDAPProvider,
|
||||
ProvidersApi,
|
||||
@ -73,37 +70,6 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Search group")} name="searchGroup">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group?.pk;
|
||||
}}
|
||||
.selected=${(group: Group): boolean => {
|
||||
return group.pk === this.instance?.searchGroup;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Bind mode")} name="bindMode">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
|
@ -45,11 +45,6 @@ body {
|
||||
.pf-c-card.pf-m-non-selectable-raised {
|
||||
--pf-c-card--BackgroundColor: var(--ak-dark-background-lighter);
|
||||
}
|
||||
.pf-c-card.pf-m-hoverable-raised::before,
|
||||
.pf-c-card.pf-m-selectable-raised::before,
|
||||
.pf-c-card.pf-m-non-selectable-raised::before {
|
||||
--pf-c-card--m-selectable-raised--before--BackgroundColor: var(--ak-dark-background-light);
|
||||
}
|
||||
.pf-c-card__title,
|
||||
.pf-c-card__body {
|
||||
color: var(--ak-dark-foreground);
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB |
@ -1,17 +1,15 @@
|
||||
---
|
||||
title: Generic Setup
|
||||
title: Create an LDAP provider
|
||||
---
|
||||
|
||||
### Create User/Group
|
||||
### Create Service account
|
||||
|
||||
1. Create a new user account to bind with under _Directory_ -> _Users_ -> _Create_, in this example called `ldapservice`.
|
||||
|
||||
Note the DN of this user will be `cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io`
|
||||
|
||||
2. Create a new group for LDAP searches. In this example `ldapsearch`. Add the `ldapservice` user to this new group.
|
||||
|
||||
:::info
|
||||
Note: The `default-authentication-flow` validates MFA by default, and currently everything but SMS-based devices are supported by LDAP. If you plan to use only dedicated service accounts to bind to LDAP, or don't use SMS-based authenticators, then you can use the default flow and skip the extra steps below and continue at [Create LDAP Provider](#create-ldap-provider)
|
||||
Note: The `default-authentication-flow` validates MFA by default, and currently everything but SMS-based devices and WebAuthn devices are supported by LDAP. If you plan to use only dedicated service accounts to bind to LDAP, or don't use SMS-based authenticators, then you can use the default flow and skip the extra steps below and continue at [Create LDAP Application & Provider](#create-ldap-application--provider)
|
||||
:::
|
||||
|
||||
### LDAP Flow
|
||||
@ -20,20 +18,20 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
|
||||
|
||||
1. Create a new identification stage. _Flows & Stage_ -> _Stages_ -> _Create_
|
||||

|
||||
2. Name it something meaningful like `ldap-identification-stage`. Select User fields Username and Email (and UPN if it is relevant to your setup).
|
||||
2. Name it `ldap-identification-stage`. Select User fields Username and Email (and UPN if it is relevant to your setup).
|
||||

|
||||
3. Create a new password stage. _Flows & Stage_ -> _Stages_ -> _Create_
|
||||

|
||||
4. Name it something meaningful like `ldap-authentication-password`. Leave the defaults for Backends.
|
||||
4. Name it `ldap-authentication-password`. Leave the defaults for Backends.
|
||||

|
||||
5. Create a new user login stage. _Flows & Stage_ -> _Stages_ -> _Create_
|
||||

|
||||
6. Name it something meaningful like `ldap-authentication-login`.
|
||||
6. Name it `ldap-authentication-login`.
|
||||

|
||||
|
||||
#### Create Custom Flow
|
||||
|
||||
1. Create a new authentication flow under _Flows & Stage_ -> _Flows_ -> _Create_, and name it something meaningful like `ldap-authentication-flow`
|
||||
1. Create a new authentication flow under _Flows & Stage_ -> _Flows_ -> _Create_, and name it `ldap-authentication-flow`
|
||||

|
||||
2. Click the newly created flow and choose _Stage Bindings_.
|
||||

|
||||
@ -46,22 +44,23 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
|
||||
6. Change the Password stage to `ldap-authentication-password`.
|
||||

|
||||
|
||||
### Create LDAP Provider
|
||||
### Create LDAP Application & Provider
|
||||
|
||||
1. Create the LDAP Provider under _Applications_ -> _Providers_ -> _Create_.
|
||||
1. Create the LDAP Application under _Applications_ -> _Applications_ -> _Create With Wizard_ and name it `LDAP`.
|
||||

|
||||
2. Name is something meaningful like `LDAP`, bind the custom flow created previously (or the default flow, depending on setup) and specify the search group created earlier.
|
||||

|
||||
|
||||
### Create LDAP Application
|
||||
### Assign LDAP permissions
|
||||
|
||||
1. Create the LDAP Application under _Applications_ -> _Applications_ -> _Create_ and name it something meaningful like `LDAP`. Choose the provider created in the previous step.
|
||||

|
||||
1. Navigate to the LDAP Provider under _Applications_ -> _Providers_ -> `Provider for LDAP`.
|
||||
2. Switch to the _Permissions_ tab.
|
||||
3. Click the _Assign to new user_ button to select a user to assign the full directory search permission to.
|
||||
4. Select the `ldapservice` user in the modal by typing in its username. Select the _Search full LDAP directory_ permission and click _Assign_
|
||||
|
||||
### Create LDAP Outpost
|
||||
|
||||
1. Create (or update) the LDAP Outpost under _Applications_ -> _Outposts_ -> _Create_. Set the Type to `LDAP` and choose the `LDAP` application created in the previous step.
|
||||

|
||||

|
||||
|
||||
:::info
|
||||
The LDAP Outpost selects different providers based on their Base DN. Adding multiple providers with the same Base DN will result in inconsistent access
|
||||
|
@ -10,7 +10,7 @@ Note: This provider requires the deployment of the [LDAP Outpost](../../outposts
|
||||
|
||||
All users and groups in authentik's database are searchable. Currently, there is limited support for filters (you can only search for objectClass), but this will be expanded in further releases.
|
||||
|
||||
Binding against the LDAP Server uses a flow in the background. This allows you to use the same policies and flows as you do for web-based logins. For more info, see [Bind modes](#bind-modes).
|
||||
Binding against the LDAP Server uses a flow in the background. This allows you to use the same policies and flows as you do for web-based logins. For more info, see [Bind modes](#binding--bind-modes).
|
||||
|
||||
You can configure under which base DN the information should be available. For this documentation we'll use the default of `DC=ldap,DC=goauthentik,DC=io`.
|
||||
|
||||
@ -72,7 +72,7 @@ This enables you to bind on port 636 using LDAPS.
|
||||
|
||||
See the integration guide for [sssd](../../../integrations/services/sssd/) for an example guide.
|
||||
|
||||
## Bind Modes
|
||||
## Binding & Bind Modes
|
||||
|
||||
All bind modes rely on flows.
|
||||
|
||||
@ -102,7 +102,15 @@ In this mode, the outpost will always execute the configured flow when a new bin
|
||||
|
||||
This mode uses the same logic as direct bind, however the result is cached for the entered credentials, and saved in memory for the standard session duration. Sessions are saved independently, meaning that revoking sessions does _not_ remove them from the outpost, and neither will changing a users credentials.
|
||||
|
||||
## Search Modes
|
||||
## Searching & Search Modes
|
||||
|
||||
Any user that is authorized to access the LDAP provider's application can execute search the LDAP directory. Without explicit permissions to do broader searches, a user's search request will return information about themselves, including user info, group info, and group membership.
|
||||
|
||||
[Users](../../user-group-role/user/index.mdx) and [roles](../../user-group-role/roles/index.mdx) can be assigned the permission "Search full LDAP directory" to allow them to search the full LDAP directory and retrieve information about all users in the authentik instance.
|
||||
|
||||
:::info
|
||||
Up to authentik version 2024.8 this was managed using the "Search group" attribute in the LDAP Provider, where users could be added to a group to grant them this permission. With authentik 2024.8 this is automatically migrated to the "Search full LDAP directory" permission, which can be assigned more flexibly.
|
||||
:::
|
||||
|
||||
#### Direct search
|
||||
|
||||
|
@ -70,16 +70,28 @@ To try out the release candidate, replace your Docker image tag with the latest
|
||||
|
||||
It is now possible to configure a SAML Source to decrypt and validate encrypted assertions. This can be configured by certaing a [Certificate-keypair](../../core/certificates.md) and selecting it in the SAML Source.
|
||||
|
||||
- **Removal of LDAP Provider search group**
|
||||
|
||||
The LDAP provider now uses RBAC to assign the permission to search the full directory instead of requiring a dedicated group to be created. As part of the upgrade, existing search groups' users are migrated to grant the required permission to search the full directory.
|
||||
|
||||
- **RBAC support for Blueprints and Terraform**
|
||||
|
||||
RBAC permissions for global/object level permissions for users/roles can now be managed via blueprints and Terraform. This allows for the automatic configuration of permissions.
|
||||
|
||||
## Upgrading
|
||||
|
||||
This release does not introduce any new requirements.
|
||||
This release does not introduce any new requirements. You can follow the upgrade instructions below; for more detailed information about upgrading authentik, refer to our [Upgrade documentation](../../installation/upgrade.mdx).
|
||||
|
||||
### docker-compose
|
||||
:::warning
|
||||
When you upgrade, be aware that the version of the authentik instance and of any outposts must be the same. We recommended that you always upgrade any outposts at the same time you upgrade your authentik instance.
|
||||
:::
|
||||
|
||||
### Docker Compose
|
||||
|
||||
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
|
||||
|
||||
```shell
|
||||
wget -O docker-compose.yml https://goauthentik.io/version/2024.8/docker-compose.yml
|
||||
wget -O docker-compose.yml https://goauthentik.io/version/xxxx.x/docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
@ -91,7 +103,7 @@ Upgrade the Helm Chart to the new version, using the following commands:
|
||||
|
||||
```shell
|
||||
helm repo update
|
||||
helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.8
|
||||
helm upgrade authentik authentik/authentik -f values.yaml --version ^xxxx.x
|
||||
```
|
||||
|
||||
## Minor changes/fixes
|
||||
|