Skip to main content

Overrides and PowerShell

Building a connector, step 7 (LDAP track, 4 of 4). Previous: Install-editable values.

The definition covers what every install of a directory needs. When one operation needs something it cannot express, you have two ways out, and neither is a reason to build an HTTP connector instead:

  • An override replaces one handler with TypeScript, and can call the engine's own handler inside it.
  • A PowerShell script runs on the gateway, as a custom operation or in place of one handler. Use it for work LDAP cannot do, such as creating an Exchange mailbox or a home folder on a file share.

Both apply to SQL connectors in the same way.

Override one handler​

The Handlers tab lists every handler the engine runs for your definition, such as user.create or grant.member_of.add. Select Override beside one, and the builder writes a module at that handler's path, which calls the engine's own handler unchanged:

import type { Override } from "../../types.gen.ts";

// Overrides the LDAP engine's user.create. `ctx.engineDefault` runs the engine's own handler for
// this key with the same context: change what goes in or what comes out, or replace it outright.
const handler: Override<"user.create"> = async (input, ctx) => {
return ctx.engineDefault(input);
};

export default handler;

The module's path comes from the handler key, with / between the parts:

Handler keyOverride file
user.createhandlers/user/create.ts
group.fetchRelationshipshandlers/group/fetchRelationships.ts
grant.member_of.addhandlers/grants/member_of/add.ts
user.op.unlockAccounthandlers/user/op/unlockAccount.ts

A flat name such as handlers/user.create.ts is refused. You can override only a handler the definition enables: an override of user.delete on a connector without write.delete stops the connector from building, and publishing is refused until you remove it.

ctx.engineDefault takes the same input the override received, so an override can adjust the input, call the engine, and adjust the result. This one fills in displayName when provisioning sends none:

import type { Override } from "../../types.gen.ts";

const handler: Override<"user.create"> = async (input, ctx) => {
const displayName = input.displayName ?? [input.givenName, input.sn].filter(Boolean).join(" ");
return ctx.engineDefault({ ...input, displayName });
};

export default handler;

The engine writes only attributes the definition declares as writable, so displayName must be in the account's attribute list.

When an override builds a DN or a filter, build it with ctx.ldap.dn and ctx.ldap.filter, which escape every value: ctx.ldap.dn.child(parent, attribute, value), ctx.ldap.dn.escape(value), ctx.ldap.filter.equals(attribute, value), ctx.ldap.filter.and(...) and the rest. Names from an HR system and member DNs are data an attacker can influence; concatenating them into a filter is an injection. Every method of ctx.ldap, and the result codes a refused call carries, is in the Handler API. An override that replaces a membership handler takes on the Grant write contract.

PowerShell scripts​

A script is a file in the connector, scripts/<name>.ps1, where the name starts with a letter or digit and uses letters, digits, _ and -, up to 64 characters. Add it on the Files tab. The definition then binds it in one of two ways.

As a custom operation​

"config": {
"properties": {
"homeRoot": {
"type": "string",
"title": "Home folder root",
"description": "The share home folders are created under, for example \\\\fs1.corp.northwind.example\\home.",
"default": ""
}
}
},
"custom_ops": {
"create-home-folder": {
"script": "create-home-folder",
"title": "Create home folder",
"description": "Creates the account's home folder under the install's home folder root.",
"params_schema": { "type": "object", "additionalProperties": false, "properties": {} },
"config": ["homeRoot"],
"timeoutMs": 120000
}
}
FieldWhat it does
scriptThe script's name, without scripts/ and .ps1.
entityOptional, and only the account entity: custom operations are actions on a person's account.
title, descriptionWhat admins and workflows see.
params_schemaJSON Schema for the operation's parameters. Owlie checks the parameters against it before the script runs.
configInstall fields the script receives, as $Params.config.
secretsParameters bound to gateway secrets (below).
timeoutMsHow long the script may run, up to 900000 milliseconds. Two gateway limits cap it, both 60000 by default (below).

This operation's timeoutMs of 120000 takes effect only when the gateway's operator raises its two time limits to at least that (How a script runs). The operation's handler is <account entity>.op.<key>, here user.op.create-home-folder. The script reads its input from $Params, a hashtable: the operation's parameters, $Params.identifiers (the entry the operation runs on, with its dn and identifier) and $Params.config. A caller cannot set identifiers or config, so a script acts on the account it was run for, never on one a caller names:

# Creates the home folder of the account this operation runs on, under the install's home root,
# and gives the account Modify rights on it.
$ids = $Params.identifiers
if (-not ($ids -and $ids.objectGUID)) { throw 'create-home-folder runs on an account.' }
$root = [string]$Params.config.homeRoot
if (-not $root.StartsWith('\\')) { throw 'The install has no home folder root.' }

$user = Get-ADUser -Identity ([guid]$ids.objectGUID)
$path = Join-Path $root $user.SamAccountName
New-Item -ItemType Directory -Path $path -Force | Out-Null

$acl = Get-Acl -Path $path
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new($user.SID, 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
$acl.AddAccessRule($rule)
Set-Acl -Path $path -AclObject $acl

[pscustomobject]@{ path = $path } | ConvertTo-Json -Compress

This script needs the ActiveDirectory PowerShell module on the gateway host, and the gateway's own account needs the rights to create the folder and set its permissions.

In place of one handler​

handlers.<key> replaces the engine's handler for a key the definition enables, with the fields script, config, secrets and timeoutMs:

"handlers": {
"user.disable": { "script": "disable-account" }
}

The script must write what the engine's handler would return, and what it reads as $Params depends on the key:

  • fetchOne, create, fetchAll, fetchAllSince, delete, enable, disable, a Grant kind's add and remove, and connection.testConnection/connection.validateConfig all take one value, which the script reads as $Params.payload, beside $Params.identifiers for the entry the operation runs on: null when the operation has none, such as create, fetchAll, fetchAllSince, a Grant write, or either connection operation, which has no entry at all, only the install being tested or validated.
  • update takes the entry and the change, so the script reads $Params.identifiers and $Params.input.
  • fetchRelationships takes the external id of the entry whose relationships are read (the group, for a membership grant) and the page request (its cursor and page size), so the script reads $Params.externalId and $Params.options. There is no $Params.identifiers here: the operation is not about one entry, only about what points at it.

A script replacing fetchOne, fetchAll, fetchAllSince or fetchRelationships stands in for a read, so Owlie checks its output before using it: a page (fetchAll, fetchAllSince, fetchRelationships) must be { records, nextCursor, hasMore }, with nextCursor set whenever hasMore is true, and fetchOne must be { record }, record being the entry or null when there is none. A script that returns something else fails the operation with a message that says so, instead of passing the bad result on to a sync or a lookup.

A script cannot report "already present" or "already absent" for a Grant write; override the handler in TypeScript when you need to. A binding for a key the definition does not enable is refused, and so is one for a read-only operation such as directory discovery: there is no PowerShell broker in that context, so a script bound there would fail every time it ran.

How a script runs​

  • The gateway runs only the text of the script in the build being run, checked by its digest: a published version for installs, or your draft during a test run. Changing a script on an install means publishing a new version. A script and its parameters may each be up to 256 KiB.
  • The gateway starts the script with $ErrorActionPreference = 'Stop' and $Params already set, from JSON it passes on standard input, never on the command line. Because that preamble comes first, a script cannot begin with param(...), using or #requires.
  • The script writes exactly one JSON value to standard output, which becomes the operation's result. A non-zero exit, no output, or output that is not JSON fails the operation with the end of the script's error output.
  • Publishing refuses a script that evaluates text or starts another program: Invoke-Expression, [ScriptBlock]::Create, Add-Type, & or . before a variable, expression or string, dot-sourcing a file, starting pwsh, powershell or cmd /c, Start-Process, Invoke-Command and Invoke-Item. A script's parameters come from identity data that people can influence, and must stay data.
  • A script may run 60 seconds by default. timeoutMs asks for more, but the gateway caps it twice: at the connector's powershellTimeoutMs, and at customConnectorLimits.timeoutMs, its limit on a whole operation. Both default to 60000, so a timeoutMs above 60000 has no effect until the gateway's operator raises both (gateway settings). powershellTimeoutMs goes up to 900000 (15 minutes). A script that runs out of time is stopped with every process it started, and the operation is not retried, because the script may already have changed something.

Secrets​

secrets binds a parameter to a secret the gateway holds, so the script can use a credential that never passes through Owlie's cloud or the connector's own code:

"secrets": { "fileServerPassword": "NORTHWIND_FS_PASSWORD" }

The script reads the value as $Params.fileServerPassword. The gateway resolves the name from its own secrets, and only when its operator has granted that name to this connector. A caller's parameters can never name a secret. In a draft test the gateway resolves none of its secrets, so an operation whose script binds one runs only from a published, installed version.

A parameter name cannot be one Owlie already fills for that binding, and publishing refuses one that tries. For a custom operation that is identifiers or config; its parameters never carry anything else Owlie fills, so nothing else is off limits (that also means a custom operation's params_schema cannot declare a property named identifiers or config). For a binding in place of one handler, config is always off limits too, plus whatever else that handler's shape fills: payload and identifiers for most handlers, identifiers and input for update, or externalId and options for fetchRelationships.

The operator's gate, and what it means​

A gateway runs a connector's scripts only when its operator sets allowPowershell for that connector. Without it, every script operation fails with a message naming the setting.

A script runs as the gateway's operating-system account, and whatever it writes to standard output returns to Owlie as the operation's result, so a script must never print a secret. The gateway logs the full text of each new script the first time it runs it, so its operator can read what runs. Private-network gateways explains what allowPowershell gives a connector and how operators should grant it.

Next: Test your connector