Example: LDAP directory
A complete, read-write LDAP connector for the OpenLDAP directory of Northwind, a fictional company. It syncs accounts, groups and group membership, creates, updates, disables, enables and deletes accounts, and adds and removes memberships. Each install sets its own search bases, object classes, attribute lists and disable attribute. An Active Directory variation follows.
Paste the definition into the Connector Builder's Raw JSON tab of a new LDAP connector, or write it
as definition.json through the API. Both definitions on this page derive with Owlie's LDAP engine and
pass its publish checks. The LDAP track explains every field.
The directory
- Base DN
dc=northwind,dc=example, reached atldaps://ldap.northwind.example:636. - People are
inetOrgPersonentries named byuidin one OU,ou=people,dc=northwind,dc=examplein production. Groups aregroupOfNamesentries named bycninou=groups,dc=northwind,dc=example, listing their members' DNs inmember. - Every entry has OpenLDAP's
entryUUID. - The directory runs the ppolicy overlay with a default policy that has
pwdLockout: TRUE, so settingpwdAccountLockedTimeto000001010000Zstops an account binding.
Decisions
- Identifier.
entryUUIDnever changes, so it is each entity'sidentifier. The engine putsdnfirst, as it does for every LDAP connector. - Search bases are install-editable with no seed. Test and production directories use different
OUs, and there is no sensible default, so the install form asks for
userBasesandgroupBases. - Object classes and attribute lists are install-editable with seeds. Most installs keep
inetOrgPersonandgroupOfNames; a site with its own object class or extra attributes changes its copy. scopeisone. People and groups sit directly under their OUs.- Baselines. A new account's
uidcomes from the local part of its email address, with a number added when it is taken. Becauseuidalso names the entry, a retry gives the new entry a new DN too.mail,cn,givenNameandsncome from the identity.createTimestampis read-only, so it has no baseline. - Locked state for Sync. The lifecycle gives every account record
account_disabled, so Sync reads a locked account as disabled, with each install's own lifecycle values.pwdAccountLockedTimeis also read as a read-only attribute, so the account shows when it was locked. - Writes.
writewithdelete: truelets Owlie create, update and delete accounts. New accounts go under the first account search base, because there is nowrite.base.passwordwritesuserPassword. - Lifecycle. A value lifecycle on ppolicy's lock.
enabledValueis seeded empty, so enabling removes the lock. All three values are install-editable, so a directory without ppolicy can use another attribute. - Membership.
member-on-groupandwritable, so Owlie adds and removes members.nestedstays off. Turning it on needs OpenLDAP's memberof overlay, which keepsmemberOfon accounts and groups. - Incremental sync on
modifyTimestamp, re-reading five minutes before the last marker.
The definition
{
"engine": 1,
"description": "Northwind's OpenLDAP directory: accounts, groups and group membership, synced and provisioned.",
"icon": "@openldap.org",
"config": {
"properties": {
"userBases": {
"type": "array",
"items": { "type": "string" },
"title": "Account search bases",
"description": "The OUs whose accounts this install syncs, as whole DNs, for example ou=people,dc=northwind,dc=example."
},
"groupBases": {
"type": "array",
"items": { "type": "string" },
"title": "Group search bases",
"description": "The OUs whose groups this install syncs, as whole DNs, for example ou=groups,dc=northwind,dc=example."
},
"userObjectClasses": {
"type": "array",
"items": { "type": "string" },
"title": "Account object classes",
"default": ["inetOrgPerson"]
},
"groupObjectClasses": {
"type": "array",
"items": { "type": "string" },
"title": "Group object classes",
"default": ["groupOfNames"]
},
"userAttributes": {
"type": "object",
"title": "Account attributes",
"description": "The account attributes this install syncs, keyed by LDAP attribute name.",
"default": {
"uid": {
"title": "Username",
"baseline": {
"usernameGenerator": {
"steps": [
{
"blocks": [
{
"type": "field",
"field": "email",
"modifiers": [
{ "type": "splitAt", "delimiter": "@", "keep": "before" },
{ "type": "lowercase" }
]
}
]
},
{
"blocks": [
{
"type": "field",
"field": "email",
"modifiers": [
{ "type": "splitAt", "delimiter": "@", "keep": "before" },
{ "type": "lowercase" }
]
},
{ "type": "increment", "startFrom": 1, "padding": 1 }
]
}
],
"maxAttempts": 20
}
}
},
"mail": { "title": "Email", "baseline": "identity.email" },
"cn": { "title": "Name", "baseline": "identity.display_name" },
"givenName": { "title": "Given name", "baseline": "identity.first_name" },
"sn": { "title": "Surname", "baseline": "identity.last_name" },
"title": { "title": "Job title" },
"departmentNumber": { "title": "Department number" },
"createTimestamp": { "title": "Created", "readOnly": true },
"pwdAccountLockedTime": { "title": "Locked since", "readOnly": true }
}
},
"groupAttributes": {
"type": "object",
"title": "Group attributes",
"description": "The group attributes this install syncs, keyed by LDAP attribute name.",
"default": {
"cn": { "title": "Name" },
"description": { "title": "Description" }
}
},
"lifecycleAttribute": {
"type": "string",
"title": "Disable attribute",
"description": "The attribute that disables an account. ppolicy's pwdAccountLockedTime stops the account binding.",
"default": "pwdAccountLockedTime"
},
"lifecycleDisabledValue": {
"type": "string",
"title": "Disabled value",
"description": "The value that marks an account disabled. 000001010000Z is ppolicy's permanent lock.",
"default": "000001010000Z"
},
"lifecycleEnabledValue": {
"type": "string",
"title": "Enabled value",
"description": "The value that marks an account enabled. Empty removes the attribute, which lifts ppolicy's lock.",
"default": ""
}
}
},
"pageSize": 500,
"entities": {
"user": {
"name": "Account",
"bases": { "$oxl": "config.userBases" },
"scope": "one",
"objectClasses": { "$oxl": "config.userObjectClasses" },
"identifier": { "attribute": "entryUUID" },
"rdnAttribute": "uid",
"attributes": { "$oxl": "config.userAttributes" },
"write": { "delete": true },
"lifecycle": {
"attribute": { "$oxl": "config.lifecycleAttribute" },
"enabledValue": { "$oxl": "config.lifecycleEnabledValue" },
"disabledValue": { "$oxl": "config.lifecycleDisabledValue" }
},
"password": { "attribute": "userPassword" }
},
"group": {
"name": "Group",
"bases": { "$oxl": "config.groupBases" },
"scope": "one",
"objectClasses": { "$oxl": "config.groupObjectClasses" },
"identifier": { "attribute": "entryUUID" },
"rdnAttribute": "cn",
"attributes": { "$oxl": "config.groupAttributes" },
"membership": {
"attribute": "member",
"direction": "member-on-group",
"writable": true
}
}
},
"incremental": {
"attribute": "modifyTimestamp",
"kind": "timestamp",
"overlapSeconds": 300
}
}
What an install fills in
| Field | Value for Northwind production |
|---|---|
url | ldaps://ldap.northwind.example:636 |
baseDn | dc=northwind,dc=example |
bindDn | cn=owlie-sync,ou=services,dc=northwind,dc=example |
bindPassword | The name of the gateway secret that holds the bind password |
userBases | ["ou=people,dc=northwind,dc=example"] |
groupBases | ["ou=groups,dc=northwind,dc=example"] |
Every other install field starts at its seed. The bind account needs to read the account and group
OUs, and, for writes, to create, change and delete entries under ou=people and change member on the
groups.
What Owlie derives
- Entities
user(the account) andgroup, each with the identifiers["dn", "entryUUID"]. - The Grant kind
member_of, fromusertogroup, a membership, writable. providesofsyncandprovision, and the handlersuser.fetchOne,user.fetchAll,user.fetchAllSince,user.create,user.update,user.delete,user.enable,user.disable,group.fetchOne,group.fetchAll,group.fetchRelationships,grant.member_of.addandgrant.member_of.remove, plus the connection checks.
Active Directory variation
The same shape for Northwind's Active Directory domain, DC=corp,DC=northwind,DC=example. What changes:
- Identifier.
objectGUIDwith"type": "guid", read as the GUID's standard text form. - Filters. The account filter leaves out computer accounts and Active Directory's critical system
accounts, and is install-editable. Object classes are fixed to
userandgroup, since every domain uses them. - Attributes.
objectSidis read as a security identifier, andmemberOfis a read-only list.userAccountControlis read as a read-only number; Sync reads disabled accounts from the flag lifecycle below.userPrincipalNameandmailcome from the identity's email, andsAMAccountNamefrom its local part, with a number added when it is taken. - Collisions. A retry after "already taken" changes only
sAMAccountName. AuserPrincipalNamethat is already taken stays taken, andcn, which comes from the display name and names the entry, collides on the DN for two people with the same name in the same OU. Either collision repeats on every candidate, and the create fails. Where same-name collisions can happen, givecnthe baselinegeneratedand read the display name intodisplayNameinstead. - New accounts.
userDnTemplateplaces new accounts by theircn. Seeded empty, it creates them under the first account search base. - Lifecycle and password. A flag lifecycle on bit
2ofuserAccountControl, starting new accounts at512, andunicodePwdpasswords. A new account is created disabled, given its password, then enabled. - Nested groups are install-editable and off by default. Turned on, each group's members include the groups inside it, Owlie shows a membership through a nested group as inherited, and membership writes accept a group member. The group filter leaves out Active Directory's built-in groups, so a built-in group nested inside another is not reported, and memberships through it are not shown.
- Organizational units are a read-only lookup entity,
ou, fromouBases; an empty list reads the whole base DN. - Incremental sync follows
uSNChanged, tied to the domain controller that issued it. The install's URL should name one domain controller. - Custom operations the engine runs itself: unlock an account, and move it to another OU.
{
"engine": 1,
"description": "Northwind's Active Directory domain: accounts, groups, organizational units and group membership.",
"icon": "@microsoft.com",
"config": {
"properties": {
"userBases": {
"type": "array",
"items": { "type": "string" },
"title": "Account search bases",
"description": "The OUs whose accounts this install syncs, for example OU=Staff,DC=corp,DC=northwind,DC=example."
},
"groupBases": {
"type": "array",
"items": { "type": "string" },
"title": "Group search bases",
"description": "The OUs whose groups this install syncs, as whole DNs."
},
"ouBases": {
"type": "array",
"items": { "type": "string" },
"title": "Organizational unit search bases",
"description": "Where this install reads organizational units. Empty reads the whole base DN.",
"default": []
},
"userFilter": {
"type": "string",
"title": "Account filter",
"description": "People only, without Active Directory's critical system accounts.",
"default": "(&(objectCategory=person)(!(isCriticalSystemObject=TRUE)))"
},
"userAttributes": {
"type": "object",
"title": "Account attributes",
"default": {
"sAMAccountName": {
"title": "Logon name (pre-Windows 2000)",
"baseline": {
"usernameGenerator": {
"steps": [
{
"blocks": [
{
"type": "field",
"field": "email",
"modifiers": [
{ "type": "splitAt", "delimiter": "@", "keep": "before" },
{ "type": "lowercase" }
]
}
]
},
{
"blocks": [
{
"type": "field",
"field": "email",
"modifiers": [
{ "type": "splitAt", "delimiter": "@", "keep": "before" },
{ "type": "lowercase" }
]
},
{ "type": "increment", "startFrom": 1, "padding": 1 }
]
}
],
"maxAttempts": 20
}
}
},
"userPrincipalName": { "title": "User principal name", "baseline": "identity.email" },
"cn": { "title": "Name", "baseline": "identity.display_name" },
"givenName": { "title": "Given name", "baseline": "identity.first_name" },
"sn": { "title": "Surname", "baseline": "identity.last_name" },
"mail": { "title": "Email", "baseline": "identity.email" },
"department": { "title": "Department" },
"userAccountControl": { "title": "Account control flags", "type": "number", "readOnly": true },
"objectSid": { "title": "Security identifier", "type": "sid", "readOnly": true },
"memberOf": { "title": "Direct groups (DN)", "multi": true, "readOnly": true }
}
},
"userDnTemplate": {
"type": "string",
"title": "New account DN",
"description": "Where a new account is created, for example CN={cn},OU=Staff,DC=corp,DC=northwind,DC=example. Empty creates it under the first account search base.",
"default": ""
},
"nestedGroups": {
"type": "boolean",
"title": "Nested groups",
"description": "Sync the groups inside each group as its members, so memberships through them show as inherited, and membership writes accept a group member. Only groups within the group search bases and filter are synced: a nested group outside them, such as a built-in group the default filter leaves out, is not reported, and memberships through it are not shown.",
"default": false
}
}
},
"entities": {
"user": {
"name": "Account",
"bases": { "$oxl": "config.userBases" },
"scope": "sub",
"objectClasses": ["user"],
"filter": { "$oxl": "config.userFilter" },
"identifier": { "attribute": "objectGUID", "type": "guid" },
"rdnAttribute": "cn",
"attributes": { "$oxl": "config.userAttributes" },
"write": { "delete": true, "dn": { "$oxl": "config.userDnTemplate" } },
"lifecycle": { "attribute": "userAccountControl", "disabledFlag": 2, "newAccountValue": 512 },
"password": { "attribute": "unicodePwd", "encoding": "unicodePwd" }
},
"group": {
"name": "Group",
"bases": { "$oxl": "config.groupBases" },
"scope": "sub",
"objectClasses": ["group"],
"filter": "(!(isCriticalSystemObject=TRUE))",
"identifier": { "attribute": "objectGUID", "type": "guid" },
"rdnAttribute": "cn",
"attributes": {
"cn": { "title": "Name" },
"description": { "title": "Description" },
"objectSid": { "title": "Security identifier", "type": "sid", "readOnly": true }
},
"membership": {
"attribute": "member",
"direction": "member-on-group",
"writable": true,
"nested": { "$oxl": "config.nestedGroups" }
}
},
"ou": {
"name": "Organizational unit",
"bases": { "$oxl": "config.ouBases" },
"scope": "sub",
"objectClasses": ["organizationalUnit"],
"identifier": { "attribute": "objectGUID", "type": "guid" },
"rdnAttribute": "ou",
"attributes": { "ou": { "title": "Name" }, "description": { "title": "Description" } }
}
},
"incremental": { "attribute": "uSNChanged", "kind": "counter", "source": "invocationId" },
"custom_ops": {
"unlockAccount": {
"title": "Unlock account",
"description": "Clears the lockout Active Directory applies after too many failed sign-ins.",
"ldap": { "replace": { "lockoutTime": "0" } }
},
"moveToOu": {
"title": "Move to organizational unit",
"description": "Moves the account, keeping its name, under the organizational unit targetOuDn names.",
"ldap": { "move": { "param": "targetOuDn" } }
}
}
}
An install sets url (such as ldaps://dc1.corp.northwind.example:636), baseDn, the bind
credentials, userBases and groupBases, and may change the rest. Active Directory accepts
unicodePwd only over an encrypted connection, which the gateway always uses.
The bind account needs to read the account, group and OU containers. For the writes, it needs, in the
account OUs, the rights to create and delete user objects, reset passwords (for unicodePwd) and write
userAccountControl, lockoutTime and the attributes Owlie sets; the right to write member on the
groups; and, for moveToOu, the rights to move an account out of its OU and into the destination OU.