Compare commits
8 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
aba857753b | |||
022ff9b3a8 | |||
d6af506a78 | |||
080ac6b5bb | |||
622c0faebf | |||
935821857a | |||
5fe737326e | |||
ff0d3c3d63 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2023.6.0
|
||||
current_version = 2023.6.2
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2023.6.0"
|
||||
__version__ = "2023.6.2"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ from django.utils.http import urlencode
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter, UUIDFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import (
|
||||
@ -284,7 +284,7 @@ class UsersFilter(FilterSet):
|
||||
)
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||
uuid = CharFilter(field_name="uuid")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
path = CharFilter(
|
||||
field_name="path",
|
||||
|
@ -73,6 +73,7 @@ outposts:
|
||||
|
||||
ldap:
|
||||
task_timeout_hours: 2
|
||||
page_size: 50
|
||||
tls:
|
||||
ciphers: null
|
||||
|
||||
|
@ -9,6 +9,7 @@ from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
@ -92,7 +93,7 @@ class BaseLDAPSynchronizer:
|
||||
types_only=False,
|
||||
get_operational_attributes=False,
|
||||
controls=None,
|
||||
paged_size=5,
|
||||
paged_size=int(CONFIG.y("ldap.page_size", 50)),
|
||||
paged_criticality=False,
|
||||
):
|
||||
"""Search in pages, returns each page"""
|
||||
|
@ -30,12 +30,15 @@ CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
|
||||
def ldap_sync_all():
|
||||
"""Sync all sources"""
|
||||
for source in LDAPSource.objects.filter(enabled=True):
|
||||
ldap_sync_single(source)
|
||||
ldap_sync_single(source.pk)
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def ldap_sync_single(source: LDAPSource):
|
||||
def ldap_sync_single(source_pk: str):
|
||||
"""Sync a single source"""
|
||||
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
|
||||
if not source:
|
||||
return
|
||||
task = chain(
|
||||
# User and group sync can happen at once, they have no dependencies on each other
|
||||
group(
|
||||
@ -71,9 +74,8 @@ def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) ->
|
||||
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str, page_cache_key: str):
|
||||
"""Synchronization of an LDAP Source"""
|
||||
self.result_timeout_hours = int(CONFIG.y("ldap.task_timeout_hours"))
|
||||
try:
|
||||
source: LDAPSource = LDAPSource.objects.get(pk=source_pk)
|
||||
except LDAPSource.DoesNotExist:
|
||||
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
|
||||
if not source:
|
||||
# Because the source couldn't be found, we don't have a UID
|
||||
# to set the state with
|
||||
return
|
||||
|
@ -12,7 +12,7 @@ from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.models import FlowDesignation, FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
@ -82,6 +82,11 @@ class EmailStageView(ChallengeStageView):
|
||||
"""Helper function that sends the actual email. Implies that you've
|
||||
already checked that there is a pending user."""
|
||||
pending_user = self.get_pending_user()
|
||||
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
|
||||
# Pending user does not have a primary key, and we're in a recovery flow,
|
||||
# which means the user entered an invalid identifier, so we pretend to send the
|
||||
# email, to not disclose if the user exists
|
||||
return
|
||||
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
|
||||
if not email:
|
||||
email = pending_user.email
|
||||
|
@ -5,18 +5,20 @@ from unittest.mock import MagicMock, PropertyMock, patch
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
|
||||
class TestEmailStageSending(APITestCase):
|
||||
class TestEmailStageSending(FlowTestCase):
|
||||
"""Email tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -44,6 +46,13 @@ class TestEmailStageSending(APITestCase):
|
||||
):
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={
|
||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
|
||||
@ -54,6 +63,32 @@ class TestEmailStageSending(APITestCase):
|
||||
self.assertEqual(event.context["to_email"], [self.user.email])
|
||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||
|
||||
def test_pending_fake_user(self):
|
||||
"""Test with pending (fake) user"""
|
||||
self.flow.designation = FlowDesignation.RECOVERY
|
||||
self.flow.save()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={
|
||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_send_error(self):
|
||||
"""Test error during sending (sending will be retried)"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
|
@ -118,8 +118,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
username=uid_field,
|
||||
email=uid_field,
|
||||
)
|
||||
self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
if not current_stage.show_matched_user:
|
||||
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
|
||||
if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
|
||||
# When used in a recovery flow, always continue to not disclose if a user exists
|
||||
return attrs
|
||||
raise ValidationError("Failed to authenticate.")
|
||||
self.pre_user = pre_user
|
||||
if not current_stage.password_stage:
|
||||
|
@ -188,7 +188,7 @@ class TestIdentificationStage(FlowTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_recovery_flow(self):
|
||||
def test_link_recovery_flow(self):
|
||||
"""Test that recovery flow is linked correctly"""
|
||||
flow = create_test_flow()
|
||||
self.stage.recovery_flow = flow
|
||||
@ -226,6 +226,38 @@ class TestIdentificationStage(FlowTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_recovery_flow_invalid_user(self):
|
||||
"""Test that an invalid user can proceed in a recovery flow"""
|
||||
self.flow.designation = FlowDesignation.RECOVERY
|
||||
self.flow.save()
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
component="ak-stage-identification",
|
||||
user_fields=["email"],
|
||||
password_fields=False,
|
||||
show_source_labels=False,
|
||||
primary_action="Continue",
|
||||
sources=[
|
||||
{
|
||||
"challenge": {
|
||||
"component": "xak-flow-redirect",
|
||||
"to": "/source/oauth/login/test/",
|
||||
"type": ChallengeTypes.REDIRECT.value,
|
||||
},
|
||||
"icon_url": "/static/authentik/sources/default.svg",
|
||||
"name": "test",
|
||||
}
|
||||
],
|
||||
)
|
||||
form_data = {"uid_field": generate_id()}
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
response = self.client.post(url, form_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_api_validate(self):
|
||||
"""Test API validation"""
|
||||
self.assertTrue(
|
||||
|
@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.2}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -53,7 +53,7 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.6.2}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.6.0"
|
||||
const VERSION = "2023.6.2"
|
||||
|
@ -40,11 +40,17 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
"name": {u.Name},
|
||||
"displayName": {u.Name},
|
||||
"mail": {*u.Email},
|
||||
"objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser},
|
||||
"uidNumber": {pi.GetUidNumber(u)},
|
||||
"gidNumber": {pi.GetUidNumber(u)},
|
||||
"homeDirectory": {fmt.Sprintf("/home/%s", u.Username)},
|
||||
"sn": {u.Name},
|
||||
"objectClass": {
|
||||
constants.OCUser,
|
||||
constants.OCOrgPerson,
|
||||
constants.OCInetOrgPerson,
|
||||
constants.OCAKUser,
|
||||
constants.OCPosixAccount,
|
||||
},
|
||||
"uidNumber": {pi.GetUidNumber(u)},
|
||||
"gidNumber": {pi.GetUidNumber(u)},
|
||||
"homeDirectory": {fmt.Sprintf("/home/%s", u.Username)},
|
||||
"sn": {u.Name},
|
||||
})
|
||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||
}
|
||||
|
27
internal/outpost/ldap/entries_test.go
Normal file
27
internal/outpost/ldap/entries_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package ldap_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"beryju.io/ldap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api/v3"
|
||||
)
|
||||
|
||||
func Test_UserEntry(t *testing.T) {
|
||||
pi := ProviderInstance()
|
||||
u := api.User{
|
||||
Username: "foo",
|
||||
Name: "bar",
|
||||
}
|
||||
entry := pi.UserEntry(u)
|
||||
assert.Equal(t, "cn=foo,ou=users,dc=ldap,dc=goauthentik,dc=io", entry.DN)
|
||||
assert.Contains(t, entry.Attributes, &ldap.EntryAttribute{
|
||||
Name: "cn",
|
||||
Values: []string{u.Username},
|
||||
})
|
||||
assert.Contains(t, entry.Attributes, &ldap.EntryAttribute{
|
||||
Name: "displayName",
|
||||
Values: []string{u.Name},
|
||||
})
|
||||
}
|
31
internal/outpost/ldap/ldap_test.go
Normal file
31
internal/outpost/ldap/ldap_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package ldap_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"beryju.io/ldap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
oldap "goauthentik.io/internal/outpost/ldap"
|
||||
)
|
||||
|
||||
func ProviderInstance() *oldap.ProviderInstance {
|
||||
return &oldap.ProviderInstance{
|
||||
BaseDN: "dc=ldap,dc=goauthentik,dc=io",
|
||||
UserDN: "ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
VirtualGroupDN: "ou=virtual-groups,dc=ldap,dc=goauthentik,dc=io",
|
||||
GroupDN: "ou=groups,dc=ldap,dc=goauthentik,dc=io",
|
||||
}
|
||||
}
|
||||
|
||||
func AssertLDAPAttributes(t *testing.T, attrs []*ldap.EntryAttribute, expected *ldap.EntryAttribute) {
|
||||
found := false
|
||||
for _, attr := range attrs {
|
||||
if attr.Name == expected.Name {
|
||||
assert.Equal(t, expected.Values, attr.Values)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Key %s not found in ldap attributes", expected.Name)
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
||||
"type": "search",
|
||||
"app": selectedApp,
|
||||
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
|
||||
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
|
||||
req.Log().WithField("attributes", searchReq.Attributes).WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
@ -40,10 +40,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
||||
}
|
||||
selectedApp = selectedProvider.GetAppSlug()
|
||||
result, err := ls.searchRoute(req, selectedProvider)
|
||||
if err != nil {
|
||||
return result, nil
|
||||
}
|
||||
return ls.filterResultAttributes(req, result), nil
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) fallbackRootDSE(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||
|
@ -87,7 +87,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig())
|
||||
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
||||
|
||||
scope := req.SearchRequest.Scope
|
||||
scope := req.Scope
|
||||
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
||||
|
||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||
|
@ -75,19 +75,3 @@ func (r *Request) Log() *log.Entry {
|
||||
func (r *Request) RemoteAddr() string {
|
||||
return utils.GetIP(r.conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (r *Request) FilterLDAPAttributes(res ldap.ServerSearchResult, cb func(attr *ldap.EntryAttribute) bool) ldap.ServerSearchResult {
|
||||
for _, e := range res.Entries {
|
||||
newAttrs := []*ldap.EntryAttribute{}
|
||||
for _, attr := range e.Attributes {
|
||||
include := cb(attr)
|
||||
if include {
|
||||
newAttrs = append(newAttrs, attr)
|
||||
} else {
|
||||
r.Log().WithField("key", attr.Name).Trace("filtering out field based on LDAP request")
|
||||
}
|
||||
}
|
||||
e.Attributes = newAttrs
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -53,32 +53,3 @@ func (ls *LDAPServer) searchRoute(req *search.Request, pi *ProviderInstance) (ld
|
||||
req.Log().Trace("routing to default")
|
||||
return pi.searcher.Search(req)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) filterResultAttributes(req *search.Request, result ldap.ServerSearchResult) ldap.ServerSearchResult {
|
||||
allowedAttributes := []string{}
|
||||
if len(req.Attributes) == 1 && req.Attributes[0] == constants.SearchAttributeNone {
|
||||
allowedAttributes = []string{"objectClass"}
|
||||
}
|
||||
if len(req.Attributes) > 0 {
|
||||
// Only strictly filter allowed attributes if we haven't already narrowed the attributes
|
||||
// down
|
||||
if len(allowedAttributes) < 1 {
|
||||
allowedAttributes = req.Attributes
|
||||
}
|
||||
// Filter LDAP returned attributes by search requested attributes, taking "1.1"
|
||||
// into consideration
|
||||
return req.FilterLDAPAttributes(result, func(attr *ldap.EntryAttribute) bool {
|
||||
for _, allowed := range allowedAttributes {
|
||||
if allowed == constants.SearchAttributeAllUser ||
|
||||
allowed == constants.SearchAttributeAllOperational {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(allowed, attr.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ filterwarnings = [
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.6.0"
|
||||
version = "2023.6.2"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2023.6.0
|
||||
version: 2023.6.2
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
@ -250,6 +250,7 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
"organizationalPerson",
|
||||
"inetOrgPerson",
|
||||
"goauthentik.io/ldap/user",
|
||||
"posixAccount",
|
||||
],
|
||||
"uidNumber": 2000 + o_user.pk,
|
||||
"gidNumber": 2000 + o_user.pk,
|
||||
@ -277,6 +278,7 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
"organizationalPerson",
|
||||
"inetOrgPerson",
|
||||
"goauthentik.io/ldap/user",
|
||||
"posixAccount",
|
||||
],
|
||||
"uidNumber": 2000 + embedded_account.pk,
|
||||
"gidNumber": 2000 + embedded_account.pk,
|
||||
@ -304,6 +306,7 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
"organizationalPerson",
|
||||
"inetOrgPerson",
|
||||
"goauthentik.io/ldap/user",
|
||||
"posixAccount",
|
||||
],
|
||||
"uidNumber": 2000 + self.user.pk,
|
||||
"gidNumber": 2000 + self.user.pk,
|
||||
@ -320,3 +323,97 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
@reconcile_app("authentik_outposts")
|
||||
def test_ldap_schema(self):
|
||||
"""Test LDAP Schema"""
|
||||
self._prepare()
|
||||
server = Server("ldap://localhost:3389", get_info=ALL)
|
||||
_connection = Connection(
|
||||
server,
|
||||
raise_exceptions=True,
|
||||
user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
password=self.user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertIsNotNone(server.schema)
|
||||
self.assertTrue(server.schema.is_valid())
|
||||
self.assertIsNotNone(server.schema.object_classes["goauthentik.io/ldap/user"])
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
"default/flow-default-invalidation-flow.yaml",
|
||||
)
|
||||
@reconcile_app("authentik_outposts")
|
||||
def test_ldap_search_attrs_filter(self):
|
||||
"""Test search with attributes filtering"""
|
||||
# Remove akadmin to ensure list is correct
|
||||
# Remove user before starting container so it's not cached
|
||||
User.objects.filter(username="akadmin").delete()
|
||||
|
||||
outpost = self._prepare()
|
||||
server = Server("ldap://localhost:3389", get_info=ALL)
|
||||
_connection = Connection(
|
||||
server,
|
||||
raise_exceptions=True,
|
||||
user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
password=self.user.username,
|
||||
)
|
||||
_connection.bind()
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.LOGIN,
|
||||
user={
|
||||
"pk": self.user.pk,
|
||||
"email": self.user.email,
|
||||
"username": self.user.username,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
embedded_account = Outpost.objects.filter(managed=MANAGED_OUTPOST).first().user
|
||||
|
||||
_connection.search(
|
||||
"ou=Users,DC=ldaP,dc=goauthentik,dc=io",
|
||||
"(objectClass=user)",
|
||||
search_scope=SUBTREE,
|
||||
attributes=["cn"],
|
||||
)
|
||||
response: dict = _connection.response
|
||||
# Remove raw_attributes to make checking easier
|
||||
for obj in response:
|
||||
del obj["raw_attributes"]
|
||||
del obj["raw_dn"]
|
||||
o_user = outpost.user
|
||||
self.assertCountEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
"dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"attributes": {
|
||||
"cn": o_user.username,
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
{
|
||||
"dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"attributes": {
|
||||
"cn": embedded_account.username,
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
{
|
||||
"dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"attributes": {
|
||||
"cn": self.user.username,
|
||||
},
|
||||
"type": "searchResEntry",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2023.6.0";
|
||||
export const VERSION = "2023.6.2";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -277,6 +277,16 @@ Timeout in hours for LDAP synchronization tasks.
|
||||
|
||||
Defaults to `2`.
|
||||
|
||||
### `AUTHENTIK_LDAP__PAGE_SIZE`
|
||||
|
||||
:::info
|
||||
Requires authentik 2023.6.1
|
||||
:::
|
||||
|
||||
Page size for LDAP synchronization. Controls the number of objects created in a single task.
|
||||
|
||||
Defaults to `50`.
|
||||
|
||||
### `AUTHENTIK_LDAP__TLS__CIPHERS`
|
||||
|
||||
:::info
|
||||
|
@ -152,6 +152,10 @@ image:
|
||||
|
||||
- \*: fix [CVE-2023-36456](../security/CVE-2023-36456), Reported by [@thijsa](https://github.com/thijsa)
|
||||
|
||||
## Fixed in 2023.5.6
|
||||
|
||||
- \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
|
||||
|
||||
## API Changes
|
||||
|
||||
#### What's Changed
|
||||
|
@ -81,6 +81,18 @@ image:
|
||||
- web/user: refactor LibraryPage for testing, add CTA (#5665)
|
||||
- web: Replace lingui.js with lit-localize (#5761)
|
||||
|
||||
## Fixed in 2023.6.1
|
||||
|
||||
- core: fix UUID filter field for users api (#6203)
|
||||
- outposts/ldap: revert attribute filtering (#6188)
|
||||
- outposts/ldap: add test for attribute filtering (#6189)
|
||||
- sources/ldap: fix more errors (#6191)
|
||||
- sources/ldap: fix page size (#6187)
|
||||
|
||||
## Fixed in 2023.6.2
|
||||
|
||||
- \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
|
||||
|
||||
## API Changes
|
||||
|
||||
#### What's New
|
||||
|
27
website/docs/security/CVE-2023-39522.md
Normal file
27
website/docs/security/CVE-2023-39522.md
Normal file
@ -0,0 +1,27 @@
|
||||
# CVE-2023-39522
|
||||
|
||||
_Reported by [@markrassamni](https://github.com/markrassamni)_
|
||||
|
||||
## Username enumeration attack
|
||||
|
||||
### Summary
|
||||
|
||||
Using a recovery flow with an identification stage an attacker is able to determine if a username exists.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2023.5.6 and 2023.6.2 fix this issue.
|
||||
|
||||
### Impact
|
||||
|
||||
Only setups configured with a recovery flow are impacted by this.
|
||||
|
||||
### Details
|
||||
|
||||
An attacker can easily enumerate and check users' existence using the recovery flow, as a clear message is shown when a user doesn't exist. Depending on configuration this can either be done by username, email, or both.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
@ -322,6 +322,7 @@ module.exports = {
|
||||
},
|
||||
items: [
|
||||
"security/policy",
|
||||
"security/CVE-2023-39522",
|
||||
"security/CVE-2023-36456",
|
||||
"security/2023-06-cure53",
|
||||
"security/CVE-2023-26481",
|
||||
|
Reference in New Issue
Block a user