Key points
- Effective permissions are the authoritative answer; grants and roles explain how it was reached.
inheritdecides whether a grant covers one resource or everything nested under it — the setting most often wrong when access is broader than intended.- Granting requires owner-level access on the resource, not the role being granted.
- A resource-use check answers 200 with
allowed: falsefor a denial; a 403 means the caller may not even ask. - An authorization team comes from the identity provider; a NopsAI team path is ownership. Granting to one does not change the other.
- When authorization is unavailable these routes fail closed with 503 rather than guessing.
Operations
GET/v1/access/grantsAuthorized
Lists access grants on a resource or for a subject.
Notes
Grants explain an answer; they are not the answer. Use effective permissions to find out what a caller may actually do.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
resource_type | query | string | Optional | Restrict to one resource kind. |
resource_id | query | string | Optional | Restrict to one resource. Requires resource_type. |
subject_type | query | string | Optional | Restrict to one kind of subject.Allowed: user, service_account, auth_team |
subject_id | query | string | Optional | Restrict to one subject. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" \
"$NOPSAI_URL/v1/access/grants?resource_type=pipeline&resource_id=platform/release-service" | jqResponses
Grants matching the filters.
[{
"id": "a41c9d02-6f38-4b1d-9c77-51e3a0b4f912",
"subject_type": "auth_team",
"subject_id": "platform-sre",
"subject_display": "Platform SRE",
"role": "pipeline-operator",
"resource_type": "pipeline",
"resource_id": "platform/release-service",
"inherit": true
}]When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A filter combination that cannot be resolved, such as resource_id without resource_type. | Supply both halves of a resource filter. |
| 401 | No usable caller identity. | Send a token. |
| 403 | The caller may not read grants on this resource. | Reading who has access is itself an access decision. |
| 404 | The named resource does not exist. | Confirm the resource id. |
| 503 | Authorization is unavailable. | Check AAA. |
| 500 | The grant query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/access_grants_test.goservices/nopsai/access_grants.goservices/nopsai/access_grants_handlers.go
POST/v1/access/grantsAuthorized
Grants a subject a role on a resource.
Notes
inherit is the difference between "this pipeline" and "this pipeline and everything under it". It is the setting most often wrong when access is broader than intended.
Call it
curl -sX POST "$NOPSAI_URL/v1/access/grants" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject_type":"auth_team","subject_id":"platform-sre","role":"pipeline-operator","resource_type":"pipeline","resource_id":"platform/release-service","inherit":true}' | jqResponses
Grant created.
{
"id": "a41c9d02-6f38-4b1d-9c77-51e3a0b4f912",
"subject_type": "auth_team",
"subject_id": "platform-sre",
"subject_display": "Platform SRE",
"role": "pipeline-operator",
"resource_type": "pipeline",
"resource_id": "platform/release-service",
"inherit": true
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | An unknown role, an unusable subject, or a malformed resource reference. | The message names the field. |
| 401 | No usable caller identity. | Send a token. |
| 403 | The caller may not grant access on this resource. | Granting requires owner-level access, not the role being granted. |
| 404 | The subject or the resource does not exist. | Confirm both before retrying. |
| 409 | The same grant already exists. | Read the grant list; re-granting is a no-op rather than an update. |
| 503 | Authorization is unavailable. | Check AAA. Grants fail closed. |
Side effects
- Changes what the subject can do immediately.
- Writes an audit record naming the granter and the grant.
Proven by
services/nopsai/access_grants_test.goservices/nopsai/aaa_integration_test.goservices/nopsai/access_grants.go
DELETE/v1/access/grants/{grantID}Authorized
Revokes an access grant.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
grantID | path | uuid | Required | Grant identifier from the list route. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/access/grants/$GRANT_ID" -w "%{http_code}\n"Responses
Grant revoked.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The id is malformed. | Use the id from the grant list. |
| 403 | The caller may not revoke grants on this resource. | Revoking requires owner-level access. |
| 404 | No grant with that id. | It may already be revoked. |
| 503 | Authorization is unavailable. | Check AAA. |
Side effects
- Removes the access immediately.
- Writes an audit record.
Proven by
services/nopsai/access_grants_test.goservices/nopsai/access_grants.go
GET/v1/access/effective-permissionsAuthenticated
Resolves what the caller may actually do, after roles, grants, and inheritance.
Notes
This is the route to trust when a caller insists they should have access. Roles and grants explain the answer; this is the answer.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
action | query | string | Optional | Narrow to one AAA action. |
resource_type | query | string | Optional | Narrow to one resource kind. |
resource_id | query | string | Optional | Narrow to one resource. |
team_path | query | string | Optional | Evaluate within one team path. run_team_path is accepted as an alias. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" \
"$NOPSAI_URL/v1/access/effective-permissions?action=pipeline.run&resource_type=pipeline&resource_id=platform/release-service" | jqResponses
Effective permissions for the caller, narrowed by whatever filters were supplied.
{
"caller_type": "user",
"caller_id": "admin",
"allowed": true
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | A filter cannot be parsed, or a resource filter is incomplete. | Supply resource_type alongside resource_id. |
| 401 | No usable caller identity. | Send a token. |
| 503 | Authorization is unavailable. | Check AAA rather than assuming denial. |
Side effects
- None.
Proven by
services/nopsai/aaa_integration_test.goservices/nopsai/access_grants_resolve.go
GET/v1/access/teamsAuthenticated
Lists the teams the caller can act within.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/access/teams" | jqResponses
Teams available to the caller.
[
{ "id": 12, "path": "platform/payments", "valid": true }
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 401 | No usable caller identity. | Send a token. |
| 503 | Authorization is unavailable. | Check AAA. |
| 500 | The team query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/aaa_integration_test.goservices/nopsai/resource_authz.go
GET/v1/access/auth-teamsAuthenticated
Lists the identity-provider teams that can appear as grant subjects.
Notes
An authorization team is a group from the identity provider. A NopsAI team path is ownership. Granting to the first does not change the second.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/access/auth-teams" | jqResponses
Authorization teams known to the platform.
[
{ "id": "platform-sre", "name": "Platform SRE" }
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 401 | No usable caller identity. | Send a token. |
| 500 | The query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/aaa_integration_test.goservices/nopsai/access_grants_handlers.go
POST/v1/authz/resource-use/checkAuthenticated
Checks whether a caller may use one resource in a given context.
Notes
Read allowed rather than the status code: a permitted question about a forbidden resource still answers 200.
Call it
curl -sX POST "$NOPSAI_URL/v1/authz/resource-use/check" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action":"model.use","resource_type":"llm_profile","resource_id":"reasoning-large"}' | jqResponses
The decision. A denial is a 200 with allowed: false, not a 403.
{
"allowed": false,
"action": "model.use",
"resource_type": "llm_profile",
"resource_id": "reasoning-large"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The body is malformed or names no action. | Send action, resource_type, and resource_id. |
| 403 | The caller may not ask about this resource. | Asking about a resource is itself an access decision. |
Side effects
- None. A check never grants or records access.
Proven by
services/nopsai/resource_authz_test.goservices/nopsai/resource_authz.go
POST/v1/authz/resource-use/batch-checkAuthenticated
Checks many resource uses in one request.
Notes
This is what the pipeline editor calls before a save, so an author sees "you cannot use this model" while editing rather than when the run fails.
Call it
curl -sX POST "$NOPSAI_URL/v1/authz/resource-use/batch-check" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @checks.json | jqResponses
A decision per check.
[
{ "allowed": true, "action": "model.use", "resource_type": "llm_profile", "resource_id": "reasoning-large" },
{ "allowed": false, "action": "mcp.use", "resource_type": "mcp_profile", "resource_id": "jira-readonly" }
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The body is malformed or empty. | Send a non-empty list of checks. |
| 403 | The caller may not ask about one of the resources. | The whole batch fails rather than silently dropping an entry. |
Side effects
- None.
Proven by
services/nopsai/resource_authz_test.goservices/nopsai/resource_authz.go
ANY/v1/resources/...Authorized
Reads settings, changes visibility, and adds or removes use grants for a concrete resource.
Notes
Registered as a bare path prefix rather than a method-qualified route, which is why the generated CLI catalogue cannot see it. Handler-authorized: the middleware defers, then the handler resolves the concrete resource and checks owner-level manage access.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/resources/llm_profile/reasoning-large" | jqResponses
Resource access settings.
{
"resource_type": "llm_profile",
"resource_id": "reasoning-large",
"visibility": "team"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 404 | The path does not resolve to a known resource. | Check the resource type segment. |
| 405 | A method the resource surface does not support. | Read the resource first to see what it offers. |
Side effects
- Mutating variants change visibility or use grants and write an audit record.
Proven by
services/nopsai/resource_authz_test.goservices/nopsai/resource_authz.goservices/nopsai/routes.go
How it works
Debug access in one order: read effective permissions for the caller, and only then read the grants that produced the answer. Reconstructing the answer from roles and grants by hand is how people convince themselves the platform is wrong.
The batch check exists so an editor can warn while a pipeline is being written rather than when the run fails. It asks about every resource the pipeline would touch — the model, the MCP profiles, the scope — in one request.
Reading who has access is itself an access decision, which is why the list route can answer 403. That is deliberate: an access list is a map of who to compromise.
Implementation evidence
services/nopsai/access_grants.goGrant documents and handlers.
services/nopsai/resource_authz.goResource-use checks and the resource access surface.
doc/access-control.mdAAA service, product roles, and route authorization.

