Skip to content

feat: sync app user profile attributes as grant metadata#146

Open
c1-dev-bot[bot] wants to merge 3 commits into
mainfrom
feat/app-user-profile-sync
Open

feat: sync app user profile attributes as grant metadata#146
c1-dev-bot[bot] wants to merge 3 commits into
mainfrom
feat/app-user-profile-sync

Conversation

@c1-dev-bot
Copy link
Copy Markdown
Contributor

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

Summary

  • Include app-specific user profile attributes (assigned apps, scopes, custom fields) in grant metadata when syncing app user and group assignments
  • Surface Okta app assignment profile data (e.g., assigned scopes, app roles, external IDs) in ConductorOne via GrantMetadata annotations
  • Add profile metadata extraction for both user-to-app and group-to-app assignment grants

Details

When syncing app grants, the connector now attaches the app user's profile attributes as grant metadata. This enables ConductorOne to surface app-specific attributes that are set on the user's assignment in Okta, such as:

  • Application scopes
  • Custom profile fields (e.g., attributes synced from Cognito)
  • Assignment status and scope type (direct vs group-based)
  • External IDs

Key Changes in pkg/connector/app.go:

  • appUserProfileToMetadata(): Extracts AppUser profile fields plus assignment-level fields (scope, status, externalId) prefixed with _
  • appGroupAssignmentProfileToMetadata(): Extracts ApplicationGroupAssignment profile fields
  • Updated listAppUsersGrants() and listAppGroupGrants() to attach profile metadata to grants

This follows the same pattern used in AWS mode (aws_account.go) for managing app user profile attributes like samlRoles.

Test Plan

  • go build ./... passes
  • go test ./... passes
  • go vet ./... passes
  • Manual testing with an Okta instance that has apps with custom user profile attributes
  • Verify grant metadata appears correctly in ConductorOne after sync

Fixes: CE-526


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

Include app-specific user profile attributes (assigned apps, scopes,
custom fields) in grant metadata when syncing app user and group
assignments. This surfaces Okta app assignment profile data in
ConductorOne, enabling visibility into app-specific attributes like
assigned scopes that are set on user profiles within Okta apps.

Changes:
- Add appUserProfileToMetadata() to convert AppUser profile to grant
  metadata, including scope, status, and externalId fields
- Add appGroupAssignmentProfileToMetadata() for group assignment profiles
- Attach profile metadata to user app grants via WithGrantMetadata
- Attach profile metadata to group app grants via WithGrantMetadata
@linear
Copy link
Copy Markdown

linear Bot commented Mar 12, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

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


PR Review: Sync app user profile attributes as grant metadata

  • Read PR diff and changed files
  • Review implementation for correctness and best practices
  • Post review findings

Summary

This is a clean, well-structured PR that adds two helper functions (appUserProfileToMetadata and appGroupAssignmentProfileToMetadata) to extract Okta app user/group profile attributes and attach them as grant metadata. The approach is sound and the code is easy to follow.

What looks good

  • Nil checks on appUser, profile, and empty map are thorough
  • The _ prefix convention for assignment-level fields (_scope, _status, _externalId) avoids collisions with profile keys
  • Conditional attachment (only when metadata is non-nil) keeps grants clean when profiles are empty
  • Consistent pattern between user and group assignment metadata extraction

Issues found

Panic risk with structpb.NewStruct (medium severity) — NewGrant panics (not returns error) if a GrantOption fails. WithGrantMetadata calls structpb.NewStruct(metadata) internally, which could fail on unconvertible values. While JSON-deserialized maps are typically safe, Okta app profiles are arbitrary and the failure mode is a process crash. I've left inline suggestions to pre-validate with structpb.NewStruct() before returning the metadata.

Missing structpb import — If the suggestions above are adopted, "google.golang.org/protobuf/types/known/structpb" will need to be added to the imports.

Minor observations (non-blocking)

  • The PR description mentions following the AWS mode pattern in aws_account.go, but that file doesn't use WithGrantMetadata — this is actually the first usage in this connector, so there's no existing pattern to compare against. Not a problem, just a documentation note.

Comment thread pkg/connector/app.go
Comment on lines +620 to +640
if len(profile) == 0 {
return nil
}

metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}

// Include scope and status from the app user assignment itself
if appUser.Scope != "" {
metadata["_scope"] = appUser.Scope
}
if appUser.Status != "" {
metadata["_status"] = appUser.Status
}
if appUser.ExternalId != "" {
metadata["_externalId"] = appUser.ExternalId
}

return metadata
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential panic risk: WithGrantMetadata calls structpb.NewStruct(metadata), and if that returns an error, NewGrant will panic (see grant.go:90). While JSON-deserialized profiles should contain only structpb-compatible types, Okta app profiles are arbitrary and could theoretically contain values that structpb can't handle (e.g., deeply nested structures or unexpected types from custom schema extensions).

Consider either:

  1. Pre-validating the metadata with structpb.NewStruct() before passing it, logging and skipping on error, or
  2. Converting all values to strings (safest for arbitrary profile data)

This is a low-probability issue since JSON-unmarshaled maps typically contain only compatible types, but since the failure mode is a panic (not a returned error), it's worth hardening.

Suggested change
if len(profile) == 0 {
return nil
}
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
// Include scope and status from the app user assignment itself
if appUser.Scope != "" {
metadata["_scope"] = appUser.Scope
}
if appUser.Status != "" {
metadata["_status"] = appUser.Status
}
if appUser.ExternalId != "" {
metadata["_externalId"] = appUser.ExternalId
}
return metadata
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
// Include scope and status from the app user assignment itself
if appUser.Scope != "" {
metadata["_scope"] = appUser.Scope
}
if appUser.Status != "" {
metadata["_status"] = appUser.Status
}
if appUser.ExternalId != "" {
metadata["_externalId"] = appUser.ExternalId
}
// Validate that metadata is structpb-compatible to avoid panic in NewGrant
if _, err := structpb.NewStruct(metadata); err != nil {
return nil
}
return metadata

Comment thread pkg/connector/app.go
Comment on lines +656 to +664
return nil
}

metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}

return metadata
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same panic risk as appUserProfileToMetadata — if the profile contains structpb-incompatible values, NewGrant will panic. Consider adding the same validation here.

Suggested change
return nil
}
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
return metadata
metadata := make(map[string]interface{})
for k, v := range profile {
metadata[k] = v
}
// Validate that metadata is structpb-compatible to avoid panic in NewGrant
if _, err := structpb.NewStruct(metadata); err != nil {
return nil
}
return metadata

Address review feedback: ensure profile metadata values are compatible
with structpb.NewStruct before passing to WithGrantMetadata. Adds
toStructpbCompatibleValue() to coerce non-primitive types to strings,
and validates the full metadata map with structpb.NewStruct, returning
nil if conversion would fail.
@c1-dev-bot c1-dev-bot Bot requested a review from a team March 12, 2026 16:35
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

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


PR Review: Sync app user profile attributes as grant metadata

  • Read PR diff and changed files
  • Review implementation for correctness and best practices
  • Post review findings

Summary

This PR adds two helper functions to extract Okta app user and group assignment profile attributes and attach them as grant metadata via WithGrantMetadata. The second commit (ead7cf5) addresses the structpb panic risk raised in the previous review by adding both value normalization (toStructpbCompatibleValue) and pre-validation with structpb.NewStruct().

What looks good

  • Thorough nil/empty checks — Both metadata extraction functions guard against nil objects, nil profiles, type assertion failures, and empty maps
  • Panic prevention — The toStructpbCompatibleValue helper normalizes values to structpb-compatible types, and the structpb.NewStruct() pre-validation acts as a belt-and-suspenders safety net
  • Clean integration — The grantOptions slice pattern is a nice refactor that makes the grant construction more extensible without changing behavior for cases where metadata is absent
  • _ prefix convention — Good approach to namespace assignment-level fields (_scope, _status, _externalId) separately from profile keys to avoid collisions
  • Recursive handlingtoStructpbCompatibleValue correctly handles nested []interface{} and map[string]interface{} types

Issues

1. Silent data loss on validation failure (low severity, non-blocking)

At app.go:643 and app.go:673, if structpb.NewStruct(metadata) fails, the entire metadata map is silently dropped. While this is safer than panicking, it means profile data could disappear without any indication in logs. Consider adding a debug-level log message before returning nil so operators can diagnose issues:

if _, err := structpb.NewStruct(metadata); err != nil {
    // Consider: logger.Debug("skipping invalid profile metadata", zap.Error(err))
    return nil
}

This is non-blocking since the functions don't currently have access to a logger, and adding one would change the function signatures. But it's worth considering for future observability.

2. Precision loss for large integer IDs (low severity, non-blocking)

In toStructpbCompatibleValue at app.go:692-698, int64 and uint64 values are converted to float64. This loses precision for integers larger than 2^53 (which is noted in CLAUDE.md's guidance about scientific notation). Since Okta profile fields could theoretically contain large numeric IDs, consider converting large integers to strings instead:

case int64:
    if val > 1<<53 || val < -(1<<53) {
        return strconv.FormatInt(val, 10)
    }
    return float64(val)

This is unlikely to be an issue in practice with Okta profiles, so non-blocking.

3. Double conversion overhead (observation, non-blocking)

The code first normalizes all values via toStructpbCompatibleValue, then validates the entire map with structpb.NewStruct(). The validation call effectively re-does the type checking. This is fine for correctness (defense in depth), but the structpb.NewStruct() call does allocate a full protobuf struct that's immediately discarded. For profiles with many fields, this is a minor inefficiency. Not worth changing given the safety benefit.

Minor observations

  • The structpb import is correctly added at line 21
  • The approach of building grantOptions as a slice and spreading with grantOptions... is idiomatic Go
  • Both listAppUsersGrants and listAppGroupGrants follow the same conditional attachment pattern, keeping the code consistent

Verdict

The PR is well-implemented and the second commit properly addresses the panic risk from the initial review. The remaining observations are all non-blocking. This looks good to merge after manual testing with an Okta instance confirms the metadata appears correctly.

The gosec linter no longer flags the ApiToken field name, so the
//nolint:gosec directive is unused and triggers a nolintlint error.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

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


PR Review: Sync app user profile attributes as grant metadata

  • Read PR diff and changed files
  • Review implementation for correctness, security, and best practices
  • Post review findings

Summary

This PR adds grant metadata for app user and group assignment profiles across 3 commits: the initial feature (1d27a3b), structpb validation to prevent panics (ead7cf5), and a nolint cleanup (4969bf2). The implementation is clean and well-structured.

What looks good

  • Thorough nil/empty guards — Both appUserProfileToMetadata and appGroupAssignmentProfileToMetadata check for nil objects, nil profiles, type assertion failures, and empty maps
  • Panic prevention is correct — I verified that NewGrant at vendor/.../grant/grant.go:90 does panic(err) when a GrantOption fails. The structpb.NewStruct() pre-validation at app.go:643 and app.go:673 ensures the metadata is safe before it reaches WithGrantMetadata, which is the right approach
  • _ prefix convention — Good namespacing for assignment-level fields (_scope, _status, _externalId) to avoid collisions with arbitrary profile keys
  • Conditional attachment — Only adds WithGrantMetadata when metadata is non-nil, keeping grants clean for empty profiles
  • toStructpbCompatibleValue is thorough — Handles recursive []interface{} and map[string]interface{} types, with a fmt.Sprintf fallback for unknown types
  • grantOptions slice pattern — Nice refactor that makes grant construction extensible without changing behavior when metadata is absent

Issues

1. Silent data loss on validation failure (low, non-blocking)

At app.go:643 and app.go:673, if structpb.NewStruct(metadata) fails, the entire metadata map is silently dropped. While safer than panicking, this means profile data could vanish with no indication. Since the functions don't have logger access, this is acceptable for now, but worth noting for future observability. A context-aware logger would help here.

2. Precision loss for large integer IDs (low, non-blocking)

In toStructpbCompatibleValue at app.go:692-699, int64 and uint64 values are converted to float64, which loses precision for integers > 2^53. Per CLAUDE.md guidance on large numeric IDs, this could theoretically affect Okta profile fields containing large numeric identifiers. In practice, JSON-deserialized numbers from Okta arrive as float64 already, so the int64/uint64 branches would only be hit if the Okta SDK explicitly typed them — unlikely but worth documenting.

3. Double allocation in validation (observation, non-blocking)

The code first normalizes values via toStructpbCompatibleValue, then allocates a full structpb.Struct for validation that's immediately discarded. This is fine for correctness (defense in depth) and the profile maps are small, so the overhead is negligible.

Minor observations

  • The baton_capabilities.json change is just whitespace normalization (removing extra spaces before colons) — no functional change
  • The connector.go change removes an unnecessary //nolint:gosec directive from ApiToken — clean hygiene fix
  • The structpb import is correctly added at line 21

Verdict

This is a solid, well-implemented PR. The panic prevention is correctly placed, the code is consistent between user and group paths, and the helper functions are well-guarded. The remaining observations are all non-blocking. Looks good to merge after manual testing with an Okta instance confirms metadata appears correctly.

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