Skip to main content

Start an LDAP connector

Building a connector, step 7 (LDAP track, 1 of 4). Previous: Choose a kind.

An LDAP connector is one file, definition.json, written in the LDAP engine's vocabulary. You describe where the directory keeps accounts and groups, which attributes Owlie reads, how group membership is stored and which writes Owlie may make. Owlie derives the connector's full declaration from the definition, and the engine built into Owlie performs every operation: search, create, update, enable, disable, delete, and add or remove a membership. You write code only to replace an operation the definition cannot express, which the last page of this track covers.

When LDAP is the right kind​

Choose LDAP when the system is a directory that speaks LDAP: Active Directory Domain Services on your own servers, OpenLDAP, FreeIPA, 389 Directory Server and similar. A cloud directory reached through a REST API, such as Microsoft Entra ID, Okta or Google Workspace, is an HTTP connector.

It runs on a private-network gateway​

An LDAP connector runs only on a private-network gateway: an Owlie agent your organization runs inside its network, which opens the connection to the directory and keeps the bind password there. Owlie's cloud never connects to the directory. Before you can test or install the connector:

  • your tenant needs an active gateway;
  • the gateway's operator adds the connector's id to the gateway's allowedCustomConnectors, and lists the directory's host:port (for example ldap.northwind.example:636) in the connector's egressAllowlist;
  • for installs, the operator grants the connector the gateway secret that holds the bind password.

A test run in the Connector Builder runs your unpublished draft on the gateway with directory credentials you type in, so it asks more than an install does:

  • the person or API key running the test needs the custom_connector.publish permission, not only custom_connector.modify;
  • the run config takes the bind password itself. Gateway secrets apply to installs only: a draft test never resolves one, and a secret name in its place is refused (draft.secret-ref);
  • a handler that can change the directory (create, update, delete, enable, disable, a membership add or remove, or a custom operation) runs only after you confirm it, and a PowerShell script does not run at all without that confirmation.

The connection always uses TLS: ldaps:// connects over TLS, and ldap:// upgrades with StartTLS. The gateway verifies the directory's certificate. A directory whose certificate comes from your own certificate authority, or one you do not want verified, needs the operator's allowTlsOverrides for the connector. The gateway page lists every setting and where the operator sets it.

Create the connector​

The kind is chosen when the connector is created and cannot change later.

In the Connector Builder​

  1. Go to Admin > Integrations > Connectors, open the Custom tab and select New connector.
  2. In the New Connector dialog, keep Start from on Blank and choose LDAP as the Kind.
  3. Enter a Name and a Key (lowercase letters, digits, - and _, starting with a letter or digit), and optionally a Description. Select Create connector.

The LDAP editor opens with a starter definition: inetOrgPerson accounts named by uid, groupOfNames groups named by cn, both identified by entryUUID, and membership stored in the group's member attribute. Its tabs are Basics, Connection, Users, Groups, Sync, Install values, Handlers, Derived manifest, Raw JSON and Files. The builder saves as you type. Anything the forms do not show, such as lookup entities and custom operations, you edit in Raw JSON.

Once your tenant has an active gateway, the strip above the tabs shows the connector's id with a Copy connector id button; until then it offers Set up a gateway instead. Send that id to your gateway's operator for allowedCustomConnectors.

On the Connection tab, Discover the directory reads the directory's root DSE and schema on the gateway, using the Test panel's run config, and offers its naming contexts, object classes and attributes to fill Users and Groups. Select Discover to run it. It changes nothing in the directory.

Through the API​

Create the connector with createCustomConnector, passing kind: "ldap" with the key and name. Its draft starts from the same starter definition. Write your definition.json into the draft with updateCustomConnectorDraft. Build with the API and agents lists the operations and their inputs.

A test run through the API is connectors.testArtifact on POST /api/rpc (Test your connector has the rest). For an LDAP connector, integration_config holds the install fields under config and the bind option with the password typed in:

{
"procedure": "connectors.testArtifact",
"input": {
"connector_id": "<connector id>",
"version_id": "<draft version id>",
"artifact_key": "user.fetchAll",
"test_input": {},
"integration_config": {
"config": {
"url": "ldaps://ldap.northwind.example:636",
"baseDn": "dc=northwind,dc=example",
"userBases": ["ou=people,dc=northwind,dc=example"]
},
"auth": {
"key": "bind",
"config": {
"bindDn": "cn=owlie-test,ou=services,dc=northwind,dc=example",
"bindPassword": "<the test account's bind password>"
}
}
}
}
}

Add "allow_writes": true to run a handler that can change the directory, and "gateway_id" when your tenant has more than one connected gateway.

A test of user.fetchAll returns one page. Its nextCursor cannot be passed to a later test run, because each run opens its own connection to the directory and a search's paging state lives on that connection. The engine pages by the definition's pageSize and ignores a page size in the test input, so set a small pageSize while you test to see hasMore, and check a complete read with a Sync after you install.

The definition​

This skeleton shows the fields most definitions use. The table lists every top-level field, and the sections below and the next pages fill them in.

{
"engine": 1,
"description": "Northwind's OpenLDAP directory: accounts and groups.",
"icon": "@openldap.org",
"config": { "properties": {} },
"pageSize": 500,
"entities": {
"user": {
"name": "Account",
"bases": ["ou=people,dc=northwind,dc=example"],
"scope": "one",
"objectClasses": ["inetOrgPerson"],
"identifier": { "attribute": "entryUUID" },
"rdnAttribute": "uid",
"attributes": { "uid": { "title": "Username" }, "mail": { "title": "Email" } }
},
"group": {
"name": "Group",
"bases": ["ou=groups,dc=northwind,dc=example"],
"objectClasses": ["groupOfNames"],
"identifier": { "attribute": "entryUUID" },
"rdnAttribute": "cn",
"attributes": { "cn": { "title": "Name" } },
"membership": { "attribute": "member", "direction": "member-on-group" }
}
}
}
FieldWhat it is
engineAlways 1, the version of the LDAP definition format.
description, iconWhat the catalog shows. icon is @ plus a domain, such as @openldap.org, or an https:// image URL.
config.propertiesThe install fields your definition reads, as JSON Schema: one per install-editable value, and any field a PowerShell script receives.
pageSizeEntries per search page, 1 to 1000. The default is 500.
entities.userRequired. The account entity.
entities.groupOptional. Groups, and the member_of Grant kind between accounts and groups.
Any other keyA lookup entity, such as ou for organizational units: a lowercase letter, then lowercase letters, digits and _. Synced, never written.
incrementalOptional. How Sync reads only what changed (next page).
custom_ops, handlersOptional. Extra operations and replacement handlers (last page).

Every install fills in the fields every LDAP connector has, which you do not declare: the directory url, the baseDn, tls (a CA certificate and whether to verify), and the bind option's bindDn and bindPassword. Every field is in the LDAP definition reference.

Choose the identifier​

Each entity names its identifier attribute: the directory's own unique id for an entry, which never changes. Choose it from what the directory provides:

Directoryidentifier
OpenLDAP{ "attribute": "entryUUID" }
Active Directory{ "attribute": "objectGUID", "type": "guid" }, read as the GUID's standard text form
389 Directory Server{ "attribute": "nsUniqueId" }
Any other id stored as bytes{ "attribute": "<name>", "type": "binary" }, read as base64

Never choose uid, cn or sAMAccountName: they change when someone is renamed.

Every record also carries the entry's dn, and the engine declares the identifiers in the order ["dn", "<identifier>"]. So for an LDAP connector the DN is the external id, the first identifier, which is an exception to the rule on Model entities. The reason is membership: a group's member attribute lists its members by DN, and each membership must name the account by the same value as the account's record. The identifier attribute is the second identifier, and Owlie marks it as the directory's stable id.

Because the DN is the external id, renaming or moving an entry changes it. Sync does not match the entry to its old record by entryUUID or objectGUID: the entry under its new DN is a new account, with no link to an identity until account matching links it again. The sync profile the engine derives matches the account's mail to the identity's email; an install can change the matching rules. The old record stays until the next full Sync, which no longer finds it. An account Owlie provisioned is then flagged as missing; any other account is removed, and Owlie marks the access it tracked on that account as revoked. Keep mail, or whatever attribute the install matches on, in the attribute list so a renamed account can be matched again.

rdnAttribute is the attribute that names an entry in its DN, such as uid in uid=ada,ou=people,dc=northwind,dc=example. It is required for every entity, and the engine always reads it and the identifier, whatever the attribute list says.

Four fields decide which entries belong to an entity:

FieldWhat it does
basesThe DNs to search under, as whole DNs. Leave it out to search under the install's base DN. With several bases, Sync reads each one to its end, in order.
scopesub (the default) searches the whole subtree under each base, one only its direct children, base only the base entry itself.
objectClassesThe object classes an entry must have. Each one becomes an (objectClass=...) condition. A new entry is created with exactly these classes.
filterOptional. A parenthesized LDAP filter the entry must also match, such as (!(employeeType=service)).

The engine combines the object classes and the filter with AND. With neither, it matches every entry (objectClass=*). Owlie checks the syntax of every DN and filter when it derives the declaration, and the gateway checks them again on every request.

Search bases, object classes and filters differ from one directory to the next, so make them install-editable rather than writing one site's values into the definition. Install-editable values shows how, and what an install's empty search base list means.

Next: Attributes, membership and writes