Skip to main content

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 at ldaps://ldap.northwind.example:636.
  • People are inetOrgPerson entries named by uid in one OU, ou=people,dc=northwind,dc=example in production. Groups are groupOfNames entries named by cn in ou=groups,dc=northwind,dc=example, listing their members' DNs in member.
  • Every entry has OpenLDAP's entryUUID.
  • The directory runs the ppolicy overlay with a default policy that has pwdLockout: TRUE, so setting pwdAccountLockedTime to 000001010000Z stops an account binding.

Decisions​

  • Identifier. entryUUID never changes, so it is each entity's identifier. The engine puts dn first, 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 userBases and groupBases.
  • Object classes and attribute lists are install-editable with seeds. Most installs keep inetOrgPerson and groupOfNames; a site with its own object class or extra attributes changes its copy.
  • scope is one. People and groups sit directly under their OUs.
  • Baselines. A new account's uid comes from the local part of its email address, with a number added when it is taken. Because uid also names the entry, a retry gives the new entry a new DN too. mail, cn, givenName and sn come from the identity. createTimestamp is 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. pwdAccountLockedTime is also read as a read-only attribute, so the account shows when it was locked.
  • Writes. write with delete: true lets Owlie create, update and delete accounts. New accounts go under the first account search base, because there is no write.base. password writes userPassword.
  • Lifecycle. A value lifecycle on ppolicy's lock. enabledValue is 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-group and writable, so Owlie adds and removes members. nested stays off. Turning it on needs OpenLDAP's memberof overlay, which keeps memberOf on 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​

FieldValue for Northwind production
urlldaps://ldap.northwind.example:636
baseDndc=northwind,dc=example
bindDncn=owlie-sync,ou=services,dc=northwind,dc=example
bindPasswordThe 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) and group, each with the identifiers ["dn", "entryUUID"].
  • The Grant kind member_of, from user to group, a membership, writable.
  • provides of sync and provision, and the handlers user.fetchOne, user.fetchAll, user.fetchAllSince, user.create, user.update, user.delete, user.enable, user.disable, group.fetchOne, group.fetchAll, group.fetchRelationships, grant.member_of.add and grant.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. objectGUID with "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 user and group, since every domain uses them.
  • Attributes. objectSid is read as a security identifier, and memberOf is a read-only list. userAccountControl is read as a read-only number; Sync reads disabled accounts from the flag lifecycle below. userPrincipalName and mail come from the identity's email, and sAMAccountName from its local part, with a number added when it is taken.
  • Collisions. A retry after "already taken" changes only sAMAccountName. A userPrincipalName that is already taken stays taken, and cn, 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, give cn the baseline generated and read the display name into displayName instead.
  • New accounts. userDnTemplate places new accounts by their cn. Seeded empty, it creates them under the first account search base.
  • Lifecycle and password. A flag lifecycle on bit 2 of userAccountControl, starting new accounts at 512, and unicodePwd passwords. 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, from ouBases; 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.