Contents

OpenLDAP ACL set Parameter — Complete Reference

Scope: the set option in the <by> clause of access directives (slapd.conf access / cn=config olcAccess), plus the related ACI userClasses set / set-ref.

Reliability note: This document is derived directly from the OpenLDAP source code, not from the man pages. The official man page (doc/man/man5/slapd.access.5) contains exactly one sentence about it, and that sentence (still present verbatim in the 2.5, 2.6 and current development trees) is:

The statement set=<pattern> is undocumented yet.

So the man page itself admits it is undocumented. Everything below was verified against the parser and the evaluator in the source.

Sources used (verified line by line):

Repository : https://git.openldap.org/openldap/openldap.git
Local copy : /tmp/ol-src
Branches   : master   (2.7-dev, HEAD 62a0a6b, 2026-08-18)
             rel24    (OPENLDAP_REL_ENG_2_4)
             rel25    (OPENLDAP_REL_ENG_2_5)
             rel26    (OPENLDAP_REL_ENG_2_6, e21e777, 2026-08-20)

Key files:

servers/slapd/aclparse.c   option parsing        (acl_parse_who())
                           master  ~line 1137
                           2.5/2.6 line 1508
                           2.4     line 1587
servers/slapd/sets.c       expression evaluator  (slap_set_filter(),
                           master lines 550-839; logic identical in
                           2.4/2.5/2.6 — a diff across all four
                           versions shows only trivial memory-error
                           handling differences)
servers/slapd/acl.c        bridge + value chasing
                           acl_match_set()    master line 2407
                           acl_set_gather()   master line 2206
                           acl_set_gather2()  master line ~2377
                           acl_string_expand() master line 2562
                           by-set check in acl_mask_dn() master line 1583
                           ACL_BUF_SIZE 1024  (master line 40)
servers/slapd/aci.c        ACI userClasses set / set-ref
                           lines 663 / 668, default attr "template"
                           (SLAPD_ACI_SET_ATTR, line 51)
servers/slapd/slap.h       AclRegexMatches, MAXREMATCHES = 100

NOTE: there is NO file "libraries/libslapd/str2set.c" and NO
"slapo-set" module in any OpenLDAP version 2.4-2.7. Verified by
file listings of all four branches and by git history search
(git log -S 'str2set' over 656 commits of master, back to 2016:
zero results). Any document or blog post referring to such a
file or module is describing something that does not exist in
OpenLDAP.

1. Where set may appear

set is a form of the <by> (who) clause only. It is NOT a <what> form. The grammar string printed by the parser itself (aclparse.c, line 2242) is:

<access clause> ::= access to <what> by <who> ...
<what> ::= * | dn[.<dnstyle>]=<DN>] [filter=<filter>] [attrs=<attrspec>]

set is not in that list, and a set=... token in the to clause fails with expecting <what> got "set".

Syntax:

access to <what>
    by set[.<style>]=<setexpression> <access>

cn/config: same parser, so olcAccess behaves identically.

2. The option and its styles (aclparse.c, acl_parse_who)

set[.<setstyle>]=<pattern>

Verified behaviour from the parser:

Style on the option Meaning
(none), exact, baseObject, base default: pattern is evaluated verbatim
expand pattern text is first run through $-substitution (section 6), then evaluated
regex deprecated: silently converted to expand, with a config-log message: deprecated set style "regex" in <by> clause; use "expand" instead
anything else (one, sub, children, level{n}, ip, path, …) config error: inappropriate style "<style>" in by clause

Other verified parser rules:

3. What the clause means

by set=<expr> grants the access to the operation iff the set expression evaluates to a NON-EMPTY set for the entry currently being checked. The set itself is a set of strings (almost always DNs); only its emptiness matters — the results parameter of slap_set_filter() is passed as NULL from the ACL code (acl.c line 2463):

rc = ( slap_set_filter( acl_set_gather,
            (SetCookie *)&cookie, &set,
            &op->o_ndn,      /* "user"   */
            &e->e_nname,     /* "this"   */
            NULL ) > 0 );

Consequences:

4. The set expression language (COMPLETE)

Source: slap_set_filter() in servers/slapd/sets.c (lines 550-839). It is a small stack-based character parser. The following is the entire language — the parser has exactly these cases and nothing else:

<expr>  :=  <term> { ( '|' | '&' | '+' ) <term> }      left-associative
<term>  :=  '(' <expr> ')'
          |  this
          |  user
          |  '[' <literal> ']'
          |  <expr> '/' <attr> { '*' | '-' <number> | '-' '*' }

Whitespace (space, tab, LF, CR) is ignored anywhere. -> is accepted in place of / (the - case consumes a following > and falls through to the / case). The expression stack is 64 deep — deeper nesting is an overflow error.

4.1 Primary elements

Element Meaning (source)
user Set containing the single DN of the authorized user (op->o_ndn). Only at the start or after an operator — an error if it follows a set or /. Error (clause skipped) for anonymous operations.
this Set containing the single DN of the entry currently checked (e->e_nname). Same position rules.
[literal] A one-element set containing the literal text between the brackets. Everything is allowed except ] (no nesting, no escapes). An empty literal [] yields one empty string — it pollutes unions and kills intersections. Must start an expression or follow an operator.
(...) Grouping. Operators may appear before a parenthesized group: A & (B | C) works; note the operator sits on the left of the (.

this and user are matched case-sensitively with memcmp — This, USER etc. are NOT recognized and are instead treated as attribute names (and fail, because they don’t follow a /).

4.2 The chase: /attr (postfix, the core feature)

For every DN in the current set, take the values of attribute <attr> from the entry that DN points to, and replace the DN by those values. Details from acl_set_gather() / acl_set_gather2() (acl.c lines 2206-2405):

Chase chains compose: this/member/entryDN = chase member, then normalize the result.

4.2a Reduction order: where a chase binds (critical trap)

The parser is a stack machine (slap_set_filter): a /attr chase consumes only the immediately preceding term — the set on top of the stack when the attribute name is read. A pending binary operator sits under that term on the stack and is reduced only when a ) closes the group that opened above it, the next operator character is read, or input ends.

Consequence: a chase never sees the result of a pending operator. To chase the result of a concatenation — the classic case: building an ldap:/// URI from literal + user + literal — the group containing the concatenation MUST close before the chase:

BROKEN   (([A] + user + [B])/entryDN/member) & this
          at /entryDN the stack holds: ( [A]+user  +  [B] /
          -> the chase chases the literal [B] alone. If [B] is
             not a valid DN it contributes nothing; the pending
             '+' is reduced LATER at ')' and joins the prefix
             with the already-destroyed set.

WORKS    ([A] + user + [B])/entryDN/member & this
                   ^^ this ')' closes the group: the pending
                      '+' is reduced here, the fully
                      concatenated string exists as ONE set,
                      and only then does /entryDN chase it.

Two compounding subtleties (both bit a production deployment):

Diagnosis: enable ACL debug and read the intermediate ACL set[n]=<value> lines. They are printed at each reduction step, so for a + expression the concatenated string appears before the chase runs. In the production incident behind this note, the log printed the concatenated URI missing its trailing literal paren one line before ACL set: empty — which identified the trap in minutes, whereas re-deriving the parse in your head is exactly where it hides.

4.3 Binary operators

Left-associative, no precedence (evaluation order is textual):

Op Meaning (source: slap_set_join)
\| union; duplicates removed; left side order preserved
& intersection; result empty if either side empty
+ “add”: the set of all pairwise string concatenations of the two sets (concatenating with an empty string is skipped, i.e. yields the other side); duplicates removed

+ is the way to build DN strings from pieces before a chase or an intersection, e.g. [uid=] + user/uid + [,dc=example,dc=com] (reconstructs the user’s own DN). Note that is the user atom, not a literal [user] — the literal would be the four characters “user”, is not a valid DN, and would silently collapse the whole set to empty. And when the chase target is itself the result of a +, the reduction-order rule of section 4.2a applies: close the group before the chase.

5. Worked examples

All of these are directly supported by the grammar above.

E1 — Reverse group membership (transitive), no memberOf needed. Allow read to entries whose owning group (checked per entry) transitively contains the user:

access to dn.subtree="ou=people,dc=example,dc=com"
    by set="[cn=admins,ou=groups,dc=example,dc=com]/member* & user" read
    by * none

literal {admins DN}  --/member*-->  all members, incl. members of members
                                     &  {user DN}  ->  non-empty?

E2 — Forward membership with memberOf (requires memberOf values, e.g. from slapo-memberof or the dynlist memberOf feature):

access to *
    by set="user/memberOf & [cn=admins,ou=groups,dc=example,dc=com]" read
    by * none

E3 — User is a direct member of the checked group entry (the checked entry this is the group; its member values are chased):

access to dn.subtree="ou=groups,dc=example,dc=com"
    by set="this/member & user" write

E4 — Ancestor-based ACL: the user DN is under the target entry’s organizational context:

access to dn.subtree="dc=example,dc=com"
    by set="user/entryDN-1 & this" read

user/entryDN-1 = the user's parent DN; intersect with {this}.

E5 — Search-style selection via LDAP URI (the set runs an internal search; no filter: keyword exists — the URI is the filter):

access to *
    by set="[ldap:///ou=people,dc=example,dc=com??sub?(ou=staff)]/entryDN & user" read

E6 — Literal membership of a known fixed set of DNs:

by set="[uid=alice,ou=people,dc=example,dc=com] | [uid=bob,ou=people,dc=example,dc=com] & user" read

E7 — Operator before a parenthesized group (valid; the & binds to the following (...) group):

by set="user/memberOf & ([cn=admins,ou=groups,dc=example,dc=com] | [cn=operators,ou=groups,dc=example,dc=com])" read

E8 — Dynamic URI: a service account reads the members of the group it owns (production pattern; the URI is built from the bound DN at request time, so ONE rule covers every current and future service account — no per-account ACL lines):

# DIT: group entry carrying both links:
#   cn=svc,ou=service-groups,dc=example,dc=com
#     objectClass: groupOfNames
#     owner:  uid=svc-bind,ou=service-accounts,dc=example,dc=com
#     member: uid=alice,ou=users,dc=example,dc=com
access to dn.subtree="ou=users,dc=example,dc=com"
    by set="([ldap:///ou=service-groups,dc=example,dc=com??sub?(owner=] + user + [)])/entryDN/member & this" read
    by * break

Evaluation for bound user = the owner: the internal search
(owner=<bound DN>) finds the group -> /entryDN -> /member gives
its members -> & this is non-empty exactly for those entries.
The four-character segment [ ) ] ) is the literal filter-closing
paren followed by the group close; the group MUST close before
/entryDN (section 4.2a) or the chase chases the bare literal ")"
and the clause never matches. `by * break` (NOT `by * none`)
lets non-matching identities fall through to later rules. The
companion rule for the group subtree itself is
by set="user & (this/owner)" read by * break.
Adding a service later = one group entry (member+owner) plus one
account entry. Zero ACL edits.

6. The expand style: $ substitution

With set.expand=... (or the deprecated set.regex=...), the pattern text is first expanded by acl_string_expand() (acl.c line 2562):

Token Substitutes
$N, ${N} N-th DN submatch (0 = whole match); up to MAXREMATCHES = 100
${vN} N-th attribute-value submatch (only available if the <what> clause has a val.regex= pattern)
${dN} explicit DN match (same as $N)
$$ literal $
$ at end of pattern literal $

Where the submatches come from (acl.c, acl_mask_dn, lines 1590-1640):

Example:

access to dn.subtree="dc=example,dc=com"
    by set.expand="user & [$0]" read

$0 expands to the DN of each entry being checked; the clause then matches exactly for the user’s own entry (a self-like behaviour built from pieces).

Buffer limit: the expanded text is written into a 1024-byte buffer (ACL_BUF_SIZE, acl.c line 40). Longer expansions are silently truncated. If a submatch index does not exist (e.g. $1 with a dn.base what clause), the expansion fails and the clause is skipped.

7. ACI userClasses (experimental)

The same engine is also reachable from experimental ACIs (OpenLDAPaci, requires the dynacl/ACI build; servers/slapd/aci.c):

userClass set:      <setexpression>          (line 663, same semantics as by set=)
userClass set-ref:  <DN>[/<attr>]            (line 668)

For set-ref, the first value of <attr> on the entry <DN> is fetched (with ACL_NONE) and that value is used as the set expression. The default attribute when none is given is literally "template" (SLAPD_ACI_SET_ATTR, aci.c line 51) — a quirk of the experimental ACI implementation, not a general default.

8. Version history (verified per branch)

Version by set= Notes
2.4 works (parsed at aclparse.c:1587, same evaluator) completely absent from the man page
2.5 works man page adds set[.<setstyle>]=<pattern> to the synopsis with <setstyle>={exact|expand}, body still: “is undocumented yet”
2.6 works man page unchanged, still “undocumented yet”
2.7 (master, 2026) works code moved to servers/slapd/; behaviour identical

The set evaluator (servers/slapd/sets.c) is functionally identical across 2.4 → 2.7 (verified by diff; only NULL-handling additions).

9. What set= is NOT (common confusions)

None of the following exist in the ACL set expression — verified by reading the whole parser (sets.c) and the option handler (aclparse.c):

If you have seen a “set” syntax with list:/filter:/and()/or() /not(), it comes from a different mechanism or product, not from the OpenLDAP ACL set option.

10. Error and edge-case behaviour (all from source)

Situation Behaviour
Invalid expression syntax (config time) accepted — no validation
Invalid expression syntax (runtime) clause silently skipped (no match)
Unknown attribute in /attr runtime syntax error → clause skipped
Anonymous operation + user in expression error → clause skipped
Chased DN does not exist / is not a valid DN that value contributes nothing
Missing submatch in expand ($1 with base what) expansion error → clause skipped
Expanded pattern > 1024 bytes silent truncation
Nesting deeper than 64 overflow error → clause skipped
set= repeated in one by clause config error: set attribute already specified
set= with empty pattern config error: no set is defined
set.regex= works as expand + deprecation log message
empty literal [] one-element set containing the empty string
/attr while an operator is pending below its operand (A + B/attr) no error — the chase binds to B alone; if B is a non-DN (e.g. the literal ")") the result is silently empty (section 4.2a)

Debugging aid: every evaluation is logged with slapd -d acl (LDAP_DEBUG_ACL): acl.c prints <= check a_set_pat: <pattern> and sets.c prints either ACL set: empty or ACL set[n]=<value> for the result. The intermediate values are printed at EACH reduction step — for + expressions the concatenated string appears in the log BEFORE the chase runs. When a chase comes back empty, read that line: if a piece is missing from the string (e.g. a literal paren), you have the reduction-order trap (section 4.2a), not a schema or data problem — and the clause text in check a_set_pat confirms what the parser actually stored.

slapacl cannot test by set= clauses — servers/slapd/slapacl.c contains no set handling at all; it silently ignores them. Validate set ACLs against a running slapd with ACL debug logging, or a dedicated test database.

11. Security & performance notes

  1. Chasing bypasses ACLs. backend_attribute(..., ACL_NONE) reads the chased attribute of any entry without access checks. An ACL clause that chases description of arbitrary entries lets the very evaluation of that clause disclose those values in debug logs, and means your by set= grant implicitly carries read access to whatever you chase. Choose chased attributes deliberately (member, entryDN are the safe, intended ones).
  2. Cost. Each chase is a backend lookup per DN in the set, per entry checked. In searches, by set= is evaluated per result entry. /attr* (closure) and ldap:/// URI expansion (internal searches) can be very expensive on large DITs. Do not put expression-heavy set clauses on hot access to * lines.
  3. Exact string matching. Intersections compare strings byte-for- byte. Normalize both sides with /entryDN when comparing DNs from different sources. DN value case is preserved by normalization (only attribute names are lowercased), so cn=Admins and cn=admins do not match.
  4. this depends on the entry being checked — in a search, results are filtered per entry, so one clause can match some entries of a result and not others.

12. Quick reference card

by set[.exact|base|expand]=<expr>      regex= is a deprecated alias for expand

<expr>  := <term> { ('|' | '&' | '+') <term> }
<term>  := '(' <expr> ')' | this | user | '[' literal ']'
          | <expr> '/' <attr> ['*' | '-' <n> | '-' '*']

  this        DN of the entry being checked
  user        DN of the authorized (proxy-aware) user
  [x]         literal string set
  /attr       chase attribute from each DN (ACLs bypassed!)
  /attr*      transitive chase (nested groups)
  /attr-N     N-th ancestor of each DN        (N=0: unchanged)
  /attr-*     DN + all its ancestors
  ldap:///... any set value that is a local LDAP URI runs an
              internal search (no host/exts allowed)
  & | +       intersection / union / pairwise concatenation
  $0 $N ${vN} ${dN} $$   expand-style substitution (1024-byte buffer)

Source-verified against OpenLDAP 2.4-2.7 (sets.c, acl.c, aclparse.c)
Maintainer Marc-Robin Wendt (marc-robin.wendt@tu-berlin.de)
this page last updated: 31-Aug-2026