Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`c1i entitlements create`.** Modelling a manually-managed app took three
raw `api` calls -- resource type, resource, then entitlement -- with the ids
hand-carried between them. One command now does it, and reuses objects you
already have: pass `--resource-type-id` to skip the first call, and
`--resource-id` as well to skip the second. `--owner-id` is set
inline rather than needing a follow-up call, and `--duration-grant` is a flag
rather than a hand-written body field.

On a partial failure nothing is rolled back; the error names what that run
created, the flags to re-run with, and the create-only flags that retry has
to drop, so the command it prints is one that works. `--dry-run` previews
all three requests.

Empty values are usage errors (exit 2) before anything is sent, rather than
silent fallbacks: `--owner-id ""` from an unset shell variable would have
created an ownerless entitlement at exit 0, and an empty
`--resource-type-display-name`/`--resource-display-name` would have quietly
reused `--display-name`. Only a `CUSTOM` resource type can repeat on one
app; the help now says so, quoting the server's own
`app resource type already exists`.

### Changed

- **Fixtures and command documentation now use placeholder identifiers**, and
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,54 @@ c1i accounts set-owner <app-user-id> --app-id <id> --user-id <id>
```sh
c1i entitlements list [--app-id <id>] [--query <text>] [--page-size N] [--page-token TOKEN] [--limit N]
c1i entitlements get <entitlement-id> --app-id <id>

# Create one on a manually-managed app, with its resource type and resource
c1i entitlements create --app-id <id> --display-name "Payroll admin" \
[--description <text>] [--slug member] [--alias payroll_admin] [--owner-id <user-id>] \
[--duration-grant 3600s] [--resource-type CUSTOM] \
[--resource-type-display-name "Payroll role"] [--resource-display-name "Payroll admins"]

# Reuse an existing resource type (or an existing resource) instead of creating one
c1i entitlements create --app-id <id> --display-name "Payroll viewer" --resource-type-id <id>
c1i entitlements create --app-id <id> --display-name "Payroll viewer (RO)" \
--resource-type-id <id> --resource-id <id>
```

An entitlement points at an app resource, which lives under an app resource
type, so `entitlements create` is up to three `POST`s: the resource type, the
resource, then the entitlement. `--resource-type-id` and `--resource-id` skip
whichever of the first two steps you already have — one resource type can carry
many resources, and one resource many entitlements. The server requires both
ids on the entitlement even though the OpenAPI schema marks only `displayName`
required, so `--resource-id` without `--resource-type-id` is rejected at exit
`2` before anything is sent.

`--resource-type` is `ROLE`, `GROUP`, `LICENSE`, `PROJECT`, `CATALOG`,
`CUSTOM`, `VAULT` or `PROFILE_TYPE` (case-insensitive here, uppercase on the
wire) and describes the resource type this command creates, so passing it
together with `--resource-type-id` is a usage error rather than a silently
ignored flag. Only `CUSTOM` can repeat on one app: a second resource type of
any other kind fails with a 500 (exit `6`, though retrying never helps) saying
`app resource type already exists`, so reuse the existing one with
`--resource-type-id` and drop both `--resource-type` and
`--resource-type-display-name` — either one alongside the id is a usage error.
Reusing a resource with `--resource-id` likewise means you drop
`--resource-display-name`. `--owner-id` is repeatable and goes inline in the create
request, so no follow-up call is needed; an empty one is a usage error rather
than an owner quietly dropped. `--duration-grant` takes a protobuf duration —
seconds with an `s` suffix, e.g. `3600s`; a Go-style `1h` is refused by the
server. Omit it for standing access.

`--dry-run` previews all three requests, printing
`NEW_APP_RESOURCE_TYPE_ID`/`NEW_APP_RESOURCE_ID` where an id only exists after
a real preceding step. There is no rollback: if a later step fails, the objects
the earlier ones created still exist, and the error names them along with the
flags that reuse them and the create-only flags the retry has to drop. The
created entitlement comes back as pretty JSON under `appEntitlementView`
(`--fields` is never applied to mutation output); it echoes
`appResourceTypeId`/`appResourceId` and expands both objects, so every id the
command touched is in that one payload.

### Grants ("who has access")

```sh
Expand Down
18 changes: 18 additions & 0 deletions cmd/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,20 @@ Two things are irreversible in ways their `--help` doesn't make obvious:
each toolset's app entitlement, is soft-deleted with it. Anyone whose
access came through one of those entitlements is affected.

One command sends more than one write: `entitlements create` POSTs a resource
type, a resource, then the entitlement, skipping the steps whose id you supply
via `--resource-type-id`/`--resource-id`. Its `--dry-run` previews all three.
There is no rollback, so a failure part-way through leaves the earlier objects
behind; the error names them, the flags that reuse them, and the create-only
flags the retry has to drop, and re-running without those flags creates
duplicates. Only a `CUSTOM` resource type can repeat on one app: a second
`--resource-type` of any other kind fails with a 500 (exit `6`, though
retrying never helps) saying `app resource type already exists` -- reuse the
existing type with `--resource-type-id` and drop both `--resource-type` and
`--resource-type-display-name`; either alongside the id is exit 2. Reusing a
resource with `--resource-id` likewise means you drop
`--resource-display-name`.

## Things that will surprise you

- A **repeatable** flag takes one value per occurrence; a comma is literal, not
Expand Down Expand Up @@ -316,6 +330,10 @@ Two things are irreversible in ways their `--help` doesn't make obvious:
- Entitlement ids are unique only within an app — some system-builtin
entitlements reuse the same id across every app that has one. Always key
on `(app_id, id)` together, never `id` alone.
- `POST /api/v1/apps/{app_id}/entitlements` requires `appResourceTypeId` and
`appResourceId` even though its OpenAPI schema lists only `displayName` as
required; omitting them 400s on the id regex. `entitlements create` handles
this for you.
- `mcp servers test-connection` returns `toolCount` as a JSON string, not a
number. The `tool_count` in NDJSON list rows is a real number.
- `mcp servers search` only includes `tool_count` when you pass
Expand Down
82 changes: 58 additions & 24 deletions cmd/docs_guide.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,10 @@ provisionerPolicy.delegated update is the actual provisioning trigger.
`

// guideConfigureNewApp walks through creating a manually-managed app
// container, setting its owners, and creating a custom entitlement for it
// via the 3-call resource-type/resource/entitlement sequence (no first-class
// "entitlements create" exists). Derived from cmd/apps_create.go,
// cmd/apps_set_owners.go, cmd/entitlements_get.go, cmd/entitlements_list.go,
// and cmd/api.go.
// container, setting its owners, and creating a custom entitlement for it.
// Derived from cmd/apps_create.go, cmd/apps_set_owners.go,
// cmd/entitlements_create.go, cmd/entitlements_get.go,
// cmd/entitlements_list.go, and cmd/api.go.
const guideConfigureNewApp = `# Configure a new app

Stand up an app container, assign the C1 users who administer it, and give it
Expand Down Expand Up @@ -412,32 +411,66 @@ here removes yourself. Use "apps add-owner" instead to add without replacing.

### 3. Create a custom entitlement to grant

There is no "entitlements create" — only "entitlements get"/"list". Creating
one for a manually-managed app is a 3-call sequence instead: a resource
type, a resource under it, then the entitlement pointing at both. No
first-class command covers this, so each call goes through "c1i api":
An entitlement points at an app resource, which lives under an app resource
type, so creating one is three POSTs. "entitlements create" sends all three:

c1i api --path=/api/v1/apps/$APP_ID/resource_types --body='{"displayName":"Payroll role","resourceType":"CUSTOM"}'
RT_ID=<appResourceType.id from the response>

c1i api --path=/api/v1/apps/$APP_ID/resource_types/$RT_ID/resources --body='{"displayName":"Payroll admin"}'
RES_ID=<appResource.id from the response>

c1i api --path=/api/v1/apps/$APP_ID/entitlements --body='{"displayName":"Payroll admin","slug":"member","alias":"payroll_admin","appResourceTypeId":"'$RT_ID'","appResourceId":"'$RES_ID'"}'
c1i entitlements create --app-id "$APP_ID" \
--display-name "Payroll admin" --resource-type-display-name "Payroll role" \
--slug member --alias payroll_admin --owner-id "$OWNER_USER_ID"
ENT_ID=<appEntitlementView.appEntitlement.id from the response>

resourceType is one of ROLE|GROUP|LICENSE|PROJECT|CATALOG|CUSTOM|VAULT|PROFILE_TYPE.
Omitting a duration defaults the entitlement to standing access
(durationUnset); pass a durationGrant field (e.g. 3600s) instead for
time-boxed access.
The response echoes the ids of the other two objects in that same payload:

RT_ID=<appEntitlementView.appEntitlement.appResourceTypeId from the response>
RES_ID=<appEntitlementView.appEntitlement.appResourceId from the response>

Reuse them for the next entitlement rather than minting a duplicate resource
type per entitlement:

c1i entitlements create --app-id "$APP_ID" --display-name "Payroll viewer" \
--resource-type-id "$RT_ID" # new resource under an existing type
c1i entitlements create --app-id "$APP_ID" --display-name "Payroll viewer (RO)" \
--resource-type-id "$RT_ID" --resource-id "$RES_ID" # entitlement only

--resource-type (the kind of resource type to create, default CUSTOM) is one
of ROLE, GROUP, LICENSE, PROJECT, CATALOG, CUSTOM, VAULT, PROFILE_TYPE. Only
CUSTOM can repeat on one app: a second resource type of any other kind fails
with a 500,
app resource type already exists
which maps to exit 6 even though retrying never helps. Reuse the one that
exists by passing --resource-type-id and drop both --resource-type and
--resource-type-display-name: each describes a type this command would create,
so either alongside the id is refused at exit 2. Reusing a resource with
--resource-id likewise means you drop --resource-display-name.

--owner-id is repeatable and rides along in the create request, so entitlement
owners need no follow-up call -- but the read lags the write the same way app
owners do (one measured create took 116s to show up on
"GET .../entitlements/$ENT_ID/ownerids"), so don't read that as a failure.

The three writes are not atomic and nothing is rolled back: if a later one
fails, the objects the earlier ones created still exist, and the error names
them, the flags that reuse them, and the flags to drop, e.g. "(already
created: re-run with --resource-type-id <id>, dropping
--resource-type-display-name, to reuse instead of duplicating)". Following it
verbatim is what makes the retry succeed: keeping a flag that describes an
object which now already exists is refused. "--dry-run" previews all three
requests.

Omitting --duration-grant defaults the entitlement to standing access
(durationUnset). For time-boxed access pass a protobuf duration -- seconds
with an "s" suffix, e.g. "--duration-grant 3600s"; a Go-style "1h" is refused
by the server with "invalid google.protobuf.Duration value".

## Verify

c1i entitlements list --app-id "$APP_ID"

Auto-paginates to completion; expect the builtin "Access" row plus your new
entitlement, both present immediately — the entitlement search index is not
lagged the way owners are.
entitlement. The entitlement search index is not lagged the way owners are --
three measured creates were listed within a second -- but it isn't
transactional either: a fourth was missing from a list issued immediately
after it. Re-run before concluding the create failed.

c1i entitlements get "$ENT_ID" --app-id "$APP_ID"

Expand Down Expand Up @@ -519,8 +552,9 @@ you'd take it back.

c1i auth whoami

- An app and an entitlement that already exist to request against. There is
no "entitlements create" — find real ones:
- An app and an entitlement that already exist to request against. Find real
ones — "entitlements create" only makes manually-managed ones, which is not
what this workflow is for (see "c1i docs guide configure-new-app"):

c1i apps list
APP_ID=<id of the target app>
Expand Down
Loading
Loading