feat: add support for user attributes#887
Conversation
|
Claude finished @refiito's task in 3m 1s —— View job Code Review
1.
// Current — insufficient
if (typeof item !== 'object' || item === null || !('key' in item) || !('value' in item)) {
// Fix — add type check
if (typeof item !== 'object' || item === null || typeof (item as any).key !== 'string' || !('value' in item)) {2.
3. this.log(`Attribute${keys.length === 1 ? '' : 's'} removed successfully for user ${userId}.`)
4. Only the |
📦 Bundle Stats —
|
| Metric | Value | vs main (512b79e) |
|---|---|---|
| Internal (raw) | 2.1 KB | - |
| Internal (gzip) | 799 B | - |
| Bundled (raw) | 10.95 MB | - |
| Bundled (gzip) | 2.06 MB | - |
| Import time | 845ms | +13ms, +1.5% |
bin:sanity
| Metric | Value | vs main (512b79e) |
|---|---|---|
| Internal (raw) | 975 B | - |
| Internal (gzip) | 460 B | - |
| Bundled (raw) | 9.84 MB | - |
| Bundled (gzip) | 1.77 MB | - |
| Import time | 2.04s | +56ms, +2.8% |
🗺️ View treemap · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
📦 Bundle Stats — @sanity/cli-core
Compared against main (512b79e1)
| Metric | Value | vs main (512b79e) |
|---|---|---|
| Internal (raw) | 92.3 KB | - |
| Internal (gzip) | 21.6 KB | - |
| Bundled (raw) | 21.53 MB | - |
| Bundled (gzip) | 3.41 MB | - |
| Import time | 815ms | +20ms, +2.6% |
🗺️ View treemap · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
📦 Bundle Stats — create-sanity
Compared against main (512b79e1)
| Metric | Value | vs main (512b79e) |
|---|---|---|
| Internal (raw) | 976 B | - |
| Internal (gzip) | 507 B | - |
| Bundled (raw) | 50.7 KB | - |
| Bundled (gzip) | 12.6 KB | - |
| Import time | ❌ ChildProcess denied: node | - |
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
Coverage Delta
Comparing 11 changed files against main @ Overall Coverage
|
- Add pagination notice when definitions list results are truncated (hasMore) - Move array validation outside the JSON.parse try/catch in set.ts to avoid fragile error re-throw - Extract duplicated formatValue helper to shared util/formatAttributeValue.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add per-item validation in set.ts to catch missing key/value fields early - Remove redundant `as UserAttribute[]` casts (already typed correctly via response interfaces) - Remove now-unused UserAttribute import from list.ts and set.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cngonzalez
left a comment
There was a problem hiding this comment.
Functionally looks pretty good! Most comments are nits about style and guidelines
| /** | ||
| * Get the authenticated user's own attributes within an organization | ||
| */ | ||
| export async function getMyAttributes(orgId: string): Promise<UserAttributesGetResponse> { |
There was a problem hiding this comment.
"My" is a little idiosyncratic in this repo -- maybe getCliUserAttributes (to match the getCliUser function in user.ts
There was a problem hiding this comment.
Renamed to getCliUserAttributes.
| @@ -0,0 +1,56 @@ | |||
| export type AttributeType = | |||
There was a problem hiding this comment.
I'm a bit confused on why this and the constants file are in the actions directory when there's no corresponding actions. Should they be moved to commands where they're actually used? Or do we foresee a need for actions down the line (multi-step processes etc)
There was a problem hiding this comment.
Good point — there were no action files. I inlined the types and the USER_ATTRIBUTES_API_VERSION constant into services/userAttributes.ts, which matches the pattern in services/organizations.ts (types live in the service file that owns them). The actions/userAttributes/ directory is gone.
|
|
||
| export type AttributeValue = (number | string)[] | boolean | number | string | ||
|
|
||
| export interface UserAttributeValues { |
There was a problem hiding this comment.
It might be worth making a comment on this type -- I'm a little confused because it seems to be used as the value of an attribute as part of UserAttribute, which has an activeSource that indicates if it's saml or sanity but there's a possibility for an attribute to have a value that is not in line with with the activeSource on the attribute? Is it shared or updated perhaps?
There was a problem hiding this comment.
Added a JSDoc clarifying the distinction:
Raw per-source values for a single attribute. Each entry holds the value received from that source (e.g. asserted in a SAML assertion, or set explicitly through Sanity). Both may be present at once; the value the API and access rules use is
UserAttribute.activeValue, picked according toUserAttribute.activeSource.
To answer the question directly: yes, both saml and sanity can hold values simultaneously, which can differ. The resolved value is on the outer UserAttribute (activeSource + activeValue); values is the raw per-source state.
| (baseDescription ?? 'Organization ID to use') + (isOverride ? OVERRIDE_SUFFIX : '') | ||
|
|
||
| return { | ||
| 'org-id': Flags.string({ |
There was a problem hiding this comment.
I'm not the final say on this, but I wanted to point out that elsewhere the CLI uses flags to specify an org, it's --organization (like in init and projects create). We may want to be consistent with the rest of the CLI
There was a problem hiding this comment.
Renamed to --organization everywhere: helper is now getOrganizationFlag, flag is --organization (kept the -o short form). All 5 callers + their tests updated. Verified getOrgIdFlag had no consumers outside this PR.
For now --organization accepts an org ID only (matching the API). projects create accepts slug-or-id; we can extend ours similarly in a follow-up if the API gains slug support.
| export class UserAttributeDefinitionsCreateCommand extends SanityCommand< | ||
| typeof UserAttributeDefinitionsCreateCommand | ||
| > { | ||
| static override description = 'Create an attribute definition for an organization' |
There was a problem hiding this comment.
Would "user attribute definition" be clearer here? I'm on the fence since the command is sanity user attribute definition create.
There was a problem hiding this comment.
Updated all three definitions command descriptions (create, list, delete) to use "user attribute definition", and the corresponding success/info log lines too.
| > { | ||
| static override description = 'Create an attribute definition for an organization' | ||
|
|
||
| static override examples = [ |
There was a problem hiding this comment.
Maybe not required for every command, but it might be nice to provide examples that illustrate that the organization ID is not needed for these commands, since we're prompting for it.
(I think we also have a style guide rule for progressive complexity which you're doing a great job of here! Just think the "no org" example could be nice)
There was a problem hiding this comment.
Added no-org examples to all three definitions commands (create, list, delete) — they show that running without --organization will prompt for one in interactive mode.
| } | ||
|
|
||
| if (outputJson) { | ||
| this.log(JSON.stringify(result, null, 2)) |
There was a problem hiding this comment.
There is a colorizeJson helper available that might be nice for consistency
There was a problem hiding this comment.
Switched all four --json output paths to colorizeJson (definitions list/create and attributes list/set). Color is only emitted in interactive TTY, so --json | jq still parses fine.
|
|
||
| if (result.hasMore) { | ||
| this.log( | ||
| '\nNote: Results are truncated. Use --json and the API directly with a cursor to fetch more.', |
There was a problem hiding this comment.
Would it make sense for the user to run another CLI command to get a cursor or just hit the API directly (assuming it already gives a cursor?) It might also be nice to give them the URL to hit, if they need a token, etc.
There was a problem hiding this comment.
Leaving the truncation note as-is for this PR — the User Attributes API is still in preview (vX) and the pagination contract may change. Once it's stable I'll come back and either thread the cursor through the CLI or document the API URL + token guidance in the help text, in a follow-up. Filed as a TODO for the User Attributes Followups project.
|
|
||
| static override examples = [ | ||
| { | ||
| command: '<%= config.bin %> <%= command.id %> --org-id o123 location', |
There was a problem hiding this comment.
Generally, examples tend to position arguments before flags (though technically I think OCLIF allows either)
There was a problem hiding this comment.
Reordered: example is now sanity users attributes definitions delete location --organization o123 (positional arg first). Also added a no-org variant.
| }, | ||
| { | ||
| command: | ||
| '<%= config.bin %> <%= command.id %> --org-id o123 --user-id u456 --attributes \'[{"key":"location","value":"UK"},{"key":"year_started","value":2020}]\'', |
There was a problem hiding this comment.
I'm not sure if we have a standard or recommendations about passing JSON on the command line. @binoy14 might have more thoughts about this or has encountered this before.
There was a problem hiding this comment.
Leaving the JSON-on-CLI shape as-is in this PR — happy to revise if @binoy14 has a stronger convention. Current shape is a JSON-array string passed to --attributes, which matches what we already do for other multi-value structured flags. Open to either: (1) a documented JSON-array stays, (2) per-attribute flag pairs like --key location --value UK (repeatable), (3) a --attributes-file for larger payloads. Tracking as a separate decision rather than blocking this PR.
Reviewer feedback addressed: - set.ts: validate that each attribute item has a string `key`, not just any present `key` field (Claude bot) - unset.ts: use `result.sanityUserId` from the API response in the success message instead of the raw user-id flag, mirroring set.ts (Claude bot) - Rename `--org-id` to `--organization` across all user-attributes commands and the shared `getOrganizationFlag` helper, for consistency with `init` and `projects create` (cngonzalez) - Rename `getMyAttributes` to `getCliUserAttributes`, matching the `getCliUser` naming in services/user.ts (cngonzalez) - Inline the user-attributes types and API-version constant into services/userAttributes.ts, matching the pattern in services/organizations.ts. Drops the misleading actions/userAttributes/ directory (cngonzalez) - Add JSDoc on UserAttributeValues explaining how the per-source `saml`/ `sanity` entries relate to the resolved `activeSource`/`activeValue` on UserAttribute (cngonzalez) - Use `colorizeJson` from cli-core for the `--json` output paths in list, set, definitions/list and definitions/create (cngonzalez) - Reword command descriptions to "user attribute definition" so they match the command path (cngonzalez) - Add "no --organization" examples to create/list/delete definitions commands, showing the interactive-prompt case (cngonzalez) - definitions/delete example: place positional arg before flags (cngonzalez) - Add unit tests for promptForOrganization (Claude bot — 6.7% coverage) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round 3 of review feedback addressed in b9c5b4c. Summary of disposition on the Claude bot review: 1. Plus all of @cngonzalez's inline nits — see individual thread replies. |
Add CLI support for User Attributes API
What
Adds CLI commands for managing User Attributes — the foundation for parameterised role-based access control. User attributes are key-value pairs (e.g. location="UK", departments=["hr","sales"]) that can be referenced in role definitions and access filters.
Commands
Attribute definitions (org-level schema — requires sanity.organization.manage):
User attribute values (list defaults to your own user; set/unset require sanity.organization.manage):
When --org-id is omitted in an interactive terminal, the CLI prompts for an organization selection.
Testing
pnpm test packages/@sanity/cli/src/commands/users/attributesNote
Medium Risk
New org-scoped commands can change user metadata and attribute schemas (access-control related), though behavior is gated by API permissions and the surface is preview (
vX) with validation and tests.Overview
Adds CLI support for the preview User Attributes API (
vX), enabling org admins to manage attribute schemas and per-user values used for parameterized access control.New
sanity users attributescommands cover list / set / unset on user values (with--user-id, JSON--attributesvalidation, and table or--jsonoutput) anddefinitions list / create / deletefor org-level keys and types. A shareduserAttributesservice wraps the HTTP client;promptForOrganizationplus a reusable--organization/-oflag (getOrganizationFlag) supply the org when omitted in interactive mode.Ships broad nock-based command tests, a small
formatAttributeValuehelper, a minor@sanity/clichangeset, and a trivial test import order fix indecorateIndexWithStagingScript.test.ts.Reviewed by Cursor Bugbot for commit 8547a24. Bugbot is set up for automated code reviews on this repo. Configure here.