Skip to content
English
  • There are no suggestions because the search field is empty.

08.03 Permissions and the API

API permissions are SimpleRisk user permissions. The API key inherits the user's role, direct grants, admin flag, and team membership. To restrict an integration's capabilities, restrict the user's permissions; to broaden them, broaden the user's. There's no separate API permission model.

Why this matters

Most production integration mistakes trace to a permission misunderstanding. The integration submits a risk and gets a 403; the developer thinks the API is broken; the actual cause is the integration user lacks submit_risks. The integration reads risks and gets back an empty list; the developer thinks something's wrong; the actual cause is team-membership filtering. Understanding how permissions interact with API requests prevents most of these wrong-track debugging sessions.

The model itself is simple: API permissions are user permissions. The key inherits everything the user has — role, direct grants, admin flag, team memberships. There's no separate "API can do X but not Y" layer. Restricting the integration means restricting the user account behind the key; broadening it means broadening the user.

This article covers how this plays out in practice. For the broader permission model, see The Permission Model.

How API requests resolve permissions

When a request hits an authenticated endpoint:

  1. The API key is extracted and the user identified (see Authentication and API Keys).
  2. The user's effective permissions are loaded — the union of role-derived permissions, direct per-user grants, and admin override.
  3. The user's team memberships are loaded.
  4. The endpoint handler executes the operation as that user. Permission checks and team filters apply exactly as they would in the browser.

The same code path runs whether the user came in via the web UI or the API. There's no separate "API permission" branch.

What this means in practice

The integration user's role is the integration's role

If you create a user slack-bot with the role "Risk Submitter," the API integration acting as that user can do exactly what a Risk Submitter can do — submit risks, view risks, but not modify configuration or other users. Want the integration to be able to close risks too? Either add the close-risks permission to the Risk Submitter role (affecting every Risk Submitter, not just the integration) or grant the permission directly to the slack-bot user.

The pattern: create one SimpleRisk user per integration, assign the role that matches the integration's needs, add per-user permission grants for any integration-specific capabilities the role doesn't cover.

Team membership filters the integration's view

A user on the Engineering team only sees Engineering's records. The API enforces this — GET /api/v2/risks returns risks visible to the user, not all risks in the database.

If your integration needs to see risks across multiple teams (e.g., a centralized dashboard), the integration user needs team memberships covering those teams. For "see everything" integrations, either:

  • Add the user to every team, OR
  • Set the user's admin flag to 1 (which bypasses team filtering entirely, but is a much broader grant).

The admin-flag approach is convenient but dangerous — the integration can do anything in SimpleRisk, including modify other users' records and change critical configuration. Prefer adding the user to specific teams when possible.

Permission-denied responses

When the user lacks permission for an operation:

  • 403 Forbidden — the standard response for permission denial.
  • The response body includes a status_message indicating the denial.

The 403 isn't an authentication failure — the key is valid; the user just can't do the requested operation. Check the user's permissions in SimpleRisk's User Management UI.

Some refusals answer 401 Unauthorized instead, and three different situations produce one. Only the first is about your key:

  • "Unauthenticated Access. Please log in or provide a key to use the SimpleRisk API.": No key was sent, or SimpleRisk doesn't recognize the one that was. Re-authenticating is the right response.
  • "You don't have permission for this action.": The key is valid. The account failed the general access check that gates the application as a whole, which usually means the account is disabled or has lost its access permission.
  • "Unauthorized Access. The authenticated user does not have proper permissions.": The key is valid. This is an ordinary permission refusal on an endpoint that predates the 403 convention.

Branch on the message, not on the code by itself. An integration that reads every 401 as an expired key will re-authenticate against the second and third rows and fail the same way every time.

Denials from the Incident Management and Vulnerability Management endpoints used to arrive as a redirect carrying an empty body, or as an empty HTTP 200 that a client could easily record as success. They now return the same JSON envelope as the rest of the API. If you built a workaround for either shape, you can retire it.

What gets logged on a denial

Don't assume a denied API call leaves a warning in the log — what you get depends on which check refused the request:

  • A specific record or file the caller isn't entitled to: a questionnaire attachment, a risk belonging to another team, the compliance test-result risk closure: warning
  • A named permission refused on an API or AJAX request: notice
  • A named permission refused on an ordinary page request, which redirects to the login page: info
  • An admin-only endpoint refusing a non-admin: nothing

The split between the middle two rows is deliberate. An API refusal stops an integration dead and deserves an operator's attention, while the page-level equivalent is someone clicking something they don't have rights to, which happens all day and rarely means anything. Endpoints that answer 401 through the API's shared unauthorized-access helper write no log entry at all, so the HTTP response is the only trace you get. If you're troubleshooting a denial you can't find in the log, raise logging_notice (or logging_info for the page-level case) before concluding the request never arrived.

Admin override

A user with admin = 1 bypasses all permission checks. The API key for an admin user can do anything: submit risks, modify users, deactivate Extras, change settings, generate other users' API keys.

This is appropriate for human-operator accounts; it's almost never appropriate for integration accounts. Compromise of an admin key compromises the entire SimpleRisk install. Use admin keys exclusively for break-glass scenarios; create dedicated, least-privileged users for routine integrations.

Per-user direct grants

The User Management UI lets you grant specific permissions to a user beyond their role's default. This is useful for integration users that need a slightly-different permission set than any defined role:

  • Pick a role that's mostly right.
  • Grant the additional permissions directly to the user.
  • The user's effective permission set is the union.

For integrations that recur, the grant pattern often repeats — at which point it's worth defining a new role specifically for that integration pattern.

Designing permissions for integrations

Pattern A: Read-only reporting integration

Goal: a script that reads risk data for an external dashboard.

  • Role: a custom "API Reader" role with view permissions for the relevant entities (view_risks, view_compliance, etc.) and no write permissions.
  • Teams: every team the dashboard should display data for.
  • Admin flag: 0.
  • Verify: the user can GET /api/v2/risks (returns expected data) but POST /api/v2/risks/submit returns 403.

Pattern B: Risk submitter integration

Goal: a script that imports risks from an external source weekly.

  • Role: a custom "API Submitter" role with submit_risks and the necessary related permissions.
  • Teams: the team(s) the imported risks should be assigned to (so they're visible to the right users via team-segregation).
  • Admin flag: 0.
  • Verify: the user can POST /api/v2/risks/submit (creates a risk) and the created risk appears for users on the same team(s).

Pattern C: Risk-state-update integration

Goal: a CI pipeline that closes risks when corresponding fixes ship.

  • Role: a custom "API CI" role with modify_risks and close_risks (and the related team-scoped equivalents).
  • Teams: the team the risks the CI manages belong to.
  • Admin flag: 0.
  • Verify: the user can PATCH /api/v2/risks/{id} to update the relevant risks but can't modify other team's risks (403).

Pattern D: Admin operations integration

Goal: a script that bulk-creates users from an HR feed.

  • Role: includes manage_users and related admin permissions.
  • Teams: not relevant for user management.
  • Admin flag: 0 if manage_users alone is sufficient; 1 only if you genuinely need admin override.
  • Verify: the user can POST /api/v2/users to create users, and the created users get the right initial role/team.

Common pitfalls

A handful of patterns recur with API permissions.

  • Generating keys against admin users. The compromise blast radius is enormous. Use dedicated, least-privileged integration users.

  • Forgetting that team membership filters the integration's view. "Why does my integration see only some of the risks?" — usually the integration user isn't on the team that owns the missing risks.

  • Granting admin = 1 to fix permission issues. Diagnose the actual missing permission and grant that specifically; don't paper over with admin override.

  • Not creating a per-integration user. Sharing one user across multiple integrations means rotating the key affects all integrations and audit attribution is ambiguous.

  • Granting permissions ad-hoc to individual users instead of defining roles. Three integrations with the same permission profile should use the same role; otherwise role-management drift over time produces inconsistent integration behavior.

  • Not testing the integration's permissions before going live. "It works for the developer's admin account; deploy to production" → first 403 in production is the integration first proving it lacks the actual permission.

  • Treating 403 as a transient error. It's not; the user genuinely lacks the permission. Check permissions; don't add retry logic.

  • Forgetting that some endpoints require the user be on a specific team. A risk endpoint may filter by team; a configuration endpoint may require admin. The endpoint's permission check is endpoint-specific.

  • Confusing read permissions with team filtering. A user with view_risks permission and membership in the relevant team can see those risks; either alone isn't enough for cross-team visibility.

  • Granting permissions for hypothetical future operations. "Just give it manage_users in case." No — give it what it needs now; expand when needed. Over-granting is a liability.

Related