Managing access
Access in TagShape is: permissions go on groups, people go in groups. There is nothing else.
Setting up a group
{
"name": "Readers",
"description": "Can see the library but not change it",
"permissions": [
"TagGroupList", "TagGroupRead",
"TagList", "TagRead",
"VariableList", "VariableRead"
]
}
Needs UserCreate, and the ability to grant each permission listed. See
granting versus holding.
Two checks run beyond the endpoint permission:
- Grant check — a permission being added that you cannot grant is rejected, and named in the response.
- Dependency check — a group carrying
AuditReadwithoutAuditListis refused. The permission would do nothing, and a group that silently does nothing is worse than a rejected save.
Restricting a group to specific library sets
The six Variable* permissions can each be narrowed to specific library sets instead of applying to every one
in the organization — see Library-set scoping for the full
model. Add a librarySetScopes entry alongside the permission it narrows:
{
"id": "…", "version": 3, "name": "Payments team",
"permissions": ["VariableList", "VariableRead", "VariableUpdate"],
"librarySetScopes": [
{ "permission": "VariableUpdate", "variableSetIds": [12, 47] }
]
}
Here the group can list and read every library set in the organization, but only update variables in sets
12 and 47. Leaving VariableUpdate out of librarySetScopes entirely — or omitting the field altogether —
means it applies everywhere, which is the default and unchanged behaviour.
Needs nothing beyond what granting the permission for every set already needs: whoever could put VariableUpdate
on a group without restriction could always have restricted it to one set. The same grant check and dependency
check above apply to the scope too - naming VariableUpdate for sets the group does not have VariableUpdate
for at all is refused, as is scoping a permission the group does not hold.
Two things worth knowing before narrowing a permission:
- Creating a library set is never scoped.
VariableCreaterestricted to specific sets still lets its holder create variables inside them, but a brand-new library set — root or nested — always needsVariableCreateheld for every set. - Batch import and export refuse a scoped grant outright. Either operation can touch any library set named
in the file, so
POST /importandGET /exportneed every relevantVariable*permission unrestricted.
Adding people
{ "userIds": ["9c1e7a4d-…", "3f52a18b-…"] }
Needs UserUpdate and no grant check — adding somebody to a group is not the same act as deciding what the
group may do. The same membership can also be set from the other side, through PUT /users/{id}/groups.
Changes take effect at the member's next token refresh, not immediately. With the default one-hour token that is at most an hour.
A worked set of groups
A shape that works for most organizations:
| Group | Permissions | For |
|---|---|---|
| Readers | TagGroupList, TagGroupRead, TagList, TagRead, VariableList, VariableRead | Anyone who needs to see the library |
| Editors | Readers + TagGroupCreate/Update/Delete, TagCreate/Update/Delete, VariableCreate/Update/Delete | People who maintain configuration |
| Secret readers | VariableRead, VariableReadSecrets | The few who need actual secret values |
| Importers | BatchProcessingImport | Pipeline accounts that apply a library file |
| Exporters | BatchProcessingExport | Accounts that pull a backup or a copy for another organization |
| Deployment | VariableRead, VariableReadSecrets | Accounts behind deployment API keys |
| Auditors | AuditList, AuditRead | Compliance and investigation |
| Administrators | UserList…UserDelete (also governs groups), PermissionsAll | People who manage access |
| Platform | PermissionsAllGlobal, GlobalOrganizationList…GlobalOrganizationDelete, AuditGlobalList, AuditGlobalRead | Whoever runs the installation |
Two things worth noticing.
Secret readers is separable from Editors. Writing a sensitive value needs only VariableUpdate, so the people
who maintain configuration need not be the people who can read the secrets in it.
Administrators holds PermissionsAll, not every permission. PermissionsAll lets them hand out any
non-global permission without their being able to use any of them. An administrator can put
VariableReadSecrets on a group without becoming a reader of secrets.
Global permissions
Six permissions reach beyond one organization, and only somebody holding PermissionsAllGlobal can hand any of
them out:
PermissionsAllGlobal, PermissionsImpersonateUser, AuditGlobalList, AuditGlobalRead, and the five
Organization* permissions.
Keep them to a single platform group with very few members.
Issuing API keys
A key carries the permissions of the account that owns it, so the way to scope a key is to scope its account.
- Create a service account for the consumer — not a person's account.
- Put it in a group holding only what the consumer needs.
- Make sure whoever will issue the key holds
UserCreateServiceAccountApiKey(which also needsUserRead). - That person opens the service account in the management UI and creates the key there, or calls
POST /users/{id}/apikeys.
A service account has no login, so it cannot create a key for itself — which is what
UserCreateServiceAccountApiKey is for. Keep it to the few people who may stand up machine credentials, since a
key acts as the account with whatever its groups grant.
For a key that never expires the person issuing it also needs ProfileNeverExpiresApiKey. Think twice: a key
that never expires is one that cannot be forgotten about safely, which is why it is a separate decision.
A person who wants a key for their own account is a different case: they hold ProfileCreateApiKey and make it
from their profile.
See API keys.
Reviewing access
| Question | Where |
|---|---|
| What can this person do? | GET /users/{id} — their groups and the permissions those carry |
| Who is in this group? | GET /groups/{id} |
| What keys does this account hold? | GET /users/{id}/apikeys — needs UserRead |
| Who changed a group, and when? | Audit log, entryType of Group |
| What permissions exist? | GET /permissions |
Removing access
| To remove | Do |
|---|---|
| One person's access | Take them out of the group, or delete the account (UserDelete) |
| A capability from everyone in a group | Take the permission off the group (UserUpdate) |
| A machine credential | DELETE /users/{id}/apikeys/{apiKeyId} (UserDelete) |
Deleting an account does not retroactively invalidate an already-minted JWT. A token remains valid until it
expires — at most an hour by default. Where that matters, shorten Jwt:DurationInMinutes.
Common mistakes
Granting AuditRead alone. It depends on AuditList and does nothing without it. The group save will refuse
it.
Granting VariableReadSecrets alone. Same reason — it depends on VariableRead.
Expecting PermissionsAll to let somebody do things. It governs granting, not doing. Somebody with only
PermissionsAll cannot read a single variable.
Expecting a permission change to take effect immediately. It reaches the caller at their next token refresh.
Expecting a library-set scope to reach a set's children. Library sets can nest, but a scope naming a parent set does not extend to sets beneath it — each one that should be covered has to be named.
Scoping VariableCreate and expecting to create a new library set with it. A scoped grant only reaches
creating a variable inside a set already in scope. Creating the set itself always needs VariableCreate
unrestricted.