baton-ldap is a connector for LDAP built using the Baton SDK. It communicates with the LDAP protocol to sync data about roles, users, and groups.
Check out Baton to learn more about the project in general.
To access the LDAP server, you must provide the username and password you use to login to the LDAP server.
Also see Set up an LDAP connector in the ConductorOne documentation for instructions including using LDAP from ConductorOne.
The latest release is available from the baton-ldap Github releases page.
Pre-built container images compatible with Docker and other container runtimes are published to GHCR:
docker pull public.ecr.aws/conductorone/baton-ldap:latest
Additionally for testing on workstations, baton-ldap can be installed from Homebrew:
brew install conductorone/baton/baton conductorone/baton/baton-ldap
| CLI Flag | Environment Variable | Explaination |
|---|---|---|
--bind-dn |
BATON_BIND_DN |
required Username to bind to the LDAP server with, for example: cn=baton-service-account,ou=users,dc=baton,dc=example,dc=com |
--password |
BATON_PASSWORD |
optional Password to bind to the LDAP server with. If unset, an unathenticated bind is attempted. |
--url |
BATON_URL |
required URL to the LDAP server. Can be either ldap: or ldaps: schemes, sets the hostname, and optionally a port number. For example: ldaps://ldap.example.com:636 |
--base-dn |
BATON_BASE_DN |
optional Base Distinguished name to search for LDAP objects in, for example DC=example,DC=com |
--user-search-dn |
BATON_USER_SEARCH_DN |
optional Distinguished name to search for User objects in. If unset the Base DN is used. |
--group-search-dn |
BATON_GROUP_SEARCH_DN |
optional Distinguished name to search for User objects in. If unset the Base DN is used. |
--disable-user-attributes |
BATON_DISABLE_USER_ATTRIBUTES |
optional Map of LDAP attribute name to the value that marks an account as disabled, for example --disable-user-attributes revoke=Y. Unset by default. See User enable/disable attributes. |
--enable-user-attributes |
BATON_ENABLE_USER_ATTRIBUTES |
optional Map of LDAP attribute name to the value that marks an account as enabled, for example --enable-user-attributes revoke=N. Unset by default. When both directions are configured they must name the same attributes. |
--provisioning |
BATON_PROVISIONING |
optional Enable Provisioning of Groups by baton-ldap. true or false. Defaults to false |
Use baton-ldap --help to see all configuration flags and environment variables.
Some directories mark an account's lifecycle state with their own attribute rather than the
standard userAccountControl (Active Directory) or nsAccountLock (FreeIPA) flags -- for example
IDMWorks uses revoke, with Y meaning disabled and N meaning enabled. Two optional maps
configure that definition:
disable-user-attributes:
revoke: "Y"
enable-user-attributes:
revoke: "N"- Sync reads both maps: an account is reported disabled when any configured attribute holds its
disabled value, enabled when any holds its enabled value, and otherwise falls through to
userAccountControlandnsAccountLock. Attribute values are compared case-insensitively and whitespace-trimmed, and a multi-valued attribute counts if any of its values matches. disable_userwrites exactly the attributes named indisable-user-attributes;enable_userwrites exactly those inenable-user-attributes. Neither action touches an attribute the other direction names.- Because of that isolation, when both maps are configured they must name the same attributes --
otherwise
enable_userwould leave an attribute at its disabled value and the synced status would disagree with the action's result forever. A configuration that names different attributes in each direction is rejected at startup. "Clear on enable" is written as an explicit empty value (enable-user-attributes: {"revoke": ""}), which keeps the attribute name present. - The same attribute cannot carry the same value in both directions, attribute names must not be
empty, neither
objectClassnor any password attribute can be used as the marker, and the disable side cannot use an empty value; each is rejected at startup. The disable rule is not cosmetic: an absent attribute reads as enabled, so "disable by clearing the marker" would clear the attribute, report success, and leave sync reporting the account enabled forever. An empty value on the enable side stays legal -- that is clear-on-enable. - Both maps are unset by default: a deployment that does not configure them behaves exactly as before. When only one direction is configured, only that action is registered on the connector.
- Attribute names are LDAP attribute names and are case-insensitive; names read from a config file are lowercased by the configuration library, which does not change the attribute written.
- Quote the values. The configuration loader stringifies map values before the connector sees
them, so an unquoted
TRUEorFALSE(revoke: TRUE) arrives as the string"true"and is written that way; LDAP's Boolean syntax (RFC 4517) requires uppercase, so the modify is rejected with an invalid-syntax error instead of setting the attribute. The connector cannot tell a quoted"true"from an unquoted one by then, so this is not caught at startup -- quote every value.YandNare not YAML booleans and are safe unquoted. - As an environment variable, the value must be JSON. A nested YAML map and repeated
--disable-user-attributes key=valueflags both work, but an environment variable arrives as a string:BATON_DISABLE_USER_ATTRIBUTES='{"revoke":"Y"}'is accepted, whileBATON_DISABLE_USER_ATTRIBUTES='revoke=Y'is rejected at startup instead of silently configuring nothing.
To provision an account from the command line, you'll need to provide the login, email, and account profile. For example:
.\baton-ldap.exe --base-dn "DC=baton-dev,DC=d2,DC=ductone,DC=com" --password "password" -p --create-account-login 'example-user' --create-account-profile "{\"rdnKey\":\"uid\",\"path\":\"cn=staged users,cn=accounts,cn=provisioning\",\"suffix\":\"dc=example,dc=test\",\"objectClass\":[\"top\",\"person\",\"organizationalperson\",\"posixAccount\"],\"additionalAttributes\":{\"cn\":\"Example User\",\"sn\":\"User\",\"homeDirectory\":\"\",\"uidNumber\":\"-1\",\"gidNumber\":\"-1\"}}"'
Creates an LDAP organizational unit (organizationalUnit) under a parent container.
| Argument | Required | Description |
|---|---|---|
name |
yes | The OU name. Used as the ou attribute and the RDN (ou=<name>). |
parent_dn |
no | The container DN to create the OU under. Defaults to the configured base-dn. |
description |
no | Sets the description attribute on the OU. |
Returns ou_dn (the created OU's DN) and success.
Notes:
base-dnmust be configured; the parent DN must be at or under it, or the action is rejected (fail-closed).- The action is idempotent: creating an OU that already exists succeeds.
- The bind account must have permission to create entries at the target location.
Sets core profile fields (first name, last name, display name, email) and/or arbitrary custom LDAP attributes on an existing user.
| Argument | Required | Description |
|---|---|---|
user_id |
yes | Account resource ID reference to the user to update. From a C1 automation this is the C1 account identifier, not the LDAP DN -- see the notes below. |
first_name |
no | The user's first (given) name, mapped to givenName. Ignored if empty. |
last_name |
no | The user's last (surname) name, mapped to sn. Ignored if empty. |
display_name |
no | The user's display name, mapped to displayName. Ignored if empty. |
email |
no | The user's email address, mapped to mail. Ignored if empty. |
custom_attributes |
no | Map of arbitrary raw LDAP attribute name → value, for attributes beyond the named fields above. Keys are used verbatim as attribute names. An empty value clears the attribute. |
Returns success, updated_user (the modified user resource, re-fetched after the
write; absent if the read-back failed, though the write itself still succeeded),
applied (the number of attributes modified), and skipped (named fields or
custom_attributes entries that were not written).
updated_user carries the resource identity, displayName, and the user trait -- not
the entry's full attribute set. A value the action just wrote appears there only when it
also feeds one of those: display_name through displayName, email through the
trait's email list, and a custom_attributes key only when it maps to a trait field
(mail, displayName, sAMAccountName, userPrincipalName, a non-RDN uid or cn,
lastLogonTimestamp, authTimestamp). first_name, last_name, and every other
custom_attributes key reach the directory but do not appear in updated_user. Use
applied to confirm those.
Notes:
- Scope: this action is resource-scoped to
user. Resource-scoping is what makesupdate_profilediscoverable and usable from ConductorOne's attribute-push-rule feature, since that feature offers actions per resource type rather than the connector's global action list. - Named-field semantics:
first_name,last_name,display_name, andemailare only applied when present and non-empty -- they cannot be used to clear an attribute. A present-but-empty named field is dropped from the write and reported inskippedrather than silently vanishing. inetOrgPersonrequirement: of the four named fields, onlylast_name(sn) is universal -- it's a MUST attribute of the basepersonobject class.first_name(givenName, defined in RFC 4519),display_name(displayName, RFC 2798), andemail(mail, RFC 4524) are permitted on an entry only by RFC 2798'sinetOrgPersonobject class; writing one of them to an entry that doesn't carryinetOrgPersonfails loudly with LDAP result code 65 ("Object Class Violation"). This is safe -- the failure is atomic, with no partial write and no data corruption -- but it means those three fields only work againstinetOrgPersonentries.custom_attributessemantics: an entry is written whenever the key is present, including with an empty value, which clears the attribute.custom_attributeskeys are raw, and only the named fields are translated. The four named arguments above are the only names mapped to a different LDAP attribute (first_name→givenName, and so on). Acustom_attributeskey is used verbatim as the attribute name, whatever it looks like:{"user_id": "x"}writes an attribute literally nameduser_id-- it does not writeuid. A name your directory does not define is refused by the server, and the result code depends on the implementation: OpenLDAP returns 17 ("Undefined Attribute Type"), ApacheDS returns 16 ("No Such Attribute"). Likewiseloginandpath, which are baton profile field names rather than LDAP attributes, are attempted as literal attribute names rather than skipped. Only the safety checks below still apply tocustom_attributesentries; none of them changes the attribute you named.- Collisions: a
custom_attributeskey is dropped -- never merged with, or silently overwriting, a named field's slot -- and reported once inskippedwhen it case-insensitively matches either one of the four named argument names (first_name,last_name,display_name,email), or the LDAP attribute a supplied named field is writing (givenName,sn,displayName,mail). The second case only applies when that named field was actually supplied and non-empty; otherwise{"givenName": "Jane"}is an ordinary raw write. - The following are not modifiable and are rejected or skipped: password attributes
(
userPassword/ anything containingpassword-- use credential rotation instead),objectClass(both rejected), and the user's RDN attribute (skipped -- renaming requires a ModifyDN). - Multi-valued attributes: setting (not clearing) a value on an attribute that currently holds more than one value now returns an error instead of silently discarding the extra values. Clearing (an empty value) a multi-valued attribute is unaffected and still removes all values -- that remains an explicit, intentional "remove all values" operation.
- Value types:
custom_attributescarries one string per attribute, so binary attributes (jpegPhoto,userCertificate;binary) and option-tagged attributes (;lang-xx) cannot be set through this action. - Only entries within the configured
user-search-dn(orbase-dn) may be modified; out-of-scope or non-user DNs are rejected as "not found" (fail-closed). - From a C1 automation,
user_idtakes the C1 account identifier, not the LDAP DN. C1 resolves the account to the connector's resource before dispatching the action. A DN fails inside C1 withresource <dn> with type user was not foundand never reaches the connector, so it produces no connector log line and leaves the directory untouched. - An attribute push rule must map from a single-valued attribute. The connector's user profile carries only attributes that hold exactly one value on the entry, so a multi-valued source resolves to nothing: the rule saves and enables, and each push reports zero attributes applied.
- Actions are not gated by
--provisioning/BATON_PROVISIONING. That flag gates the provisioning surface -- grant, revoke, account create/delete, credential rotation -- and the SDK registers the action service outside it, soupdate_profileruns and writes with the flag unset. What the action does require is a bind account with permission to modify the target entry.
Marks a user account as enabled by writing the attributes configured in
--enable-user-attributes.
| Argument | Required | Description |
|---|---|---|
user_id |
yes | Account resource ID reference to the user to enable. From a C1 automation this is the C1 account identifier, not the LDAP DN -- see the notes under update_profile. |
Returns success, status ("enabled"), applied (the number of attributes modified; 0 means
the account was already enabled), and updated_user (the user resource re-fetched after the write;
absent if the resource could not be encoded, though the write itself still succeeded).
Notes:
- Only the attributes named in
--enable-user-attributesare written. An attribute that only--disable-user-attributesnames is left exactly as it is; the action never clears an attribute it was not configured to write. - Clearing on enable. Configure an attribute with an empty value
(
enable-user-attributes: {"revoke": ""}) to remove the disabled marker rather than write a value. The account then reads as enabled by fall-through: no configured attribute matches its disabled value, so the connector uses its built-in rules, which default an unspecified status to enabled. "Enabled" here means "the disabled marker is not present", not "a positive value was written". - Idempotent: an account already in the requested state succeeds with
applied: 0. That check runs before the modify, so it also covers a marker attribute that is multi-valued -- an entry whose attribute already holds the requested value among several is reported as already in state rather than failing, which is what the synced status says about it too. (Modifying such an attribute to a value it does not already hold is still refused: replacing it with a single value would silently discard the others.) - If a configured attribute cannot be written (it is the entry's RDN attribute, for example) the
action fails with
FailedPreconditionnaming the attribute, on the first call and on every retry alike. That is an error rather than returned data, which is why there is noskippedfield: it could never hold anything on a successful call. - After writing, the connector re-reads the entry and verifies that the targeted attributes now hold their configured values (case-insensitive, whitespace-trimmed, any value of a multi-valued attribute) or are absent when they were cleared. A mismatch fails the action instead of reporting a success the directory does not reflect.
- The action is registered only when
--enable-user-attributesis set, so C1 never offers a lifecycle action this deployment cannot carry out. - Not gated by
--provisioning. The bind account needs permission to modify the target entry.
Marks a user account as disabled by writing the attributes configured in
--disable-user-attributes.
| Argument | Required | Description |
|---|---|---|
user_id |
yes | Account resource ID reference to the user to disable. From a C1 automation this is the C1 account identifier, not the LDAP DN -- see the notes under update_profile. |
Returns success, status ("disabled"), applied (the number of attributes modified; 0 means
the account was already disabled), and updated_user.
Notes: every note above applies unchanged -- only --disable-user-attributes's attributes are
written, the action is idempotent, an unwritable configured attribute fails the action on the first
call as well as every retry, the write is verified against the entry's actual attribute values, and
registration is conditional on --disable-user-attributes being set.
You can use compose.yaml to launch an LDAP server and a PHP LDAP admin server to interact with the LDAP server.
Run docker-compose up to launch the containers.
You can then access the PHP LDAP admin server at http://localhost:8080 and login with the admin credentials you provided in the docker-compose file.
username: CN=admin,DC=example,DC=org
password: admin
After you login you can create new resources to be synced by baton.
After creating new resources on the LDAP server, use the baton-ldap cli to sync the data from the LDAP server with the example command below.
baton-ldap --base-dn dc=example,dc=org --bind-dn cn=admin,dc=example,dc=org --password admin --domain localhost
After successfully syncing data, use the baton CLI to list the resources and see the synced data.
baton resources
baton stats
baton-ldap will fetch information about the following LDAP resources:
- Users
- Roles as
organizationalRolein LDAP - Groups as
groupOfUniqueNamesin LDAP
baton-ldap will sync information only from under the base DN specified by the --base-dn flag in the configuration.
We started Baton because we were tired of taking screenshots and manually building spreadsheets. We welcome contributions, and ideas, no matter how small -- our goal is to make identity and permissions sprawl less painful for everyone. If you have questions, problems, or ideas: Please open a Github Issue!
See CONTRIBUTING.md for more details.
