AD Schema Access Control
Active Directory stores its schema — class and attribute definitions — as
LDIF records. Each class carries a defaultSecurityDescriptor in SDDL
that controls access to instances of that class. Attributes can belong to
property sets via attributeSecurityGUID, which are referenced by Object
ACEs for property-level access control.
The ad_schema_access example parses the real Windows Server v1903 AD DS
schema, evaluates access using win-sd, translates the SDDL ACEs into
LDAP ACIs using ldap-acis, and compares the two models through the
shared acls-rs PermissionSet bridge.
Run the example:
cargo run -p bac-examples --example ad_schema_access
Crates used
| Crate | Role |
|---|---|
ldif-parser | Parse LDIF schema files into LdapEntry structures |
win-sd | Parse SDDL, evaluate access checks, hierarchical object type list |
ldap-acis | Build ACIs, evaluate LDAP authorization, generate 389DS ACI text |
acls-rs | PermissionSet bridge between the two access control models, with PermissionMapping for AD-semantic bit names |
Schema data
The example ships with the official Microsoft AD DS schema for Windows Server v1903 (4 LDIF files, ~1.5 MB). The schema contains:
- 269 classes (239 structural) — 264 with
defaultSecurityDescriptor - 1499 attributes — 206 grouped into 17 property sets
What the example does
1. Parse LDIF
Uses LdifParser::parse() from the ldif-parser crate to load the schema
files. Binary values (GUIDs) encoded as base64 in the LDIF are decoded
into win_sd::Guid using the MS GUID format (little-endian first three
fields).
2. Parse SDDL security descriptors
Each class’s defaultSecurityDescriptor is parsed with
win_sd::parse_with_domain() using a DomainContext so domain-relative
aliases (DA, EA, CA, etc.) resolve correctly.
Of 264 classes with a defaultSecurityDescriptor attribute:
- 263 parse successfully
- 1 (
domainDNS) has an empty value — see Inheritance below
An empty defaultSecurityDescriptor means the class does not define its
own default SD. Instances inherit their security descriptor entirely from
the parent container in the directory tree. For domainDNS this makes
sense: it is the top-level domain object (dc=example,dc=com) whose SD
is set by the domain provisioning process, not by a schema template.
2b. Inheritance demonstration
The example simulates what happens when a domainDNS instance is created
under a parent container whose ACEs carry CONTAINER_INHERIT|OBJECT_INHERIT
flags. It calls create_child_sd() and verifies the inherited ACEs:
Simulated parent (domain root with CI|OI) → 3 ACEs inherited:
(allow;;;DA) [full rights] inherited=true
(allow;;;SY) [full rights] inherited=true
(allow;;;AU) [READ_PROP|...] inherited=true
Access check on inherited SD:
Domain Admins READ_PROP|WRITE_PROP → GRANTED
Auth Users READ_PROP|WRITE_PROP → DENIED
Auth Users READ_PROP only → GRANTED
Note: AD schema defaultSecurityDescriptor values typically do not include
inheritance flags (CI/OI) because they are templates for new objects.
Inheritance in a live directory comes from the parent container’s actual SD,
which administrators or group policy augment with inheritable ACEs.
3. Access matrix
For key classes (user, computer, organizationalUnit, group), the
example evaluates access for common AD principals and reports which
AD-specific rights each principal holds:
| Right code | Mask bit | Meaning |
|---|---|---|
RP | 0x0010 | Read property |
WP | 0x0020 | Write property |
CC | 0x0001 | Create child |
DC | 0x0002 | Delete child |
LC | 0x0004 | List children |
LO | 0x0080 | List object |
CR | 0x0100 | Control access (extended right) |
Domain Admins and Local System typically get full rights; Authenticated Users get read-only access.
4. Property set mapping
Attributes with an attributeSecurityGUID belong to a property set. The
example builds a map from property set GUID to the list of attributes it
protects. For example:
- Personal Information (
77b5b886-...):streetAddress,homePostalAddress,telephoneNumber, and 60+ more - Account Restrictions (
4c164200-...):accountExpires,userAccountControl,pwdLastSet - Group Membership (
bc0ac240-...):member,memberOf
Object ACEs with a property set GUID in the object_type field grant or
deny access to all attributes in that set.
5. Hierarchical access check
For the user class, the example builds an ObjectTypeEntry list with the
class GUID at level 0 and property set GUIDs at level 1, then runs
check_access_by_type_list() — the MS-DTYP AccessCheckByTypeResultList
algorithm. Each property set gets its own grant/deny result.
6. SDDL → LDAP ACI translation
Each DACL ACE from the organizationalUnit class is translated into an
equivalent ldap_acis::Aci using AciBuilder. The translation maps:
| SDDL concept | LDAP ACI concept |
|---|---|
| SID alias (DA, SY, AU) | Bind rule (groupdn, userdn, userdn = "ldap:///all") |
| Allow/Deny ACE type | allow/deny |
READ_PROP, LIST_CHILDREN | read, search |
WRITE_PROP | write (modify) |
CREATE_CHILD | add |
DELETE_CHILD, DELETE | delete |
Each original SDDL ACE is shown alongside its translated 389 Directory Server ACI:
SDDL ACE: (A;;0x000F01FF;;;DA) [READ_PROP|WRITE_PROP|...|WRITE_OWNER]
LDAP ACI: (target = "ldap:///dc=example,dc=com")
(version 3.0;acl "ou-ace-1";
allow (read,search,write,add,delete)
groupdn = "ldap:///cn=Domain Admins,cn=Users,dc=example,dc=com";)
7. Cross-model comparison
The cross-model comparison begins by showing the DA-specific ACE from the
OU’s defaultSecurityDescriptor and its translated LDAP ACI side by side,
then evaluates the same access request through both models.
Two principals are tested:
- Authenticated User — read-only access (matches
BindRule::Authenticated) - Domain Admin — full access (matches
BindRule::GroupDnviamemberofon the bind entry)
As Authenticated User (jdoe):
Read win-sd=GRANTED ldap-acis=GRANTED
Write win-sd=DENIED ldap-acis=DENIED
As Domain Admin (admin, member of DA group):
Read win-sd=GRANTED ldap-acis=GRANTED
Write win-sd=GRANTED ldap-acis=GRANTED
Add win-sd=GRANTED ldap-acis=GRANTED
Delete win-sd=GRANTED ldap-acis=GRANTED
PermissionSet bridge
The bridge shows the same “Domain Admin has full access” expressed in each
model’s vocabulary. The win-sd side uses ad_mapping() to produce
AD-semantic permission names; the ldap-acis side authorizes each LDAP
operation type separately and collects the granted operations:
PermissionSet bridge (Domain Admin):
win-sd (ad): {control_access, create_child, delete_child,
list_children, list_object, read_prop, write_prop}
ldap-acis: {read, search, modify, add, delete}
The two vocabularies are not 1:1 — AD has finer-grained rights
(list_children vs list_object, control_access) while LDAP ACI
operations (read, search, modify) are coarser. The
PermissionMapping mechanism (see acls-rs Bridge) lets each
model define its own namespace so both can coexist in a single
PermissionSet without name collisions.
Limitations
The translation is inherently lossy in both directions:
- Object ACEs with GUIDs are not translated to LDAP ACIs (LDAP ACIs have no equivalent of property-set-scoped access).
- Standard rights (
READ_CONTROL,WRITE_DAC,WRITE_OWNER) have no direct LDAP ACI equivalent. - Inheritance flags (CI, OI, NP, IO) are collapsed into LDAP scope
(
subtree). - Deny ACEs translate directly, but LDAP ACI deny evaluation semantics differ from Windows SD (LDAP uses last-match with priority; Windows uses first-match with deny-first ordering).
These gaps demonstrate why interoperability between AD and LDAP directory servers is a hard problem — the same data can be modeled, but the access control semantics diverge at the edges.