Skip to content

feat: add update_user_attrs action with extension attribute support#155

Open
c1-dev-bot[bot] wants to merge 1 commit into
mainfrom
feat/extension-attribute-support
Open

feat: add update_user_attrs action with extension attribute support#155
c1-dev-bot[bot] wants to merge 1 commit into
mainfrom
feat/extension-attribute-support

Conversation

@c1-dev-bot

@c1-dev-bot c1-dev-bot Bot commented Mar 26, 2026

Copy link
Copy Markdown

Summary

Adds a new update_user_attrs action to the LDAP connector via the GlobalActionProvider interface. This enables ConductorOne automations to update user attributes in LDAP/Active Directory.

Key features:

  • Standard profile attribute mapping: Maps ConductorOne profile fields (first_name, last_name, display_name, middle_name, job_title, department, division, company, employee_id, employee_number, employment_type, email) to their LDAP equivalents (givenName, sn, displayName, etc.)
  • Extension attribute passthrough: Attributes not in the standard mapping (e.g., extensionAttribute1 through extensionAttribute15) are passed through directly to LDAP, enabling support for AD extension attributes
  • Arbitrary custom attributes: Any valid LDAP attribute name can be used, not just the predefined set
  • Empty value handling: Setting an attribute to an empty string deletes the attribute from LDAP

Implementation details:

  • New actions.go with GlobalActions() method on the LDAP struct
  • resolveAttrName() function for profile-to-LDAP name resolution
  • buildLDAPChanges() to construct LDAP modify operations from the attrs map and update mask
  • Uses existing LdapModify client method for LDAP operations

Action schema:

  • Name: update_user_attrs
  • Arguments: resource_type (string), resource_id (string/DN), attrs (stringMap), attrs_update_mask (stringSlice)
  • Action types: ACTION_TYPE_ACCOUNT, ACTION_TYPE_ACCOUNT_UPDATE_PROFILE

Test plan

  • Unit tests for resolveAttrName covering all 12 standard mappings plus extension/custom attribute passthrough
  • Unit tests for buildLDAPChanges covering standard attributes, extension attributes, empty value deletion, missing attrs in mask, and mixed attribute types
  • Build succeeds (go build ./...)
  • Integration testing with a real LDAP/AD server (requires Docker/testcontainers, not available in CI-only env)

Fixes: CXH-1258


Automated PR Notice

This PR was automatically created by c1-dev-bot as a potential implementation.

This code requires:

  • Human review of the implementation approach
  • Manual testing to verify correctness
  • Approval from the appropriate team before merging

Add a new GlobalActions implementation that registers an update_user_attrs
action for the LDAP connector. This enables ConductorOne automations to
update user attributes in LDAP/Active Directory, including:

- Standard profile attributes (first_name, last_name, display_name, etc.)
  mapped to their LDAP equivalents (givenName, sn, displayName, etc.)
- Extension attributes (extensionAttribute1-15) passed through directly
- Arbitrary custom LDAP attributes beyond the predefined set

The action accepts a stringMap of attributes and an update mask, resolves
ConductorOne profile field names to LDAP attribute names, and uses
LdapModify to update the entry. Attributes not in the standard mapping
are passed through as-is, enabling support for any valid LDAP attribute.

Fixes: CXH-1258
@c1-dev-bot c1-dev-bot Bot requested a review from a team March 26, 2026 18:29
@linear

linear Bot commented Mar 26, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @c1-dev-bot[bot]'s task in 1m 42s —— View job


PR Review: update_user_attrs action with extension attribute support

  • Read PR changes and gather context
  • Review implementation (actions.go)
  • Review tests (actions_test.go)
  • Post review feedback

Overall this is a clean, well-structured implementation. The action schema, attribute mapping, and test coverage are solid. I have a few issues to flag — one is a security concern, and the others are consistency/correctness improvements.


1. Missing DN validation (security/consistency concern)actions.go:114

The userDN from the request is used directly without canonicalization. Every other place in this codebase that accepts a DN from external input calls ldap.CanonicalizeDN() first (see user.go:320, user.go:349, user.go:443, group.go:156, group.go:245, role.go:130, etc.). This serves two purposes:

  • Validates that the input is a syntactically valid DN
  • Normalizes the DN for consistent handling

The getAccount call on line 138 will catch truly invalid DNs at the LDAP level, but canonicalizing upfront is the established pattern and provides a cleaner error message.

// Current (line 114):
userDN := resourceID.GetStringValue()

// Suggested:
parsedDN, err := ldap.CanonicalizeDN(resourceID.GetStringValue())
if err != nil {
    return nil, nil, fmt.Errorf("baton-ldap: invalid user DN: %w", err)
}
userDN := parsedDN.String()

Fix this →

2. Attribute value logged at INFO levelactions.go:205-208

The buildLDAPChanges function logs each attribute update at INFO level. While it doesn't log the value, consider that in a debug scenario someone might add value logging here. More importantly, the attribute names themselves could be sensitive metadata (e.g., knowing which extension attributes are being updated). This is minor — just something to be aware of. The current logging level is consistent with line 156-159.

3. Test uses magic number instead of constantactions_test.go:107

// Current:
if changes[0].Operation != 1 {

// Preferred (matches how production code references it):
if changes[0].Operation != ldap3.DeleteAttribute {

This makes the test self-documenting and won't break if the constant value ever changes. The comment above it acknowledges this ("Delete operation is 1 in go-ldap") but using the constant directly is better.

Fix this →

4. GlobalActions can be simplifiedactions.go:91-97

// Current:
func (l *LDAP) GlobalActions(ctx context.Context, registry actions.ActionRegistry) error {
    err := registry.Register(ctx, updateUserAttrsActionSchema, l.updateUserAttributes)
    if err != nil {
        return err
    }
    return nil
}

// Simpler:
func (l *LDAP) GlobalActions(ctx context.Context, registry actions.ActionRegistry) error {
    return registry.Register(ctx, updateUserAttrsActionSchema, l.updateUserAttributes)
}

5. Error prefix inconsistency

The file uses "baton-ldap:" as the error prefix (e.g., lines 104, 107, 113, 116, 141, 164), which is correct per the CLAUDE.md pattern (baton-service). However, the existing codebase in user.go uses "ldap-connector:" in some places (e.g., user.go:445). This new code is actually more consistent with the CLAUDE.md guidance, so this is fine — just noting it for awareness.

6. Consider what happens with nil attrsField.GetStructValue()actions.go:124

If attrs is present in the struct but isn't actually a struct value (e.g., it's a null or different type), GetStructValue() returns nil, and GetFields() on nil returns nil, so the range loop is a no-op. This means the function would silently produce zero changes and return success. This is arguably fine (no attrs = no-op), but you might want to validate that attrsField.GetStructValue() is non-nil and return an error if attrs was declared required but empty.


Summary

The implementation is well-done with good separation of concerns. The main actionable item is #1 (DN validation) — it's a consistency issue with the rest of the codebase and provides better input validation. Items #3 and #4 are minor cleanup. The test coverage is good, covering the key scenarios (standard mapping, extension passthrough, delete, missing mask entries, mixed types).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants