Guide 04

Scenario YAML

Users, groups, ACLs, tokens, and lab policy are declared in a labldap.dev/v1alpha1 LabScenario file. Bootstrap compiles that file and writes real 389 DS entries. The YAML is the lab definition — not an in-memory directory.

Users and groups

apiVersion: labldap.dev/v1alpha1
kind: LabScenario
metadata:
  name: compose-lab
spec:
  directory:
    suffix: "dc=example,dc=test"
    peopleRDN: "ou=people"
    groupsRDN: "ou=groups"

  users:
    - id: alice
      uid: alice
      passwordFile: /run/secrets/user-alice
      enabled: true
      attributes:
        givenName: Alice
        sn: Anderson
        mail: alice@example.test

  groups:
    - id: staff
      members:
        - user: alice

What it becomes

YAMLDirectory
users[].id / uiduid=,ou=people,
passwordFileuserPassword (hashed by 389 DS)
enabled: falsensAccountLock
attributesextra string attrs (givenName, sn, mail)
groups[].idcn=,ou=groups, (groupOfNames)
members.usermember: uid=…

The shipped Compose lab is deploy/compose/scenario.yaml. Schema: config/schema/v1alpha1.json.

Rules

  • No inline passwords. Point at a passwordFile. make compose-up writes those files under secrets/.
  • Groups cannot be empty. 389 groupOfNames needs a member. Nested groups stay off unless nestedGroups: true.
  • YAML is the compiled baseline, not a live sync loop. UI / REST / MCP can add users later. Soft reset restores this file.
  • Changing the file means re-bootstrap. The running control plane does not remount a new scenario on the fly.
  • allowRawACI: false. Declare acls: in YAML. Do not paste 389 ACI text.

ACLs

  acls:
    - id: staff-read
      principal: { kind: group, ref: staff }
      target: { kind: suffix }
      permissions: [read, search, compare]
      attributes:
        allow: ["*"]
        deny: [userPassword]

That compiles to 389 ACIs. Seed user alice can search the suffix, bind as herself, and cannot read userPassword.

Names in attributes.allow and attributes.deny must be attribute types the pinned 389 image defines; anything else fails validation with unknown_attribute.

Each list compiles to one 389 targetattr list: allow: [uid, sn] gives targetattr="uid || sn", a deny list with no allow list (or allow: ["*"]) gives targetattr!="…", and both lists give the allow names no deny name covers. A deny name with options narrower than a same-attribute allow name, a combination leaving no attribute, and deny: ["*"] fail with invalid_attribute_filter; each list holds at most 64 names.

Apply

Edit deploy/compose/scenario.yaml (or set LABLDAP_SCENARIO_FILE), then make compose-up. Bind as a seeded user, not Directory Manager:

ldapsearch -H ldap://127.0.0.1:3389 -ZZ \
  -x -D 'uid=alice,ou=people,dc=example,dc=test' \
  -y secrets/user-alice \
  -b 'dc=example,dc=test' '(uid=alice)'

The shipped suffix is dc=example,dc=test.

Baseline vs runtime

PathWhat happens
Scenario YAMLCompiled at bootstrap. Soft reset target.
UI / REST / MCPLive mutations on 389 DS until reset or volume wipe.
make compose-resetDestroy the volume, apply the YAML again.

Full version: docs/guides/scenario.md.