# datadog One connection is one Datadog organization. toolpass authenticates with an API key and a scoped application key, finds the user by email, reads the permissions carried by the user's roles, and for a monitor, dashboard, SLO or notebook reads the asset's restriction policy and its legacy `restricted_roles` and author fields. A user may change such an asset only when a role carries the write permission **and** the asset's restrictions, if any, name the user, one of the user's roles or teams, or the whole org. Nothing is written. ## Credential 1. An **API key** of the organization (`api_key`). 2. An **application key** (`credential`) created for a dedicated service account or user and **scoped** to `user_access_read`, `teams_read` and the read scope of each asset type asked about: `monitors_read`, `dashboards_read`, `slos_read`, `notebooks_read`. Reading a restriction policy needs no named scope. An unscoped application key carries every permission of its creator, which is far more than toolpass needs. `toolpass probe` validates the API key, lists one user (which proves `user_access_read`) and reminds you of the other scopes. ## Connection ```yaml - id: datadog-acme integration: datadog api_key: env:DATADOG_API_KEY credential: env:DATADOG_APP_KEY # url: https://api.datadoghq.eu # default https://api.datadoghq.com; US3 api.us3.datadoghq.com, US5 api.us5.datadoghq.com, AP1 api.ap1.datadoghq.com ``` | Key | Meaning | |---|---| | `url` | the site's API URL | | `api_key` | sent as `DD-API-KEY`, `env:` or `file:` | | `credential` | the application key, sent as `DD-APPLICATION-KEY`, `env:` or `file:` | ### Identity `GET /api/v2/users?filter=&filter[status]=Active,Pending,Disabled`, every page. The filter is a substring match on name, handle and email, so only the record whose `email` equals the address (ignoring case) is the user; none is `user_not_found`, two are `user_ambiguous`. A `disabled` user, or one whose `status` is still `Pending`, is denied every action. The identity carries the user's handle and the ids of the user's roles; each role's permissions come from `GET /api/v2/roles/{id}/permissions`, cached for five minutes. Groups sent by the caller are ignored. ## Resources | Resource | Meaning | |---|---| | `monitor:` | a monitor | | `dashboard:` | a dashboard (`abc-def-ghi`) | | `slo:` | a service level objective | | `notebook:` | a notebook | | `org` | the organization, for permissions that are not about one asset | ## Actions | Action | Permission | Restriction relation | Resource | |---|---|---|---| | `monitor.edit` | `monitors_write` | editor | monitor | | `monitor.mute` | `monitors_downtime` | editor (muting counts as editing a restricted monitor) | monitor | | `monitor.read` | `monitors_read` | viewer | monitor | | `dashboard.edit` | `dashboards_write` | editor | dashboard | | `dashboard.read` | `dashboards_read` | viewer | dashboard | | `slo.edit` | `slos_write` | editor | slo | | `notebook.edit` | `notebooks_write` | editor | notebook | | `logs.read` | `logs_read_data` | | org | | `users.manage` | `user_access_manage` | | org | | `apikeys.manage` | `api_keys_write` | | org | | `raw:` | that permission | on an asset: editor when it is one of the type's write permissions (`monitors_write`, `monitors_downtime`, `dashboards_write`, `slos_write`, `notebooks_write`), viewer otherwise | any | ### Evaluation 1. The permission: one of the user's roles must list it. On the `org` resource that is the answer. 2. The asset is read (`GET /api/v1/monitor/{id}`, `/api/v1/dashboard/{id}`, `/api/v1/slo/{id}`, `/api/v1/notebooks/{id}`); a 404 is `resource_not_visible` whatever the permission. 3. The restriction policy: `GET /api/v2/restriction_policy/{type}:{id}`. When it has bindings, the user must appear in a binding whose relation is the one needed or higher (`viewer` < `editor`; type-specific relations above editor count as editor) as `user:`, `role:`, `team:` (`GET /api/v2/team/{id}/memberships`, filtered by the user's email and compared by id) or `org:`. A policy with no `viewer` binding restricts editing only: reads fall back to the permission. 4. Without a policy, the legacy fields: a monitor's or dashboard's `restricted_roles` must include one of the user's roles for an edit, except that a dashboard's author (`author_handle`) may always edit. Reads are not restricted by `restricted_roles`. ## Decisions | Situation | toolpass answers | |---|---| | a role carries the permission and the asset has no restriction | allow | | a role carries the permission and a binding at the relation names the user, a role, a team the user is on, or the org | allow, naming the principal kind | | a role carries the permission and `restricted_roles` includes a role of the user, or the user authored the dashboard | allow | | no role carries the permission | deny | | the asset's restriction policy or `restricted_roles` name none of the user's principals | deny | | disabled user, or a user whose invitation is still pending | deny | | no user with the email | deny (`user_not_found`) | | several users | unknown (`user_ambiguous`) | | user record without `disabled` | unknown (`unsupported`) | | the asset, a role of the user, or a team named by the policy answers 404 | unknown (`resource_not_visible`) | | 401, 403 (Datadog answers 403 for an invalid key as well as for a missing scope) | unknown (`credential_rejected`) | | 429, 5xx, timeout | unknown (`upstream_rate_limited` / `upstream_error` / `upstream_timeout`) | Error bodies are never copied into a decision text. ## Routes The secure-tools toolkit maps a tool's requests to the API (`api.`, the site's web host and its logs intake host, from `url`) to these actions; `toolpass routes datadog` prints the table. | Requests | Action and resource | |---|---| | a monitor, dashboard, SLO or notebook by id (`/api/v1/monitor/{id}`, ...) | its read or edit action on the asset (`monitor.edit` on `monitor:123`, `raw:slos_read` on `slo:`), so restriction policies apply | | creating or listing them | the permission on `org` (`raw:monitors_write`, `raw:dashboards_read`) | | a restriction policy of one of those assets | the asset's edit action (changes) or read action | | a downtime naming a monitor (v1 `monitor_id`, v2 `monitor_identifier.monitor_id`) | `monitor.mute` on the monitor | | a downtime by scope or monitor tags, or changing or cancelling one by id; the downtimes matching a monitor (`GET /api/v2/monitor/{id}/downtime_matches`) | `raw:monitors_downtime` on `org` | | searching or aggregating logs; an archive search's results | `logs.read` | | reading API keys or any user's application keys, which returns the keys | `raw:api_keys_read` or `raw:org_app_keys_read` on `org` | | searching spans; reading a trace (`/api/v2/trace/{id}`, `/api/v2/pruned_trace/{id}`) | `raw:apm_read` on `org` | | users, roles, API and application keys, service accounts, logs and metrics configuration, metric and event queries | the one permission the API description names for the operation, as `raw:` on `org` (`users.manage` and `apikeys.manage` where they are that permission) | Refused: a body that names a second asset (an `id` or `data.id` other than the path's, more than one dashboard or SLO in a bulk request); creating a user or service account with roles (assign them afterwards with `POST /api/v2/roles/{id}/users`); writes to assets whose restriction policies the integration does not read (synthetic tests, security rules, workflows, on-call, logs pipelines, ...); writes the API description gives no permission for, or several; sharing dashboards outside the org; the agent's own application keys and profile (`/api/v2/current_user`); data intake (metrics, events, logs), which needs the API key alone; and every other product's writes until a route maps them. Bodies are read as `application/json` only; a duplicated member or a compressed body is refused. Reads with no route are allowed and marked untrusted. ## What it cannot see - Self-elevation: a user with `user_access_manage` can add themselves to any restriction policy. toolpass answers for the policy as it stands, so such a user is denied until they do. - Assets other than monitors, dashboards, SLOs and notebooks (synthetics tests, security rules, workflows, ...), which have restriction policies of their own but no action here yet; use `raw:` on `org` for their permissions alone. - Log restriction queries, index-level log permissions, and RBAC on APM services. - Team membership through team-links or provisioning that the memberships endpoint does not list. ## Unverified Marked `# UNVERIFIED:` in the code: - Whether a monitor's creator keeps edit rights under `restricted_roles` the way a dashboard's author does. Datadog documents roles only for monitors, so the creator is not exempted. - That a restriction policy without a `viewer` binding leaves viewing to the permission. The UI writes an explicit `viewer` binding for the org whenever it restricts an asset, which is why toolpass reads a policy without one as restricting editing only. Assumed from Datadog's documentation rather than tested live: that the write permission is still required when a restriction policy grants `editor` ("the limitations are applied both in the UI and API"), and that `GET /api/v2/restriction_policy/{id}` answers `200` with empty bindings, not 404, for an asset without a policy (a 404 is treated the same). ## Test Unit tests run against a fake that serves users (paged), role permissions, monitors, dashboards, SLOs, notebooks, restriction policies and team memberships (paged), validating every request against Datadog's v1 and v2 OpenAPI descriptions (`datadog-v1` and `datadog-v2` in `test/specs/fetch.sh`). There is no live test; after configuring, run `toolpass probe` and one check for a user you know is allowed.