# databricks One connection is one Databricks workspace. toolpass authenticates as a service principal (OAuth M2M) or with a personal access token, finds the user through the workspace SCIM API (email, active flag, groups), and asks the workspace itself. Unity Catalog securables (catalogs, schemas, tables, volumes, functions, models) are answered from the effective-permissions endpoint, which folds in privileges inherited down the hierarchy; grants to the user and to any of the user's groups count, and the securable's owner holds every privilege. Workspace objects (clusters, jobs, warehouses, notebooks, ...) are answered from the Permissions API ACL, whose entries include inherited ones; workspace admins hold `CAN_MANAGE` on every object. Nothing is written. ## Credential 1. Create a service principal in the account console, add it to the workspace, and generate an OAuth secret for it (`client_id` is its application id, `credential` the secret). A personal access token of a dedicated user works too (`auth_mode: token`). 2. Decide what it may read. Databricks has **no read-only admin role**: - the Permissions API returns an object's ACL only to a principal with `CAN_MANAGE` on that object (or a workspace admin); - Unity Catalog returns another principal's grants only to a metastore admin, the securable's owner, the owner of its catalog or schema, or a principal with `MANAGE` on it. Making the service principal a **workspace admin** answers every workspace-object question, and granting it `MANAGE` (or `BROWSE` plus ownership through a group) on the catalogs it should answer for covers Unity Catalog. A workspace admin can also change the workspace, so keep the secret tightly held. Whatever toolpass cannot read answers `unknown`, never `deny`. `toolpass probe` reads the credential's own SCIM record and says whether it is a workspace admin. ## Connection ```yaml - id: databricks-prod integration: databricks url: https://adb-1234567890123456.7.azuredatabricks.net client_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 # service principal application id credential: env:DATABRICKS_OAUTH_SECRET # its OAuth secret # auth_mode: token # then credential is a personal access token # admins_manage_all: "true" # workspace admins pass every workspace-object check ``` | Key | Meaning | |---|---| | `url` | the workspace URL | | `auth_mode` | `oauth` (default): client credentials with `client_id` and the secret; `token`: a personal access token | | `client_id` | the service principal's application id, `oauth` only | | `credential` | the OAuth secret or the personal access token, `env:` or `file:` | | `token_url` | default `{url}/oidc/v1/token` | | `admins_manage_all` | `true` (default): a member of the workspace `admins` group is allowed every workspace-object action, as Databricks grants admins `CAN_MANAGE` on every object; `false`: judge admins by the ACL alone | OAuth: `POST {token_url}` with `grant_type=client_credentials&scope=all-apis` and the client id and secret in an HTTP Basic header, as Databricks documents. The token is cached and refreshed five minutes before it expires; a 401 from the workspace drops it and retries the call once. Every API call carries `Authorization: Bearer`. ### Identity `GET /api/2.0/preview/scim/v2/Users?filter=userName eq ""&attributes=id,userName,active,groups`. The email is lowercased and must be a plain address (no quotes or backslashes, so the SCIM filter cannot be broken out of); the record whose `userName` equals it case-insensitively is the user. No record is `user_not_found`, two are `user_ambiguous`. A user with `active: false` is denied every action; a record without `active` is unknown. The user's groups are the `groups[].display` names of the record; membership of `admins` marks a workspace admin. Groups sent by the caller are ignored. ## Resources | Resource | Meaning | |---|---| | `catalog:` | a Unity Catalog catalog | | `schema:.` | a schema | | `table:..` | a table or view | | `volume:..` | a volume | | `function:..` | a function | | `model:..` | a registered model in Unity Catalog | | `cluster:` | an all-purpose cluster (Permissions API `clusters`) | | `policy:` | a cluster policy (`cluster-policies`) | | `pool:` | an instance pool (`instance-pools`) | | `job:` | a job (`jobs`) | | `pipeline:` | a pipeline (`pipelines`) | | `warehouse:` | a SQL warehouse (`warehouses`) | | `notebook:`, `directory:`, `repo:` | workspace files (`notebooks`, `directories`, `repos`); the numeric object id, not the path | | `endpoint:` | a model serving endpoint (`serving-endpoints`): the endpoint's `id`, not its name, as the Permissions API keys it | | `experiment:`, `registered_model:` | MLflow experiments and workspace model registry models | Each part of a Unity Catalog name must be a plain identifier (`[A-Za-z0-9_-]`, up to 255 characters); names that need backtick quoting in SQL are not accepted. A workspace object id is `[A-Za-z0-9][A-Za-z0-9_.-]*`. Both are path-escaped before they reach a URL. ## Actions | Action | Requires | Resources | |---|---|---| | `raw:` | that one Unity Catalog privilege (`raw:SELECT`, `raw:CREATE_VOLUME`) | any Unity Catalog resource | | `raw:` | that permission level or one that implies it (`raw:CAN_RESTART`, `raw:IS_OWNER`); a level the object type does not have is `invalid_request` | any workspace object | | `table.read` | `SELECT` + `USE_SCHEMA` + `USE_CATALOG` | table | | `table.write` | `MODIFY` + `USE_SCHEMA` + `USE_CATALOG` | table | | `table.create` | `CREATE_TABLE` + `USE_SCHEMA` + `USE_CATALOG` | schema | | `volume.create` / `model.create` | `CREATE_VOLUME` / `CREATE_MODEL` + `USE_SCHEMA` + `USE_CATALOG` | schema | | `schema.create` | `CREATE_SCHEMA` + `USE_CATALOG` | catalog | | `schema.read` | `USE_SCHEMA` + `USE_CATALOG` | schema | | `catalog.use` | `USE_CATALOG` | catalog | | `volume.read` / `volume.write` | `READ_VOLUME` / `WRITE_VOLUME` + `USE_SCHEMA` + `USE_CATALOG` | volume | | `function.execute` | `EXECUTE` + `USE_SCHEMA` + `USE_CATALOG` | function | | `model.read` | `EXECUTE` + `USE_SCHEMA` + `USE_CATALOG` | model | | `uc.manage` | `MANAGE`, or ownership | any Unity Catalog resource | | `schema.manage` | `MANAGE` (or ownership) + `USE_CATALOG` | schema | | `table.manage` / `volume.manage` / `function.manage` / `model.manage` | `MANAGE` (or ownership) + `USE_SCHEMA` + `USE_CATALOG` | table / volume / function / model | | `cluster.attach` / `cluster.restart` / `cluster.manage` | `CAN_ATTACH_TO` / `CAN_RESTART` / `CAN_MANAGE` | cluster | | `job.view` / `job.run` / `job.manage` | `CAN_VIEW` / `CAN_MANAGE_RUN` / `CAN_MANAGE` | job | | `warehouse.use` / `warehouse.manage` | `CAN_USE` / `CAN_MANAGE` | warehouse | | `notebook.read` / `notebook.run` / `notebook.edit` | `CAN_READ` / `CAN_RUN` / `CAN_EDIT` | notebook, directory, repo | | `pipeline.run` | `CAN_RUN` | pipeline | | `endpoint.query` | `CAN_QUERY` | endpoint | Unity Catalog: `ALL_PRIVILEGES` covers every privilege that lives on the securable it was granted on and below: granted on the catalog it carries `USE_CATALOG` and `USE_SCHEMA`, granted on a schema it carries `USE_SCHEMA` but not `USE_CATALOG`, granted on a table it carries neither. It never includes `MANAGE`. The legacy `USAGE` counts as both `USE_CATALOG` and `USE_SCHEMA`. Privileges are read from `GET /api/2.1/unity-catalog/effective-permissions/{type}/{name}?max_results=0`, every page, and unioned over the user and the user's groups. When they do not cover the action, the securable's `owner` is read (`GET /api/2.1/unity-catalog/{tables,schemas,...}/{name}`). Ownership, directly or through a group, stands for every privilege on the securable itself, but not for `USE_CATALOG` or `USE_SCHEMA` on its parents: a table owner without `USE_SCHEMA` is denied `table.read`, as Databricks would refuse the query. Unity Catalog shows a principal without `MANAGE`, ownership or metastore admin only its own grants, with a 200 rather than a 403. A listing in which no principal but toolpass itself appears is therefore not a deny: it is answered `resource_not_visible` ("either nobody else holds any, or toolpass may only see its own"). toolpass learns its own principal name from `GET /api/2.0/preview/scim/v2/Me`, once every ten minutes. Workspace objects: the ACL is `GET /api/2.0/permissions/{type}/{id}`. A level implies the weaker ones of its chain (`CAN_ATTACH_TO` < `CAN_RESTART` < `CAN_MANAGE`; `CAN_VIEW` < `CAN_MANAGE_RUN` < `IS_OWNER` < `CAN_MANAGE`; `CAN_READ` < `CAN_RUN` < `CAN_EDIT` < `CAN_MANAGE`; `CAN_VIEW` < `CAN_QUERY` < `CAN_MANAGE`; warehouses `CAN_VIEW` < `CAN_MONITOR` and `CAN_VIEW` < `CAN_USE`, both under `CAN_MANAGE`), and `CAN_MANAGE` and `IS_OWNER` imply everything except `IS_OWNER` itself, which names the one owner and is matched exactly (workspace admins included). `CAN_MONITOR` does not imply `CAN_USE`. Entries for service principals never match a user. ## Decisions | Situation | toolpass answers | |---|---| | the needed privileges are all held (directly, through a group, or inherited from a parent securable), or covered by an `ALL_PRIVILEGES` grant at the right level | allow, saying where they come from | | privileges missing but the user (or a group of theirs) owns the securable | allow ("owns ...") | | privileges missing, the owner, but `USE_CATALOG` / `USE_SCHEMA` on a parent missing | deny ("owns ... but lacks ... on its parents") | | privileges missing, not the owner, and the listing names a principal other than toolpass | deny, naming the missing privileges | | privileges missing, not the owner, and the listing names nobody but toolpass | unknown (`resource_not_visible`) | | a held level equals or implies the needed one | allow | | no matching level, user is a workspace admin, `admins_manage_all: true` | allow ("workspace admin") | | no matching level otherwise | deny, naming the levels held | | deactivated user (`active: false`) | deny | | no SCIM record for the email | deny (`user_not_found`) | | several records | unknown (`user_ambiguous`) | | record without `active` | unknown (`unsupported`) | | securable or object answers 404 | unknown (`resource_not_visible`) | | 403 `PERMISSION_DENIED` (toolpass lacks `CAN_MANAGE`, `MANAGE` or ownership) | unknown (`credential_rejected`) | | 401 after one retry with a fresh token, token endpoint refuses the client | unknown (`credential_rejected`) | | the SCIM search endpoint itself answers 404 (wrong `url`, an account console URL) | unknown (`upstream_error`) | | 400 `INVALID_PARAMETER_VALUE` | unknown (`invalid_request`) | | 429, 5xx, timeout, too many pages of grants | unknown (`upstream_rate_limited` / `upstream_error` / `upstream_timeout`) | Only `error_code` is read from an error body; messages are never copied into a decision text. ## Probe `toolpass probe` calls `GET /api/2.0/preview/scim/v2/Me`, names the principal and whether it is a workspace admin, and warns either way: a non-admin cannot read most ACLs, an admin can change the workspace. ## Intercepted requests A toolpass tool's requests to the workspace REST API are mapped to these actions (`src/toolpass/integrations/databricks/routes.py`, `toolpass routes databricks`): jobs by `job_id` (get, runs of a job, run now, cancel all runs, delete), clusters by `cluster_id` (get, events, start, restart, terminate, resize, delete), SQL warehouses, pipelines and Git folders by id, ACLs through the Permissions API (`CAN_MANAGE` on the object; a body that grants `IS_OWNER` anywhere, or spells `access_control_list` another way, is refused), Unity Catalog catalogs, schemas, tables, volumes, functions and models by full name, and files under `/Volumes///` as `volume.read` and `volume.write`. Unity Catalog requests carry the `USE_CATALOG` and `USE_SCHEMA` Databricks requires on the securable's parents: reads are `catalog.use`, `schema.read`, `table.read`, `volume.read`, `function.execute` and `model.read` (a model's versions too); updates, deletes, renames within the same parent and grants are `.manage` (`uc.manage` on a catalog); creating schemas, managed volumes and models is `schema.create`, `volume.create` and `model.create` on the parent. Lists need no user check; the workspace filters them by the agent's own access. Refused: SQL statement execution, the command execution API and one-time job runs (they run as the agent), runs named only by `run_id`, edits Databricks checks against the caller (job and pipeline settings, cluster edits), creating jobs, clusters, warehouses, catalogs, tables, views and functions, the Workspace and DBFS APIs (they name objects by path), secrets, model serving endpoints addressed by name (reading, querying, updating or deleting one: the serving API takes the endpoint's name, while its permissions are kept under its id, which the request does not carry; an endpoint's ACL, `/api/2.0/permissions/serving-endpoints/`, is routed), and any request that names its id twice or in a form toolpass does not read exactly. ## What it cannot see - Metastore admins: their implicit privileges are not in SCIM or in the effective-permissions list, so a metastore admin without grants is denied Unity Catalog actions. - Ownership of a parent (a catalog owner acting on a table) is not modelled; only the securable's own owner is, and only for privileges on that securable. - Account-level groups the workspace SCIM record does not list, row filters and column masks, Lakehouse Federation credentials, table ACLs on the legacy Hive metastore, and the entitlements (`workspace-access`, `databricks-sql-access`, `allow-cluster-create`) that gate features rather than objects. - Whether a cluster's access mode lets the user attach at all, and cluster policies' effect on what a job may run. ## Unverified Marked `# UNVERIFIED:` in the code: - Whether the effective-permissions endpoint already lists the owner's implicit privileges. The owner is looked up separately so an owner is never denied; if the API does list them the extra call is merely redundant. Not marked in code, but assumed from Databricks' documentation rather than tested live: that a `principal=`-less effective-permissions listing includes group principals by their display name, so unioning over the user's SCIM groups is what "effective" means; that the workspace `admins` group carries `CAN_MANAGE` on every Permissions API object even when the ACL does not show an entry for it; and that `IS_OWNER` on jobs, pipelines and warehouses implies `CAN_MANAGE`. ## Test Unit tests run against one fake server that serves the token endpoint (checking the Basic header and the form), SCIM, effective permissions (paginated), securable metadata and the Permissions API. Databricks publishes no OpenAPI description to validate requests against, so the fake checks paths and parameters itself. There is no live test; after configuring, run `toolpass probe` and one check for a user you know is allowed.