You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hi - I wanted to create a discussion thread for this issue: #14082
The short background is: the LDAP outpost returns all sorts of crufty service account stuff when running a directory search that would be better to keep out of the search results. The Authentik team have kindly agreed to put this on the wishlist, so the question is what are the requirements and how to do it.
I've been digging into the structure, and personally I think the easiest/most effective way is defining an allow / deny list of groups at the provider level, and then extending the config schema to expose that for the outpost, with the outpost actually implementing the filter. I think this mirrors the existing check_access config which determines if the outpost will let the user actually get the full directory. I do think we should have both an allow and a deny list (not sure if they need to be mutually exclusive) because some people want to return only certain groups and others will just want to filter out the service accounts. I also think we need a toggle as to whether we compel inclusion of the bound LDAP account in the results this - that account may itself be a form of a "service account" that people want to filter but things may break if it doesn't show up in the results.
personally, I saw this as an outpost-level configuration for now; when this becomes wildly popular (hahaha) we could go back and revisit filtering by user or group or whatever. I also think that at some point this could be extended into a more generic filtering interface, but that also seems like it is down the line.
to be clear, I think this needs to be positioned as an administrative / convenience feature - the underlying account bound to LDAP would still have the same directory search permissions, so if they could bypass the outpost and hit the core API directly they could probably still get access to the whole directory. I don't think it is worth trying to build a whole new filter in the core API, unless this is a broader need?
I used Claude to help me review and articulate this more "formally" below, so apologies in advance if "we" got this totally wrong. I also included a summary of some other implementation paths I considered.
Happy to take any feedback, and if the team agrees, happy to do some work on a preliminary implementation.
--Tom
Proposal: group-scoped LDAP search visibility
File/line references below point to main as of this writing (2026-08-10) and will drift as the repo moves — treat them as pointers to the right file/function, not permanent anchors.
TL;DR
Give LDAPProvider an include-groups / exclude-groups list. The outpost already has all the machinery to enforce something like this — it already does a near-identical self-vs-everyone split for search permissions today, entirely client-side in Go, using a flag cached at bind time. We'd extend that exact mechanism to also apply a group filter, reusing filter parameters the Users/Groups API already supports. No new Core endpoint, and nothing about the shared /api/v3/core/users/ API that the rest of authentik uses would change at all.
Also proposing: support both an include list and an exclude list (not just whitelist) since "hide the service-account groups" is a more common need than "curate every visible group by hand," and the bound user should always be able to see their own entry regardless of the filter (with that being a configurable toggle, not hardcoded).
Scope: this is a presentation control, not a new access boundary
Worth being explicit about this up front so it isn't mistaken for a claimed security fix. Today, LDAP search access is two-tier, not "everything or nothing": without the search_full_directory permission, a bound identity still sees themselves (their own user record and group memberships); with it, they see the full directory. This proposal doesn't change that tier system, and it doesn't attempt to close any existing access. It reshapes what a directory-trusted identity sees through this specific LDAP provider — the motivating case from the issue is a legitimately-connected app (search-permitted, by design) that shouldn't incidentally see every user in the instance through its LDAP-backed picker, not an app that's improperly getting access it shouldn't have.
One assumption worth flagging explicitly for maintainers rather than quietly relying on: search_full_directory (the LDAP-provider-specific permission the outpost checks) and authentik_core.view_user (the generic permission that gates bulk listing via the REST API for every other consumer, via ObjectFilter) are two distinct Django permissions in two different apps — nothing we've found confirms they're granted together by convention or tooling. If they usually are bundled in practice, this feature is purely organizational: the identity could get equivalent data through the general API already, and the group filter is just shaping the LDAP-specific view for convenience/hygiene. If they can diverge — an identity granted search_full_directory on this provider without broader view_user access — then this LDAP provider may be the only place that identity can bulk-retrieve directory data at all, which makes the filter closer to a real boundary for that identity, not just cosmetic. Either way the proposed design is the same; it's worth surfacing this as an open question for maintainers rather than asserting one framing or the other with more confidence than we actually have.
Related and worth stating plainly: this feature doesn't defend against a search_full_directory-permitted identity pulling the same unfiltered data through some other sanctioned path (the REST API, the admin UI) if they separately hold the permissions for that. That's expected and out of scope — this is about shaping the LDAP directory's presentation, not adding a new confidentiality guarantee on top of authentik's existing permission model.
Detailed design
What's being added
Two new fields on LDAPProvider: an include-groups list and an exclude-groups list, plus a boolean for whether the bound identity is always exempt from the filter (defaults on). Empty lists = today's unrestricted behavior, so this is purely additive/opt-in. Resolution order: start from all users → narrow to include-group members if any are set → drop exclude-group members → re-add the bound user if the self-exemption is on.
Where the config lives and how the outpost gets it
LDAPOutpostConfigViewSet (authentik/providers/ldap/api.py, L128–171) already exists specifically to hand outposts their provider config — it's a separate, curated ViewSet from the generic admin-facing LDAPProviderViewSet (L76–83), serving a narrower field set (base_dn, bind_mode, search_mode, etc.) via LDAPOutpostConfigSerializer (L86–126). The new group-filter fields just get added to that existing payload. Nothing new to build here beyond two fields.
The same file also has check_access (L151–171), the action the outpost calls to get a permission verdict for a bound identity — note it returns a boolean, not data:
How enforcement actually happens — grounded in the real outpost code
This is the part that changed our thinking the most. We pulled the actual Go source (internal/outpost/ldap/) and found the self-vs-full-directory split isn't enforced by Core filtering query results — it's enforced by the outpost itself, using a flag cached locally at bind time:
// internal/outpost/ldap/instance.go#L18-L44 — ProviderInstance struct// L39: boundUsers map[string]*flags.UserFlags// internal/outpost/ldap/instance.go#L74-L93 — GetFlags(dn) / SetFlags(dn, flag)// internal/outpost/ldap/flags/flags.go#L15-L21 — UserFlags{ UserInfo, UserPk, CanSearch, Session, SessionJWT }//// set once during bind, read on every subsequent search on that connection:// internal/outpost/ldap/search/direct/direct.go#L61flags:=ds.si.GetFlags(req.BindDN)
// L109 / L127 — branch on the cached flagif flags.CanSearch {
// L111 — generic list endpoint, everyone...CoreUsersList(uapisp.Context())...
} else {
// L130 — generic single-object endpoint, self only...CoreUsersRetrieve(uapisp.Context(), flags.UserPk)...
}
For groups, it's a hybrid — same generic CoreGroupsList endpoint either way, but narrowed server-side via an existing filter param when the bound identity can't do broad search, plus client-side redaction as a backstop (search/direct/direct.go#L150-L183):
Two things fall out of this that matter a lot for the design:
The blast-radius concern is already resolved by the existing pattern. The outpost calls the same generic CoreUsersList/CoreGroupsList/CoreUsersRetrieve endpoints the web UI and everything else uses. It never modifies their behavior — it just chooses different query parameters (or a different endpoint entirely) client-side, based on a flag it already has. So extending this for group scoping means adding query params, not touching shared endpoint logic.
This resolves a security tradeoff we'd flagged earlier. We'd worried that outpost-side filtering means the outpost's local cache (in Cached search mode) still holds the full unfiltered directory even if it's hidden from responses. But the self-only case above shows the existing mechanism doesn't work that way — CoreUsersRetrieve(pk) never fetches other users' data over the wire in the first place. If the group filter is applied the same way (as a query parameter on the actual API call, not a post-hoc response filter), the outpost's cache never contains excluded users' data at all, in either search mode. That's the stronger guarantee we wanted.
Proposed implementation, concretely
Add groups_by_pk-style filtering (already exists on the Users API's UsersFilter, authentik/core/api/users.py L502–565, with groups_by_pk itself at L538–541) to the query the outpost builds in DirectSearcher.Search (internal/outpost/ldap/search/direct/direct.go L38–253) — and the equivalent cached-mode sync path — when the provider has an include-group list configured. The generic list endpoint's own queryset is defined in UserViewSet.get_queryset (L627–650), which stays untouched by any of this.
Add an exclude-groups filter param to that same FilterSet if one doesn't already exist (small, additive Core change — a new FilterSet method, not a new endpoint).
Mirror the existing group-handling pattern: narrow via query param first, then apply the same style of client-side redaction as a backstop, consistent with how !flags.CanSearch is already handled today.
This new group-scope filter is an additional, intersecting constraint on top of CanSearch — it doesn't replace or interact with the existing self/full permission logic, it just further narrows whatever CanSearch already produced. Someone with full-directory search permission still only ever sees the configured scope.
Historical note worth including in the PR description: a provider-level "Search group" field existed before 2024.8 and was migrated to the more flexible search_full_directory RBAC permission. This proposal isn't reverting that — RBAC permission still governs how much a given identity can search; this feature governs what the directory contains in the first place, a different axis, applied uniformly regardless of who's asking.
Alternatives considered
Filter the shared /api/v3/core/users/ endpoint's queryset directly in Core. Pro: single choke point, one place to reason about. Discounted immediately once raised — it would change behavior for every consumer of that endpoint (admin web UI, any other API integration), not just the LDAP outpost. Unacceptable blast radius for what should be an opt-in, provider-scoped feature.
Build a brand-new, dedicated "filtered directory" endpoint, with an explicit toggle so the outpost only calls it once opted in. Pro: fully isolated from the shared endpoint, clean audit trail, safe rollback path. This was our leading idea for a while — but discounted after finding the outpost already does exactly this shape of thing using the existing generic endpoints plus query parameters and client-side branching (self-only case, group-redaction case). Building a new endpoint would duplicate a mechanism that already exists and is already proven in production, for no real benefit. Extending the existing filter-param approach is smaller, safer, and consistent with the codebase.
Include full expression/attribute-based dynamic filtering in v1 (item #3 from earlier discussion). Pro: more flexible than group membership alone. Discounted for the initial scope: expressions are Python and can't run inside the Go outpost, so this would either need Core-side per-request evaluation (perf cost, and reopens the "why not just extend the existing endpoints" question) or a fundamentally different mechanism from the group-based approach. Flagged as a future extension — likely feasible as a simple attribute-equality filter pushed to the outpost the same way group filtering is, since custom attributes are already present in every returned entry, but full arbitrary expressions are out of scope for now.
Build group-scoping as a first-class RBAC tier, extending the existing self/full mechanism itself — i.e. a new permission concept enforced in Core's generic ObjectFilter/UserViewSet/GroupViewSet layer, the same place authentik_core.view_user is enforced today (authentik/rbac/filters.py). Pro: would be the "proper," fully-unified way to extend the existing two-tier access model to three-plus tiers, consistent with how search_full_directory itself is enforced. Discounted for this proposal: ObjectFilter and the ViewSets it filters are generic, shared infrastructure used by every API consumer, not LDAP-specific — adding a new permission concept there means real new RBAC surface plus changes to shared endpoint enforcement, the same category of blast-radius risk already ruled out above for the shared-endpoint option. It's also the wrong framing for what's being asked: this feature is a presentation/administrative control on top of access an identity is already trusted with (see "Scope" above), not a new access-control tier, so it doesn't need to live in the access-control layer at all. The outpost-side, config-driven approach gets the same practical outcome without touching any of that: no new RBAC concept, no changes to ObjectFilter or the Users/Groups ViewSets, just two new fields on the provider and smarter query construction the outpost already does for itself.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Hi - I wanted to create a discussion thread for this issue: #14082
The short background is: the LDAP outpost returns all sorts of crufty service account stuff when running a directory search that would be better to keep out of the search results. The Authentik team have kindly agreed to put this on the wishlist, so the question is what are the requirements and how to do it.
I've been digging into the structure, and personally I think the easiest/most effective way is defining an allow / deny list of groups at the provider level, and then extending the config schema to expose that for the outpost, with the outpost actually implementing the filter. I think this mirrors the existing check_access config which determines if the outpost will let the user actually get the full directory. I do think we should have both an allow and a deny list (not sure if they need to be mutually exclusive) because some people want to return only certain groups and others will just want to filter out the service accounts. I also think we need a toggle as to whether we compel inclusion of the bound LDAP account in the results this - that account may itself be a form of a "service account" that people want to filter but things may break if it doesn't show up in the results.
personally, I saw this as an outpost-level configuration for now; when this becomes wildly popular (hahaha) we could go back and revisit filtering by user or group or whatever. I also think that at some point this could be extended into a more generic filtering interface, but that also seems like it is down the line.
to be clear, I think this needs to be positioned as an administrative / convenience feature - the underlying account bound to LDAP would still have the same directory search permissions, so if they could bypass the outpost and hit the core API directly they could probably still get access to the whole directory. I don't think it is worth trying to build a whole new filter in the core API, unless this is a broader need?
I used Claude to help me review and articulate this more "formally" below, so apologies in advance if "we" got this totally wrong. I also included a summary of some other implementation paths I considered.
Happy to take any feedback, and if the team agrees, happy to do some work on a preliminary implementation.
--Tom
Proposal: group-scoped LDAP search visibility
File/line references below point to
mainas of this writing (2026-08-10) and will drift as the repo moves — treat them as pointers to the right file/function, not permanent anchors.TL;DR
Give
LDAPProvideran include-groups / exclude-groups list. The outpost already has all the machinery to enforce something like this — it already does a near-identical self-vs-everyone split for search permissions today, entirely client-side in Go, using a flag cached at bind time. We'd extend that exact mechanism to also apply a group filter, reusing filter parameters the Users/Groups API already supports. No new Core endpoint, and nothing about the shared/api/v3/core/users/API that the rest of authentik uses would change at all.Also proposing: support both an include list and an exclude list (not just whitelist) since "hide the service-account groups" is a more common need than "curate every visible group by hand," and the bound user should always be able to see their own entry regardless of the filter (with that being a configurable toggle, not hardcoded).
Scope: this is a presentation control, not a new access boundary
Worth being explicit about this up front so it isn't mistaken for a claimed security fix. Today, LDAP search access is two-tier, not "everything or nothing": without the
search_full_directorypermission, a bound identity still sees themselves (their own user record and group memberships); with it, they see the full directory. This proposal doesn't change that tier system, and it doesn't attempt to close any existing access. It reshapes what a directory-trusted identity sees through this specific LDAP provider — the motivating case from the issue is a legitimately-connected app (search-permitted, by design) that shouldn't incidentally see every user in the instance through its LDAP-backed picker, not an app that's improperly getting access it shouldn't have.One assumption worth flagging explicitly for maintainers rather than quietly relying on:
search_full_directory(the LDAP-provider-specific permission the outpost checks) andauthentik_core.view_user(the generic permission that gates bulk listing via the REST API for every other consumer, viaObjectFilter) are two distinct Django permissions in two different apps — nothing we've found confirms they're granted together by convention or tooling. If they usually are bundled in practice, this feature is purely organizational: the identity could get equivalent data through the general API already, and the group filter is just shaping the LDAP-specific view for convenience/hygiene. If they can diverge — an identity grantedsearch_full_directoryon this provider without broaderview_useraccess — then this LDAP provider may be the only place that identity can bulk-retrieve directory data at all, which makes the filter closer to a real boundary for that identity, not just cosmetic. Either way the proposed design is the same; it's worth surfacing this as an open question for maintainers rather than asserting one framing or the other with more confidence than we actually have.Related and worth stating plainly: this feature doesn't defend against a
search_full_directory-permitted identity pulling the same unfiltered data through some other sanctioned path (the REST API, the admin UI) if they separately hold the permissions for that. That's expected and out of scope — this is about shaping the LDAP directory's presentation, not adding a new confidentiality guarantee on top of authentik's existing permission model.Detailed design
What's being added
Two new fields on
LDAPProvider: an include-groups list and an exclude-groups list, plus a boolean for whether the bound identity is always exempt from the filter (defaults on). Empty lists = today's unrestricted behavior, so this is purely additive/opt-in. Resolution order: start from all users → narrow to include-group members if any are set → drop exclude-group members → re-add the bound user if the self-exemption is on.Where the config lives and how the outpost gets it
LDAPOutpostConfigViewSet(authentik/providers/ldap/api.py, L128–171) already exists specifically to hand outposts their provider config — it's a separate, curated ViewSet from the generic admin-facingLDAPProviderViewSet(L76–83), serving a narrower field set (base_dn,bind_mode,search_mode, etc.) viaLDAPOutpostConfigSerializer(L86–126). The new group-filter fields just get added to that existing payload. Nothing new to build here beyond two fields.The same file also has
check_access(L151–171), the action the outpost calls to get a permission verdict for a bound identity — note it returns a boolean, not data:How enforcement actually happens — grounded in the real outpost code
This is the part that changed our thinking the most. We pulled the actual Go source (
internal/outpost/ldap/) and found the self-vs-full-directory split isn't enforced by Core filtering query results — it's enforced by the outpost itself, using a flag cached locally at bind time:instance.go#L18-L44 · instance.go#L74-L93 · flags.go#L15-L21 · search/direct/direct.go#L106-L148
For groups, it's a hybrid — same generic
CoreGroupsListendpoint either way, but narrowed server-side via an existing filter param when the bound identity can't do broad search, plus client-side redaction as a backstop (search/direct/direct.go#L150-L183):Two things fall out of this that matter a lot for the design:
CoreUsersList/CoreGroupsList/CoreUsersRetrieveendpoints the web UI and everything else uses. It never modifies their behavior — it just chooses different query parameters (or a different endpoint entirely) client-side, based on a flag it already has. So extending this for group scoping means adding query params, not touching shared endpoint logic.CoreUsersRetrieve(pk)never fetches other users' data over the wire in the first place. If the group filter is applied the same way (as a query parameter on the actual API call, not a post-hoc response filter), the outpost's cache never contains excluded users' data at all, in either search mode. That's the stronger guarantee we wanted.Proposed implementation, concretely
groups_by_pk-style filtering (already exists on the Users API'sUsersFilter,authentik/core/api/users.pyL502–565, withgroups_by_pkitself at L538–541) to the query the outpost builds inDirectSearcher.Search(internal/outpost/ldap/search/direct/direct.goL38–253) — and the equivalent cached-mode sync path — when the provider has an include-group list configured. The generic list endpoint's own queryset is defined inUserViewSet.get_queryset(L627–650), which stays untouched by any of this.!flags.CanSearchis already handled today.CanSearch— it doesn't replace or interact with the existing self/full permission logic, it just further narrows whateverCanSearchalready produced. Someone with full-directory search permission still only ever sees the configured scope.search_full_directoryRBAC permission. This proposal isn't reverting that — RBAC permission still governs how much a given identity can search; this feature governs what the directory contains in the first place, a different axis, applied uniformly regardless of who's asking.Alternatives considered
Filter the shared
/api/v3/core/users/endpoint's queryset directly in Core. Pro: single choke point, one place to reason about. Discounted immediately once raised — it would change behavior for every consumer of that endpoint (admin web UI, any other API integration), not just the LDAP outpost. Unacceptable blast radius for what should be an opt-in, provider-scoped feature.Build a brand-new, dedicated "filtered directory" endpoint, with an explicit toggle so the outpost only calls it once opted in. Pro: fully isolated from the shared endpoint, clean audit trail, safe rollback path. This was our leading idea for a while — but discounted after finding the outpost already does exactly this shape of thing using the existing generic endpoints plus query parameters and client-side branching (self-only case, group-redaction case). Building a new endpoint would duplicate a mechanism that already exists and is already proven in production, for no real benefit. Extending the existing filter-param approach is smaller, safer, and consistent with the codebase.
Include full expression/attribute-based dynamic filtering in v1 (item #3 from earlier discussion). Pro: more flexible than group membership alone. Discounted for the initial scope: expressions are Python and can't run inside the Go outpost, so this would either need Core-side per-request evaluation (perf cost, and reopens the "why not just extend the existing endpoints" question) or a fundamentally different mechanism from the group-based approach. Flagged as a future extension — likely feasible as a simple attribute-equality filter pushed to the outpost the same way group filtering is, since custom attributes are already present in every returned entry, but full arbitrary expressions are out of scope for now.
Build group-scoping as a first-class RBAC tier, extending the existing self/full mechanism itself — i.e. a new permission concept enforced in Core's generic
ObjectFilter/UserViewSet/GroupViewSetlayer, the same placeauthentik_core.view_useris enforced today (authentik/rbac/filters.py). Pro: would be the "proper," fully-unified way to extend the existing two-tier access model to three-plus tiers, consistent with howsearch_full_directoryitself is enforced. Discounted for this proposal:ObjectFilterand the ViewSets it filters are generic, shared infrastructure used by every API consumer, not LDAP-specific — adding a new permission concept there means real new RBAC surface plus changes to shared endpoint enforcement, the same category of blast-radius risk already ruled out above for the shared-endpoint option. It's also the wrong framing for what's being asked: this feature is a presentation/administrative control on top of access an identity is already trusted with (see "Scope" above), not a new access-control tier, so it doesn't need to live in the access-control layer at all. The outpost-side, config-driven approach gets the same practical outcome without touching any of that: no new RBAC concept, no changes toObjectFilteror the Users/Groups ViewSets, just two new fields on the provider and smarter query construction the outpost already does for itself.All reactions