Access control through the REST API¶
The roles and permissions and the
projects pages describe the model in the web interface. This page gives
the limits, the detailed rules and the REST API calls (users.manage permission for the roles,
the roles and projects of the users, the members of the projects and the OIDC mappings).
Limits¶
| Element | Limit |
|---|---|
| Roles created by the administrators | 100 at most |
| Roles of a user | 100 at most |
| Projects of a user | 200 at most; beyond that (with the OIDC mappings), the account has no access |
| Members set at once on a project | 500 at most |
| OIDC mappings | 500 at most, one per value of the claim (200 characters at most), each with 100 roles and 50 projects at most |
A role only holds permissions on the whole Controller server: a role with reports.read,
reports.write or findings.triage is refused (422). These per-project permissions are given with each project
of a user (projects[].permissions, at least one) and with every project
(all_projects_permissions). Only a known project may be given (404 otherwise).
users.manage also covers the OIDC settings, since they decide who gets which role. The
admin role, with the reading, the writing and the triage of every project, is also given by a mapping
created at the login of a user who holds a value of SECREVIEW_OIDC_ADMIN_GROUPS
(OIDC) while no mapping names it; a mapping that already names this value is left as
an administrator changed it, a deleted one is created again. The values of the roles claim that no mapping names are ignored.
Roles and users¶
# A role, then a user with it, who reads "billing" and reads and triages "crm"
curl -X POST https://secreview.example.com/api/v1/roles \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Auditor", "description": "", "permissions": ["audit.read"]}'
curl -X POST https://secreview.example.com/api/v1/users \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"username": "alice", "role_ids": [2], "projects": [{"project": "billing"}, {"project": "crm", "permissions": ["reports.read", "findings.triage"]}]}'
# The reading of every project for account 7 (its roles and its projects are kept)
curl -X PUT https://secreview.example.com/api/v1/users/7 \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"all_projects_permissions": ["reports.read"]}'
| Route | Effect |
|---|---|
GET /api/v1/permissions |
Permissions a role may hold, and whether they apply per project |
GET, POST /api/v1/roles; PUT, DELETE /api/v1/roles/{id} |
Roles, with the number of users and OIDC mappings that give each one |
POST /api/v1/users |
Creates a user with its roles (role_ids), its per-project permissions on every project (all_projects_permissions) and its projects (projects, each with its permissions, reading by default) |
PUT /api/v1/users/{id} |
Replaces the roles, the per-project permissions on every project or the projects of a user; a missing field is left unchanged, those of its OIDC mappings are kept |
GET /api/v1/access/projects?search=&limit= |
Known projects whose name contains search (every project, also those the caller does not read), 200 at most |
GET, PUT /api/v1/access/projects/{project}/members |
Users who have a project, each with its permissions given there and its all_projects_permissions held on every project, by the user or its OIDC mappings (read only; a user who only has every project has empty permissions and does not count in the version of the list nor in the PUT) |
GET, PUT /api/v1/oidc/mappings |
OIDC mappings, replaced all at once (OIDC) |
An unknown field (such as the former group_ids) is refused (422). The built-in roles
reader and admin can be neither changed nor deleted; deleting another role removes it from
the users and the OIDC mappings that give it (a role used by a token cannot be deleted).
curl -X PUT https://secreview.example.com/api/v1/access/projects/billing/members \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"members": [{"user_id": 7}, {"user_id": 9, "permissions": ["reports.read", "findings.triage"]}]}'
The GET of the members of a project and of the OIDC mappings returns the version of the list in
the ETag header. Send it back in If-Match with the PUT that replaces the list: when another
administrator changed it since, the PUT is refused (412) instead of silently undoing that
change; reload the list and try again. Without If-Match (or with *), the list is replaced
whatever it holds. The web interface always sends it.
etag=$(curl -s -o /dev/null -D - https://secreview.example.com/api/v1/oidc/mappings \
-H "Authorization: Bearer $ADMIN_TOKEN" | awk 'tolower($1) == "etag:" {print $2}' | tr -d '\r')
curl -X PUT https://secreview.example.com/api/v1/oidc/mappings \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-H "If-Match: $etag" -d '{"mappings": [{"claim": "secops", "role_ids": [2]}]}'
Every change is written to the audit log (role.*, user.roles_changed,
user.projects_changed, oidc.mappings_changed). From the CLI,
security-review-controller user set-access changes the roles and projects of an account, and
security-review-controller role list / role mappings lists the roles and the OIDC mappings
(see CLI).
Permissions of the caller¶
GET /api/v1/me returns the permissions of the caller, each with its projects (null: every
project), and role is always user for a web session (the role of the token otherwise). A
user who only manages the tokens and the standards reads no project. A permission held also
gives the ones it includes: rules.manage gives rules.read, standards.manage gives
standards.read. A web session never passes
for an admin token, even with every permission: the routes reserved to tokens (/metrics, the
pipeline scans) refuse it.
{"client": "alice", "role": "user", "projects": null,
"permissions": {"reports.read": null, "findings.triage": ["crm"]}, …}
An account without any permission cannot log in, and its open sessions stop. Removing the last
holder of users.manage (deleting its account, its role, the permission of its role or the
OIDC mapping that gives it) is refused with 409 or 422. An OIDC login never removes it: the
values of the claim that gave it are kept, with a warning in the logs and a
user.roles_changed event.
Projects¶
Known projects¶
A project is known once it has a row in the database: created by its first report, a token that reaches it, an approval or a creation in the web interface. A pending request does not make a project known.
Reading of a requested project¶
The approval of a project requested by an agent
(enrollment) adds
it to the projects of the requesting user with reports.read and reports.write, never more (creating the project when new; for an
existing project, the request is an access request, which only a user holding users.manage
approves), and creating a project adds it to its creator the same way. See also
project read access.
Creating a project¶
POST /api/v1/projects creates a project, from a session of the web interface only. The only
token that creates a project is the personal MCP token of a user holding projects.create,
through the request_project tool of its agent. The Controller server adds the project to the
account with reports.read and reports.write.
curl -X POST -H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" \
https://secreview.example.com/api/v1/projects -d '{"project": "mobile-app"}'
# {"project": "mobile-app", "reports_total": 0, "last_report_at": null, …}
A known project is refused (409). A project still pending is not known yet: creating it
settles the creator's own request (the requests of other users become access requests). Each
creation is written to the audit log (project.created).
Autojoin of a project¶
A user holding projects.autojoin joins an existing project through the request_project
tool of its agent: the Controller server adds the project to the account with reports.read
and reports.write, settles its pending request of the project and writes project.joined to the audit log. A
user who already reads 200 projects is refused. PUT /api/v1/projects/{project}/autojoin
blocks (true) or allows again (false) this autojoin, with users.manage, from a session
of the web interface only, for a project the caller reads (another one is 404). A blocked
project shows "autojoin_blocked": true in GET /api/v1/projects; the users who already
joined it keep their access. Each change is written to the audit log
(project.autojoin_changed).
curl -X PUT -H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" \
https://secreview.example.com/api/v1/projects/mobile-app/autojoin -d '{"blocked": true}'
# {"project": "mobile-app", "autojoin_blocked": true}
Deleting a project¶
DELETE /api/v1/projects/{project} deletes a project, from a session of the web interface only:
a token never deletes a project. The deletion runs in one transaction:
- its reports, with their findings and the comments of those findings;
- the project in the projects of the users;
- the project in the OIDC mappings: a mapping left without project is deleted (never left reaching every project);
- the project in the tokens: a token that reaches no other project is revoked (never left reaching every project), the others keep their other projects.
curl -X DELETE -H "Authorization: Bearer $SESSION" \
https://secreview.example.com/api/v1/projects/mobile-app
# {"project": "mobile-app", "reports": 3, "findings": 12, "members_removed": 2, "mappings_changed": [], …}
The audit log keeps the events of the project and records project.deleted, with the counts in
its details; the OIDC mappings and the tokens changed or deleted have their own events
(oidc.mappings_changed, token.revoked, token.projects_changed). An unknown project gives
404.