# argocd Argo CD has no API that asks "may user X do Y" for another user (`account/can-i` is only for the caller). toolpass therefore reads the RBAC policy from the cluster and evaluates it locally with the same rules as the Argo CD API server. The evaluator is a port of `argo-cd/util/rbac` and `argo-cd/server/rbacpolicy` (v3), verified against upstream master in September 2026, and its tests are ported from Argo CD's own. ## Credential None of its own. An argocd connection names a kubernetes connection, whose ServiceAccount needs, in the Argo CD namespace: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: toolpass-argocd-reader namespace: argocd rules: - apiGroups: [""] resources: ["configmaps"] resourceNames: ["argocd-rbac-cm", "argocd-cm"] verbs: ["get"] - apiGroups: ["argoproj.io"] resources: ["appprojects"] verbs: ["get", "list"] ``` `test/kind/argocd-rbac.yaml` has the Role and RoleBinding. All reads; nothing in Argo CD is touched. ## Connection ```yaml - id: argocd-prod integration: argocd kubernetes_connection: k8s-prod-eu namespace: argocd # default rbac_configmap: argocd-rbac-cm # default user_subject: none # default; or email # oidc_issuer: https://sso.example.com # read the groups from the user's ID token # oidc_audience: argo-cd # (Argo CD's oidc.config issuer and clientID) # url: https://argocd.example.com # the API server, for observed actions ``` | Key | Meaning | |---|---| | `kubernetes_connection` | id of the kubernetes connection for the cluster Argo CD runs in | | `namespace` | Argo CD's namespace | | `rbac_configmap` | name of the RBAC config map | | `user_subject` | what Argo CD sees as the user's subject. `none`: toolpass evaluates only the default role and the groups. `email`: the email is also the subject, as when your identity provider's `sub` claim is the email | | `oidc_issuer`, `oidc_audience`, `oidc_groups_claim`, `oidc_email_claim` | optional: read the user's groups from the ID token sent with the check when the caller sends none; the issuer and client id of Argo CD's `oidc.config`. See [Groups from the user's token](../guides/groups.md) | | `url` | optional: the Argo CD API server, so that the [secure-tools toolkit](#requests-a-toolpass-tool-makes) maps requests to it to actions; toolpass itself never calls it | The policy (config maps and AppProjects) is cached for 30 seconds. ## How the evaluation works Exactly as in Argo CD: 1. Policy = built-in policy (role:readonly, role:admin, admin) + `policy.csv` + every `policy.*.csv` key in sorted order + the AppProject's roles when the request is project scoped. 2. Matching is a glob with no separators (`*` crosses `/`) or, with `policy.matchMode: regex`, an unanchored RE2 regular expression, as Argo CD's Go code evaluates it. An invalid pattern never matches. 3. Effect: some matching `allow` and no matching `deny`. 4. Order: `policy.default` role first, then the subject, then each group that appears as the first element of some `g` line. 5. Group values are the caller's `groups` or, when the caller sends none and `oidc_issuer` is set, the groups claim of the user's ID token, verified against the provider ([Groups from the user's token](../guides/groups.md)). When `scopes` in `argocd-rbac-cm` includes `email`, the email is a group value too. 6. `server.rbac.disableApplicationFineGrainedRBACInheritance` in `argocd-cm` (default true since v3): when true `app.update` does not imply `app.update/`; when false the top-level verb is checked first. 7. `server.rbac.rollback.enforce.enable` in `argocd-cm` (default false): `app.rollback` checks `sync` unless it is true, then it checks `rollback`. ## Resources | Resource | Argo CD object | |---|---| | `applications:/` or `applications://` | application | | `applicationsets:/` | ApplicationSet | | `logs:/`, `exec:/` | logs and exec (an `applications:` resource is accepted too) | | `projects:` | project | | `clusters:` or `clusters:/` | cluster | | `repositories:`, `write_repositories:` | repository | | `certificates:`, `accounts:`, `gpgkeys:`, `extensions:` | as named | An application or ApplicationSet in Argo CD's own `namespace` is `/` to Argo CD (`security.RBACName`), so `applications:/argocd/` is checked as `/`. ## Actions `app.get/create/update/delete/sync/rollback/override`, `app.action///`, `app.update////`, `app.delete////` (the resource's name may contain `:`, as in `system:aggregate-to-edit`), `app.update_with_get`, `app.delete_with_get`, `app.sync_with_get`, `logs.get`, `logs.get_with_app_get`, `exec.create`, `appset.get/create/update/delete`, `project.get/create/update/delete`, `cluster.get/create/update/delete`, `repo.get/create/update/delete`, `writerepo.get/create/update/delete`, `certificate.get/create/update/delete`, `account.get/update`, `gpgkey.get/create/delete`, `extension.invoke`. The `*_with_get` actions are what Argo CD checks for a patch, a delete, a sync and a pod log read: `get` on the application first, then the action (`get` on the logs for `logs.get_with_app_get`). Both are answered for the user; the answer is deny when either denies, unknown when either is unknown and neither denies. ## Requests a toolpass tool makes With `url` set, the [secure-tools toolkit](../guides/intercepted-actions.md) maps every request a tool's body sends to the Argo CD REST API to the action and object Argo CD's RBAC checks, and checks the ones inside `as_user()` for the session's user (`toolpass routes argocd` prints the table). Argo CD's paths name an application but not its project, and toolpass checks applications under their project. A request about an application must name the project: `?project=` on reads, deletes, updates and resource changes, `project` in the body of a sync, rollback, patch or `resource/actions/v2`. Argo CD answers "not found" when the application is in another project than the one named, so the project in the request is the one the change lands in. A request without it is refused. | Request | Action, resource | |---|---| | `GET /api/v1/applications/` and its reads (`resource-tree`, `manifests`, `events`, `resource`, `revisions/...`, ...) | `app.get`, `applications:/` (`//` with `appNamespace`) | | `.../logs`, `.../pods//logs` | `logs.get_with_app_get`, `logs:/` (the application's `get`, then its logs' `get`) | | `POST /api/v1/applications`, `PUT .../`, `PUT .../spec` | `app.create`, `app.update`, `app.update` | | `PATCH .../`, `DELETE .../` | `app.update_with_get`, `app.delete_with_get` | | `POST .../sync`, `DELETE .../operation`, `POST .../rollback` | `app.sync_with_get`, `app.sync`, `app.rollback` | | `POST`/`DELETE .../resource` | `app.update////`, `app.delete/...` | | `POST .../resource/actions`, `.../resource/actions/v2` | `app.action///` | | `POST /api/v1/applicationsets` | `appset.create`, `applicationsets:/` | | `GET`/`DELETE /api/v1/projects/`, `POST /api/v1/projects`, project role tokens | `project.get`, `project.delete`, `project.create`, `project.update` | | `POST /api/v1/clusters`, `/repositories`, `/write-repositories` | `cluster.create`, `repo.create`, `writerepo.create` on `/` or `` from the body | | `/api/v1/repocreds`, `/write-repocreds` (create, update, delete) | `repo.*`, `writerepo.*` on the credentials' URL | | `GET /api/v1/account/`, its tokens | `account.get`, `account.update` | `/api/version`, `/settings`, `/session/userinfo`, `/account/can-i/...` (the agent's own) and the login at `POST /api/v1/session` need no user permission. Refused: - **A second action**: a sync with `manifests`, `revision` or `revisions` (also `override`); an update whose body moves the application to another project (also the new project); a patch that may change the project or the metadata (Argo CD updates the application the patched metadata names, under the project it names, and reads an empty project as `default`); a create with `upsert` (may update instead). A JSON patch (`patchType` `json` or empty) is refused when an operation other than `test` writes to or moves from `/metadata...`, `/spec`, `/spec/project` or the whole document; a merge patch (`patchType` `merge`) when it is not an object, names `metadata`, sets `spec` to anything but an object, or sets `spec.project` to another value. Names are compared in any case, as Go's decoder reads them. Values are not looked at: a `repoURL` that mentions `project` is fine. Other patch types are refused. - **Listings** (`GET /api/v1/applications`, `/projects`, `/clusters`, ...): Argo CD filters them by the agent's own access, not the user's. Get one object by name. - **Objects whose project Argo CD looks up**: ApplicationSets, clusters and repositories by name. - **Project updates**, which also need permissions on the clusters and repositories they add; **certificates and GPG keys**, which Argo CD checks with an empty object. - **The agent's own account**: its password, `DELETE /api/v1/session`. - **Anything else** under `/api`, exec (`/terminal`), proxy extensions, and the argocd CLI's gRPC-web calls (a `POST` to `//`: a write with no route). - **What toolpass cannot read exactly**: a body that is not JSON, a member given twice or in another case (Go's decoder takes either), a query Go would read differently, a name that is not a valid Kubernetes name. ## Decisions | Situation | Answer | |---|---| | A rule allows and none denies | allow | | No rule allows, or a rule denies | deny | | `user_subject: none` and the policy has user-level rules toolpass cannot reach through groups | unknown (`unsupported`) rather than deny | | A `*_with_get` action (two checks) | deny when either denies, else unknown when either is unknown | | The policy is invalid (Argo CD would refuse it too) | unknown (`unsupported`) | | An AppProject's role policy is invalid | evaluated without it, as Argo CD does | | The ServiceAccount may not read the config maps or projects | unknown (`credential_rejected`) | | `oidc_issuer` is set, the caller sent no groups, and no token or one that does not verify | unknown (`invalid_request`) | ## What it cannot see - Policies bound to opaque identity provider subjects (Dex `sub`, `federated_claims.user_id`). Use `user_subject: email` only when the subject really is the email. - Local Argo CD accounts, project JWT tokens. - Sync windows, project source/destination restrictions, and anything else enforced outside RBAC. - Applications in other namespaces are addressed as `//`; toolpass does not look the application up to find its project. - The route table relies on Argo CD refusing a request whose `project` is not the application's (`getAppEnforceRBAC`, written from Argo CD's server source; UNVERIFIED against a live server). Releases that predate the `project` parameter on application requests do not check it: do not set `url` for them. ## Test Unit tests run against a fake API server. `toolpass-py/tests/integrations/argocd/rbac` contains the ported Argo CD tests. `toolpass-py/tests/integrations/argocd/test_routes.py` holds the route table to Argo CD's Swagger document (`argocd.spec` from `test/specs/fetch.sh`), its refusals, and its resources to the check itself. `toolpass-py/tests/differential/test_argocd.py` compares the evaluator with `argocd admin settings rbac can` when the `argocd` binary is on the PATH.