Replies: 1 comment
|
It is bevelling to me that the current LDAP provider works in this way in the first place. Great to hear that I am not the only one who took notice and is trying to find a solution.
For me this would imply heavily that of course Application User and Group binds apply, but apparently they don't. Hope this finds the right ears. Regards Edit: typos |
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