Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

CrateRole
ldif-parserParse LDIF schema files into LdapEntry structures
win-sdParse SDDL, evaluate access checks, hierarchical object type list
ldap-acisBuild ACIs, evaluate LDAP authorization, generate 389DS ACI text
acls-rsPermissionSet 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 codeMask bitMeaning
RP0x0010Read property
WP0x0020Write property
CC0x0001Create child
DC0x0002Delete child
LC0x0004List children
LO0x0080List object
CR0x0100Control 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 conceptLDAP ACI concept
SID alias (DA, SY, AU)Bind rule (groupdn, userdn, userdn = "ldap:///all")
Allow/Deny ACE typeallow/deny
READ_PROP, LIST_CHILDRENread, search
WRITE_PROPwrite (modify)
CREATE_CHILDadd
DELETE_CHILD, DELETEdelete

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::GroupDn via memberof on 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.