diff --git a/README.md b/README.md index 859c73d2..d5401ee7 100644 --- a/README.md +++ b/README.md @@ -78,18 +78,27 @@ custom LDAP attributes on an existing user. | Argument | Required | Description | |---|---|---| -| `user_id` | yes | Resource ID reference to the user to update. | +| `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 user resource after the update, best-effort -re-fetched; absent if the read-back failed, though the write itself still succeeded), +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 makes `update_profile` discoverable and usable from ConductorOne's attribute-push-rule @@ -101,9 +110,10 @@ re-fetched; absent if the read-back failed, though the write itself still succee rather than silently vanishing. - `inetOrgPerson` requirement: of the four named fields, only `last_name` (`sn`) is universal -- it's a MUST attribute of the base `person` object class. `first_name` - (`givenName`), `display_name` (`displayName`), and `email` (`mail`) are only defined by - RFC 2798's `inetOrgPerson` object class; writing one of them to an entry that doesn't - carry `inetOrgPerson` fails loudly with LDAP result code 65 ("Object Class Violation"). + (`givenName`, defined in RFC 4519), `display_name` (`displayName`, RFC 2798), and + `email` (`mail`, RFC 4524) are permitted on an entry only by RFC 2798's + `inetOrgPerson` object class; writing one of them to an entry that doesn't carry + `inetOrgPerson` fails 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 against `inetOrgPerson` entries. - `custom_attributes` semantics: an entry is written whenever the key is present, @@ -112,8 +122,10 @@ re-fetched; absent if the read-back failed, though the write itself still succee four named arguments above are the only names mapped to a different LDAP attribute (`first_name` → `givenName`, and so on). A `custom_attributes` key is used verbatim as the attribute name, whatever it looks like: `{"user_id": "x"}` writes an attribute - literally named `user_id` (and fails with LDAP result code 17, "Undefined Attribute - Type", if your directory has no such attribute) -- it does **not** write `uid`. + literally named `user_id` -- it does **not** write `uid`. 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"). Likewise `login` and `path`, 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 to `custom_attributes` entries; none of them changes @@ -134,10 +146,25 @@ re-fetched; absent if the read-back failed, though the write itself still succee 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_attributes` carries 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` (or `base-dn`) may be modified; out-of-scope or non-user DNs are rejected as "not found" (fail-closed). -- Provisioning must be enabled (`--provisioning` / `BATON_PROVISIONING=true`) for actions - to run, and the bind account must have permission to modify the target entry. +- **From a C1 automation, `user_id` takes 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 with `resource with type user was not found` and 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, so `update_profile` runs and writes with + the flag unset. What the action does require is a bind account with permission to modify + the target entry. # Developing baton-ldap diff --git a/docs/connector.mdx b/docs/connector.mdx index 49c00f2d..eab9201c 100644 --- a/docs/connector.mdx +++ b/docs/connector.mdx @@ -54,19 +54,27 @@ Connector actions are custom capabilities that extend C1 automations with app-sp | Action name | Additional fields | Description | | ----------- | ----------------- | ----------- | -| `update_profile` | `user_id` (resource ID, required), `first_name` (string), `last_name` (string), `display_name` (string), `email` (string), `custom_attributes` (string map) | Set core profile fields (first name, last name, display name, email) and/or arbitrary custom LDAP attributes on an existing user. This action is scoped to the `user` resource type, which is what makes it discoverable and usable from C1's attribute-push-rule feature. It also backs C1's [push profile](/product/admin/account-provisioning) flow for LDAP users. | +| `update_profile` | `user_id` (account resource ID, required), `first_name` (string), `last_name` (string), `display_name` (string), `email` (string), `custom_attributes` (string map) | Set core profile fields (first name, last name, display name, email) and/or arbitrary custom LDAP attributes on an existing user. This action is scoped to the `user` resource type, which is what makes it discoverable and usable from C1's attribute-push-rule feature. It also backs C1's [attribute push rule](/product/admin/push-rules) flow for LDAP users. | -The `update_profile` action returns `success` (bool), `updated_user` (the user resource after the update, best-effort re-fetched; absent if the read-back failed, though the write itself still succeeded), `applied` (the number of attributes changed), and `skipped` (named fields or `custom_attributes` entries that were not written). +The `update_profile` action returns `success` (bool), `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 changed), 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. + +Two requirements come from the C1 side rather than from LDAP: + +- **`user_id` takes 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 with `resource with type user was not found` and 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. `update_profile` is intended for generic LDAP directories (Active Directory and FreeIPA have their own connectors). It applies the following rules: - **Named fields:** `first_name` → `givenName`, `last_name` → `sn`, `display_name` → `displayName`, `email` → `mail`. A named field is applied only when present and non-empty; a present-but-empty named field cannot clear the attribute and is instead reported in `skipped`. - - **`inetOrgPerson` requirement:** only `last_name` (`sn`) is universal — it's a MUST attribute of the base `person` object class. `first_name` (`givenName`), `display_name` (`displayName`), and `email` (`mail`) are only defined by RFC 2798's `inetOrgPerson` object class, so writing one of them to an entry that doesn't carry `inetOrgPerson` fails loudly with LDAP result code 65 ("Object Class Violation") — an atomic, clearly-signaled failure with no partial write, not a silent no-op. - - **Custom attributes:** `custom_attributes` maps arbitrary raw LDAP attribute names to values; an empty value clears the attribute (unlike the named fields above, where empty just means "not supplied"). Keys are used **verbatim** as attribute names — the named-field mapping above applies only to the named arguments, never to `custom_attributes`. `{"user_id": "x"}` therefore writes an attribute literally named `user_id` (failing with LDAP result code 17, "Undefined Attribute Type", if the directory has no such attribute); it does not write `uid`. The same goes for baton profile field names such as `login` and `path`: they are attempted as literal attribute names rather than skipped. + - **`inetOrgPerson` requirement:** only `last_name` (`sn`) is universal — it's a MUST attribute of the base `person` object class. `first_name` (`givenName`, defined in RFC 4519), `display_name` (`displayName`, RFC 2798), and `email` (`mail`, RFC 4524) are permitted on an entry only by RFC 2798's `inetOrgPerson` object class, so writing one of them to an entry that doesn't carry `inetOrgPerson` fails loudly with LDAP result code 65 ("Object Class Violation") — an atomic, clearly-signaled failure with no partial write, not a silent no-op. + - **Custom attributes:** `custom_attributes` maps arbitrary raw LDAP attribute names to values; an empty value clears the attribute (unlike the named fields above, where empty just means "not supplied"). Keys are used **verbatim** as attribute names — the named-field mapping above applies only to the named arguments, never to `custom_attributes`. `{"user_id": "x"}` therefore writes an attribute literally named `user_id`; it does not write `uid`. A name the 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"). The same goes for baton profile field names such as `login` and `path`: they are attempted as literal attribute names rather than skipped. - **Collisions:** a `custom_attributes` key is dropped (never merged with, or overwriting, a named field's slot) and reported once in `skipped` when it case-insensitively matches either one of the four named field names, or the LDAP attribute a supplied named field is writing (`givenName`, `sn`, `displayName`, `mail`). The latter only applies when that named field was actually supplied and non-empty; otherwise `{"givenName": "Jane"}` is an ordinary raw write. - **Not modifiable:** password attributes (`userPassword`, or any name containing `password` — use credential rotation instead) and `objectClass` are rejected; the user's RDN attribute (for example `cn` when the DN is `cn=jdoe,...`) is skipped, since renaming requires a different operation. - **Multi-valued attributes:** setting (not clearing) a value on an attribute that currently holds more than one value returns an error instead of silently discarding the extra values; clearing (an empty value) still removes all values. + - **Value types:** `custom_attributes` carries one string per attribute, so binary attributes (`jpegPhoto`, `userCertificate;binary`) and option-tagged attributes (`;lang-xx`) cannot be set through this action. - **Scope:** only entries within the configured user search scope (`user-search-dn`, falling back to `base-dn`) can be modified; out-of-scope or non-user DNs are rejected. The connector's bind account must have permission to modify the target entry.