diff --git a/cmd/argocd/commands/admin/settings_rbac.go b/cmd/argocd/commands/admin/settings_rbac.go index 1c09fa0d1cfe7..eaf12e67a1a17 100644 --- a/cmd/argocd/commands/admin/settings_rbac.go +++ b/cmd/argocd/commands/admin/settings_rbac.go @@ -21,6 +21,12 @@ import ( "github.com/argoproj/argo-cd/v2/util/rbac" ) +type actionTraitMap map[string]rbacTrait + +type rbacTrait struct { + allowPath bool +} + // Provide a mapping of short-hand resource names to their RBAC counterparts var resourceMap map[string]string = map[string]string{ "account": rbacpolicy.ResourceAccounts, @@ -32,6 +38,7 @@ var resourceMap map[string]string = map[string]string{ "certs": rbacpolicy.ResourceCertificates, "certificate": rbacpolicy.ResourceCertificates, "cluster": rbacpolicy.ResourceClusters, + "extension": rbacpolicy.ResourceExtensions, "gpgkey": rbacpolicy.ResourceGPGKeys, "key": rbacpolicy.ResourceGPGKeys, "log": rbacpolicy.ResourceLogs, @@ -46,28 +53,53 @@ var resourceMap map[string]string = map[string]string{ } // List of allowed RBAC resources -var validRBACResources map[string]bool = map[string]bool{ - rbacpolicy.ResourceAccounts: true, - rbacpolicy.ResourceApplications: true, - rbacpolicy.ResourceApplicationSets: true, - rbacpolicy.ResourceCertificates: true, - rbacpolicy.ResourceClusters: true, - rbacpolicy.ResourceGPGKeys: true, - rbacpolicy.ResourceLogs: true, - rbacpolicy.ResourceExec: true, - rbacpolicy.ResourceProjects: true, - rbacpolicy.ResourceRepositories: true, +var validRBACResourcesActions map[string]actionTraitMap = map[string]actionTraitMap{ + rbacpolicy.ResourceAccounts: accountsActions, + rbacpolicy.ResourceApplications: applicationsActions, + rbacpolicy.ResourceApplicationSets: defaultCRUDActions, + rbacpolicy.ResourceCertificates: defaultCRDActions, + rbacpolicy.ResourceClusters: defaultCRUDActions, + rbacpolicy.ResourceExtensions: extensionActions, + rbacpolicy.ResourceGPGKeys: defaultCRDActions, + rbacpolicy.ResourceLogs: logsActions, + rbacpolicy.ResourceExec: execActions, + rbacpolicy.ResourceProjects: defaultCRUDActions, + rbacpolicy.ResourceRepositories: defaultCRUDActions, } // List of allowed RBAC actions -var validRBACActions map[string]bool = map[string]bool{ - rbacpolicy.ActionAction: true, - rbacpolicy.ActionCreate: true, - rbacpolicy.ActionDelete: true, - rbacpolicy.ActionGet: true, - rbacpolicy.ActionOverride: true, - rbacpolicy.ActionSync: true, - rbacpolicy.ActionUpdate: true, +var defaultCRUDActions = actionTraitMap{ + rbacpolicy.ActionCreate: rbacTrait{}, + rbacpolicy.ActionGet: rbacTrait{}, + rbacpolicy.ActionUpdate: rbacTrait{}, + rbacpolicy.ActionDelete: rbacTrait{}, +} +var defaultCRDActions = actionTraitMap{ + rbacpolicy.ActionCreate: rbacTrait{}, + rbacpolicy.ActionGet: rbacTrait{}, + rbacpolicy.ActionDelete: rbacTrait{}, +} +var applicationsActions = actionTraitMap{ + rbacpolicy.ActionCreate: rbacTrait{}, + rbacpolicy.ActionGet: rbacTrait{}, + rbacpolicy.ActionUpdate: rbacTrait{allowPath: true}, + rbacpolicy.ActionDelete: rbacTrait{allowPath: true}, + rbacpolicy.ActionAction: rbacTrait{allowPath: true}, + rbacpolicy.ActionOverride: rbacTrait{}, + rbacpolicy.ActionSync: rbacTrait{}, +} +var accountsActions = actionTraitMap{ + rbacpolicy.ActionCreate: rbacTrait{}, + rbacpolicy.ActionUpdate: rbacTrait{}, +} +var execActions = actionTraitMap{ + rbacpolicy.ActionCreate: rbacTrait{}, +} +var logsActions = actionTraitMap{ + rbacpolicy.ActionGet: rbacTrait{}, +} +var extensionActions = actionTraitMap{ + rbacpolicy.ActionInvoke: rbacTrait{}, } // NewRBACCommand is the command for 'rbac' @@ -221,8 +253,8 @@ argocd admin settings rbac validate --policy-file policy.csv # i.e. 'policy.csv' and (optionally) 'policy.default' argocd admin settings rbac validate --policy-file argocd-rbac-cm.yaml -# If --policy-file is not given, and instead --namespace is giventhe ConfigMap 'argocd-rbac-cm' -# from K8s is used. +# If --policy-file is not given, and instead --namespace is giventhe ConfigMap 'argocd-rbac-cm' +# from K8s is used. argocd admin settings rbac validate --namespace argocd # Either --policy-file or --namespace must be given. @@ -376,11 +408,9 @@ func checkPolicy(subject, action, resource, subResource, builtinPolicy, userPoli // If in strict mode, validate that given RBAC resource and action are // actually valid tokens. if strict { - if !isValidRBACResource(realResource) { - log.Fatalf("error in RBAC request: '%s' is not a valid resource name", realResource) - } - if !isValidRBACAction(action) { - log.Fatalf("error in RBAC request: '%s' is not a valid action name", action) + if err := validateRBACResourceAction(realResource, action); err != nil { + log.Fatalf("error in RBAC request: %v", err) + return false } } @@ -406,17 +436,18 @@ func resolveRBACResourceName(name string) string { } } -// isValidRBACAction checks whether a given action is a valid RBAC action -func isValidRBACAction(action string) bool { - if strings.HasPrefix(action, rbacpolicy.ActionAction+"/") { - return true +// validateRBACResourceAction checks whether a given resource is a valid RBAC resource. +// If it is, it validates that the action is a valid RBAC action for this resource. +func validateRBACResourceAction(resource, action string) error { + validActions, ok := validRBACResourcesActions[resource] + if !ok { + return fmt.Errorf("'%s' is not a valid resource name", resource) } - _, ok := validRBACActions[action] - return ok -} -// isValidRBACResource checks whether a given resource is a valid RBAC resource -func isValidRBACResource(resource string) bool { - _, ok := validRBACResources[resource] - return ok + realAction, _, hasPath := strings.Cut(action, "/") + actionTrait, ok := validActions[realAction] + if !ok || hasPath && !actionTrait.allowPath { + return fmt.Errorf("'%s' is not a valid action for %s", action, resource) + } + return nil } diff --git a/cmd/argocd/commands/admin/settings_rbac_test.go b/cmd/argocd/commands/admin/settings_rbac_test.go index 79835ffd0c14d..1821d7a7af796 100644 --- a/cmd/argocd/commands/admin/settings_rbac_test.go +++ b/cmd/argocd/commands/admin/settings_rbac_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/argoproj/argo-cd/v2/server/rbacpolicy" "github.com/argoproj/argo-cd/v2/util/assets" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,35 +42,75 @@ func (f *FakeClientConfig) ConfigAccess() clientcmd.ConfigAccess { return nil } -func Test_isValidRBACAction(t *testing.T) { - for k := range validRBACActions { - t.Run(k, func(t *testing.T) { - ok := isValidRBACAction(k) - assert.True(t, ok) - }) +func Test_validateRBACResourceAction(t *testing.T) { + type args struct { + resource string + action string + } + tests := []struct { + name string + args args + valid bool + }{ + { + name: "Test valid resource and action", + args: args{ + resource: rbacpolicy.ResourceApplications, + action: rbacpolicy.ActionCreate, + }, + valid: true, + }, + { + name: "Test invalid resource", + args: args{ + resource: "invalid", + }, + valid: false, + }, + { + name: "Test invalid action", + args: args{ + resource: rbacpolicy.ResourceApplications, + action: "invalid", + }, + valid: false, + }, + { + name: "Test invalid action for resource", + args: args{ + resource: rbacpolicy.ResourceLogs, + action: rbacpolicy.ActionCreate, + }, + valid: false, + }, + { + name: "Test valid action with path", + args: args{ + resource: rbacpolicy.ResourceApplications, + action: rbacpolicy.ActionAction + "/apps/Deployment/restart", + }, + valid: true, + }, + { + name: "Test invalid action with path", + args: args{ + resource: rbacpolicy.ResourceApplications, + action: rbacpolicy.ActionGet + "/apps/Deployment/restart", + }, + valid: false, + }, } - t.Run("invalid", func(t *testing.T) { - ok := isValidRBACAction("invalid") - assert.False(t, ok) - }) -} - -func Test_isValidRBACAction_ActionAction(t *testing.T) { - ok := isValidRBACAction("action/apps/Deployment/restart") - assert.True(t, ok) -} -func Test_isValidRBACResource(t *testing.T) { - for k := range validRBACResources { - t.Run(k, func(t *testing.T) { - ok := isValidRBACResource(k) - assert.True(t, ok) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := validateRBACResourceAction(tt.args.resource, tt.args.action) + if tt.valid { + assert.NoError(t, result) + } else { + assert.NotNil(t, result) + } }) } - t.Run("invalid", func(t *testing.T) { - ok := isValidRBACResource("invalid") - assert.False(t, ok) - }) } func Test_PolicyFromCSV(t *testing.T) { diff --git a/docs/operator-manual/rbac.md b/docs/operator-manual/rbac.md index 6341482a69cf4..8b318e77b7060 100644 --- a/docs/operator-manual/rbac.md +++ b/docs/operator-manual/rbac.md @@ -1,195 +1,286 @@ # RBAC Configuration -The RBAC feature enables restriction of access to Argo CD resources. Argo CD does not have its own -user management system and has only one built-in user `admin`. The `admin` user is a superuser and +The RBAC feature enables restrictions of access to Argo CD resources. Argo CD does not have its own +user management system and has only one built-in user, `admin`. The `admin` user is a superuser and it has unrestricted access to the system. RBAC requires [SSO configuration](user-management/index.md) or [one or more local users setup](user-management/index.md). Once SSO or local users are configured, additional RBAC roles can be defined, and SSO groups or local users can then be mapped to roles. +There are two main components where RBAC configuration can be defined: + +- The global RBAC config map (see [argo-rbac-cm.yaml](argocd-rbac-cm-yaml.md)) +- The [AppProject's roles](../user-guide/projects.md#project-roles) + ## Basic Built-in Roles Argo CD has two pre-defined roles but RBAC configuration allows defining roles and groups (see below). -* `role:readonly` - read-only access to all resources -* `role:admin` - unrestricted access to all resources +- `role:readonly`: read-only access to all resources +- `role:admin`: unrestricted access to all resources These default built-in role definitions can be seen in [builtin-policy.csv](https://github.com/argoproj/argo-cd/blob/master/assets/builtin-policy.csv) -### RBAC Permission Structure +## Default Policy for Authenticated Users + +When a user is authenticated in Argo CD, it will be granted the role specified in `policy.default`. + +!!! warning "Restricting Default Permissions" + + **All authenticated users get _at least_ the permissions granted by the default policies. This access cannot be blocked + by a `deny` rule.** It is recommended to create a new `role:authenticated` with the minimum set of permissions possible, + then grant permissions to individual roles as needed. + +## Anonymous Access + +Enabling anonymous access to the Argo CD instance allows users to assume the default role permissions specified by `policy.default` **without being authenticated**. + +The anonymous access to Argo CD can be enabled using the `users.anonymous.enabled` field in `argocd-cm` (see [argocd-cm.yaml](argocd-cm-yaml.md)). + +!!! warning + + When enabling anonymous access, consider creating a new default role and assigning it to the default policies + with `policy.default: role:unauthenticated`. + +## RBAC Model Structure + +The model syntax is based on [Casbin](https://casbin.org/docs/overview). There are two different types of syntax: one for assigning policies, and another one for assigning users to internal roles. + +**Group**: Allows to assign authenticated users/groups to internal roles. + +Syntax: `g, , ` + +- ``: The entity to whom the role will be assigned. It can be a local user or a user authenticated with SSO. + When SSO is used, the `user` will be based on the `sub` claims, while the group is one of the values returned by the `scopes` configuration. +- ``: The internal role to which the entity will be assigned. + +**Policy**: Allows to assign permissions to an entity. + +Syntax: `p, , , , , ` + +- ``: The entity to whom the policy will be assigned +- ``: The type of resource on which the action is performed. +- ``: The operation that is being performed on the resource. +- ``: The object identifier representing the resource on which the action is performed. Depending on the resource, the object's format will vary. +- ``: Whether this policy should grant or restrict the operation on the target object. One of `allow` or `deny`. + +Below is a table that summarizes all possible resources and which actions are valid for each of them. + +| Resource\Action | get | create | update | delete | sync | action | override | invoke | +| :------------------ | :-: | :----: | :----: | :----: | :--: | :----: | :------: | :----: | +| **applications** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| **applicationsets** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **clusters** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **projects** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **repositories** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **accounts** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **certificates** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **gpgkeys** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **logs** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **exec** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **extensions** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | + +### Application-Specific Policy + +Some policy only have meaning within an application. It is the case with the following resources: + +- `applications` +- `applicationsets` +- `logs` +- `exec` + +While they can be set in the global configuration, they can also be configured in [AppProject's roles](../user-guide/projects.md#project-roles). +The expected `` value in the policy structure is replaced by `/`. + +For instance, these policies would grant `example-user` access to get any applications, +but only be able to see logs in `my-app` application part of the `example-project` project. + +```csv +p, example-user, applications, get, *, allow +p, example-user, logs, get, example-project/my-app, allow +``` + +#### Application in Any Namespaces -Breaking down the permissions definition differs slightly between applications and every other resource type in Argo CD. +When [application in any namespace](app-any-namespace.md) is enabled, the expected `` value in the policy structure is replaced by `//`. +Since multiple applications could have the same name in the same project, the policy below makes sure to restrict access only to `app-namespace`. -* All resources *except* application-specific permissions (see next bullet): +```csv +p, example-user, applications, get, */app-namespace/*, allow +p, example-user, logs, get, example-project/app-namespace/my-app, allow +``` + +### The `applications` resource + +The `applications` resource is an [Application-Specific Policy](#application-specific-policy). - `p, , , , ` +#### Fine-grained Permissions for `update`/`delete` action -* Applications, applicationsets, logs, and exec (which belong to an `AppProject`): +The `update` and `delete` actions, when granted on an application, will allow the user to perform the operation on the application itself **and** all of its resources. +It can be desirable to only allow `update` or `delete` on specific resources within an application. - `p, , , , /` +To do so, when the action if performed on an application's resource, the `` will have the `////` format. + +For instance, to grant access to `example-user` to only delete Pods in the `prod-app` Application, the policy could be: + +```csv +p, example-user, applications, delete/*/Pod/*, default/prod-app, allow +``` -### RBAC Resources and Actions +If we want to grant access to the user to update all resources of an application, but not the application itself: -Resources: `clusters`, `projects`, `applications`, `applicationsets`, -`repositories`, `certificates`, `accounts`, `gpgkeys`, `logs`, `exec`, -`extensions` +```csv +p, example-user, applications, update/*, default/prod-app, allow +``` -Actions: `get`, `create`, `update`, `delete`, `sync`, `override`,`action/` +If we want to explicitly deny delete of the application, but allow the user to delete Pods: -Note that `sync`, `override`, and `action/` only have meaning for the `applications` resource. +```csv +p, example-user, applications, delete, default/prod-app, deny +p, example-user, applications, delete/*/Pod/*, default/prod-app, allow +``` -#### Application resources +!!! note -The resource path for application objects is of the form -`/`. + It is not possible to deny fine-grained permissions for a sub-resource if the action was **explicitly allowed on the application**. + For instance, the following policies will **allow** a user to delete the Pod and any other resources in the application: -Delete access to sub-resources of a project, such as a rollout or a pod, cannot -be managed granularly. `/` grants access to all -subresources of an application. + ```csv + p, example-user, applications, delete, default/prod-app, allow + p, example-user, applications, delete/*/Pod/*, default/prod-app, deny + ``` #### The `action` action The `action` action corresponds to either built-in resource customizations defined [in the Argo CD repository](https://github.com/argoproj/argo-cd/tree/master/resource_customizations), or to [custom resource actions](resource_actions.md#custom-resource-actions) defined by you. -The `action` path is of the form `action///`. For -example, a resource customization path -`resource_customizations/extensions/DaemonSet/actions/restart/action.lua` -corresponds to the `action` path `action/extensions/DaemonSet/restart`. You can -also use glob patterns in the action path: `action/*` (or regex patterns if you have -[enabled the `regex` match mode](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-rbac-cm.yaml)). -If the resource is not under a group (for examples, Pods or ConfigMaps), then omit the group name from your RBAC -configuration: +The `` has the `action///` format. + +For example, a resource customization path `resource_customizations/extensions/DaemonSet/actions/restart/action.lua` +corresponds to the `action` path `action/extensions/DaemonSet/restart`. If the resource is not under a group (for example, Pods or ConfigMaps), +then the path will be `action//Pod/action-name`. + +The following policies allows the user to perform any action on the DaemonSet resources, as well as the `maintenance-off` action on a Pod: ```csv p, example-user, applications, action//Pod/maintenance-off, default/*, allow +p, example-user, applications, action/extensions/DaemonSet/*, default/*, allow ``` -#### The `exec` resource +To allow the user to perform any actions: -`exec` is a special resource. When enabled with the `create` action, this privilege allows a user to `exec` into Pods via -the Argo CD UI. The functionality is similar to `kubectl exec`. +```csv +p, example-user, applications, action/*, default/*, allow +``` -See [Web-based Terminal](web_based_terminal.md) for more info. +#### The `override` action -#### The `applicationsets` resource +When granted along with the `sync` action, the override action will allow a user to synchronize local manifests to the Application. +These manifests will be used instead of the configured source, until the next sync is performed. + +### The `applicationsets` resource + +The `applicationsets` resource is an [Application-Specific policy](#application-specific-policy). [ApplicationSets](applicationset/index.md) provide a declarative way to automatically create/update/delete Applications. -Granting `applicationsets, create` effectively grants the ability to create Applications. While it doesn't allow the +Allowing the `create` action on the resource effectively grants the ability to create Applications. While it doesn't allow the user to create Applications directly, they can create Applications via an ApplicationSet. -In v2.5, it is not possible to create an ApplicationSet with a templated Project field (e.g. `project: {{path.basename}}`) -via the API (or, by extension, the CLI). Disallowing templated projects makes project restrictions via RBAC safe: +!!! note + + In v2.5, it is not possible to create an ApplicationSet with a templated Project field (e.g. `project: {{path.basename}}`) + via the API (or, by extension, the CLI). Disallowing templated projects makes project restrictions via RBAC safe: + +With the resource being application-specific, the `` of the applicationsets policy will have the format `/`. +However, since an ApplicationSet does belong to any project, the `` value represents the projects in which the ApplicationSet will be able to create Applications. + +With the following policy, a `dev-group` user will be unable to create an ApplicationSet capable of creating Applications +outside the `dev-project` project. ```csv p, dev-group, applicationsets, *, dev-project/*, allow ``` -With this rule in place, a `dev-group` user will be unable to create an ApplicationSet capable of creating Applications -outside the `dev-project` project. +### The `logs` resource -#### The `extensions` resource +The `logs` resource is an [Application-Specific Policy](#application-specific-policy). -With the `extensions` resource it is possible configure permissions to -invoke [proxy -extensions](../developer-guide/extensions/proxy-extensions.md). The -`extensions` RBAC validation works in conjunction with the -`applications` resource. A user logged in Argo CD (UI or CLI), needs -to have at least read permission on the project, namespace and -application where the request is originated from. +When granted with the `get` action, this policy allows a user to see Pod's logs of an application via +the Argo CD UI. The functionality is similar to `kubectl logs`. -Consider the example below: +### The `exec` resource + +The `exec` resource is an [Application-Specific Policy](#application-specific-policy). + +When granted with the `create` action, this policy allows a user to `exec` into Pods of an application via +the Argo CD UI. The functionality is similar to `kubectl exec`. + +See [Web-based Terminal](web_based_terminal.md) for more info. + +### The `extensions` resource + +With the `extensions` resource, it is possible to configure permissions to invoke [proxy extensions](../developer-guide/extensions/proxy-extensions.md). +The `extensions` RBAC validation works in conjunction with the `applications` resource. +A user **needs to have read permission on the application** where the request is originated from. + +Consider the example below, it will allow the `example-user` to invoke the `httpbin` extensions in all +applications under the `default` project. ```csv -g, ext, role:extension -p, role:extension, applications, get, default/httpbin-app, allow -p, role:extension, extensions, invoke, httpbin, allow +p, example-user, applications, get, default/*, allow +p, example-user, extensions, invoke, httpbin, allow ``` -Explanation: +### The `deny` effect -* *line1*: defines the group `role:extension` associated with the - subject `ext`. -* *line2*: defines a policy allowing this role to read (`get`) the - `httpbin-app` application in the `default` project. -* *line3*: defines another policy allowing this role to `invoke` the - `httpbin` extension. +When `deny` is used as an effect in a policy, it will be effective if the policy matches. +Even if more specific policies with the `allow` effect match as well, the `deny` will have priority. -**Note 1**: that for extensions requests to be allowed, the policy defined -in the *line2* is also required. +The order in which the policies appears in the policy file configuration has no impact, and the result is deterministic. -**Note 2**: `invoke` is a new action introduced specifically to be used -with the `extensions` resource. The current actions for `extensions` -are `*` or `invoke`. +## Policies Evaluation and Matching -## Tying It All Together +The evaluation of access is done in two parts: validating against the default policy configuration, then validating against the policies for the current user. -Additional roles and groups can be configured in `argocd-rbac-cm` ConfigMap. The example below -configures a custom role, named `org-admin`. The role is assigned to any user which belongs to -`your-github-org:your-team` group. All other users get the default policy of `role:readonly`, -which cannot modify Argo CD settings. +**If an action is allowed or denied by the default policies, then this effect will be effective without further evaluation**. +When the effect is undefined, the evaluation will continue with subject-specific policies. -!!! warning - All authenticated users get *at least* the permissions granted by the default policy. This access cannot be blocked - by a `deny` rule. Instead, restrict the default policy and then grant permissions to individual roles as needed. +The access will be evaluated for the user, then for each configured group that the user is part of. -*ArgoCD ConfigMap `argocd-rbac-cm` Example:* +The matching engine, configured in `policy.matchMode`, can use two different match modes to compare the values of tokens: -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: argocd-rbac-cm - namespace: argocd -data: - policy.default: role:readonly - policy.csv: | - p, role:org-admin, applications, *, */*, allow - p, role:org-admin, clusters, get, *, allow - p, role:org-admin, repositories, get, *, allow - p, role:org-admin, repositories, create, *, allow - p, role:org-admin, repositories, update, *, allow - p, role:org-admin, repositories, delete, *, allow - p, role:org-admin, projects, get, *, allow - p, role:org-admin, projects, create, *, allow - p, role:org-admin, projects, update, *, allow - p, role:org-admin, projects, delete, *, allow - p, role:org-admin, logs, get, *, allow - p, role:org-admin, exec, create, */*, allow - - g, your-github-org:your-team, role:org-admin -``` +- `glob`: based on the [`glob` package](https://pkg.go.dev/github.com/gobwas/glob). +- `regex`: based on the [`regexp` package](https://pkg.go.dev/regexp). ----- +When all tokens match during the evaluation, the effect will be returned. The evaluation will continue until all matching policies are evaluated, or until a policy with the `deny` effect matches. +After all policies are evaluated, if there was at least one `allow` effect and no `deny`, access will be granted. -Another `policy.csv` example might look as follows: +### Glob matching -```csv -p, role:staging-db-admin, applications, create, staging-db-project/*, allow -p, role:staging-db-admin, applications, delete, staging-db-project/*, allow -p, role:staging-db-admin, applications, get, staging-db-project/*, allow -p, role:staging-db-admin, applications, override, staging-db-project/*, allow -p, role:staging-db-admin, applications, sync, staging-db-project/*, allow -p, role:staging-db-admin, applications, update, staging-db-project/*, allow -p, role:staging-db-admin, logs, get, staging-db-project/*, allow -p, role:staging-db-admin, exec, create, staging-db-project/*, allow -p, role:staging-db-admin, projects, get, staging-db-project, allow -g, db-admins, role:staging-db-admin +When `glob` is used, the policy tokens are treated as single terms, without separators. + +Consider the following policy: + +``` +p, example-user, applications, action/extensions/*, default/*, allow ``` -This example defines a *role* called `staging-db-admin` with nine *permissions* that allow users with that role to perform the following *actions*: +When the `example-user` executes the `extensions/DaemonSet/test` action, the following `glob` matches will happen: -* `create`, `delete`, `get`, `override`, `sync` and `update` for applications in the `staging-db-project` project, -* `get` logs for objects in the `staging-db-project` project, -* `create` exec for objects in the `staging-db-project` project, and -* `get` for the project named `staging-db-project`. +1. The current user `example-user` matches the token `example-user`. +2. The value `applications` matches the token `applications`. +3. The value `action/extensions/DaemonSet/test` matches `action/extensions/*`. Note that `/` is not treated as a separator and the use of `**` is not necessary. +4. The value `default/my-app` matches `default/*`. -!!! note - The `scopes` field controls which OIDC scopes to examine during rbac - enforcement (in addition to `sub` scope). If omitted, defaults to: - `'[groups]'`. The scope value can be a string, or a list of strings. +## Using SSO Users/Groups + +The `scopes` field controls which OIDC scopes to examine during RBAC enforcement (in addition to `sub` scope). +If omitted, it defaults to `'[groups]'`. The scope value can be a string, or a list of strings. + +For more information on `scopes` please review the [User Management Documentation](user-management/index.md). -Following example shows targeting `email` as well as `groups` from your OIDC provider. +The following example shows targeting `email` as well as `groups` from your OIDC provider. ```yaml apiVersion: v1 @@ -209,12 +300,29 @@ data: scopes: '[groups, email]' ``` -For more information on `scopes` please review the [User Management Documentation](user-management/index.md). +This can be useful to associate users' emails and groups directly in AppProject. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: team-beta-project + namespace: argocd +spec: + roles: + - name: admin + description: Admin privileges to team-beta + policies: + - p, proj:team-beta-project:admin, applications, *, *, allow + groups: + - user@example.org # Value from the email scope + - my-org:team-beta # Value from the groups scope +``` ## Local Users/Accounts -[Local users](user-management/index.md#local-usersaccounts) are assigned access by either grouping them with a role or by assigning policies directly -to them. +[Local users](user-management/index.md#local-usersaccounts) are assigned access by either grouping them with a role or by assigning policies directly +to them. The example below shows how to assign a policy directly to a local user. @@ -228,18 +336,19 @@ This example shows how to assign a role to a local user. g, my-local-user, role:admin ``` -!!!warning "Ambiguous Group Assignments" - If you have [enabled SSO](user-management/index.md#sso), any SSO user with a scope that matches a local user will be - added to the same roles as the local user. For example, if local user `sally` is assigned to `role:admin`, and if an +!!! warning "Ambiguous Group Assignments" + + If you have [enabled SSO](user-management/index.md#sso), any SSO user with a scope that matches a local user will be + added to the same roles as the local user. For example, if local user `sally` is assigned to `role:admin`, and if an SSO user has a scope which happens to be named `sally`, that SSO user will also be assigned to `role:admin`. An example of where this may be a problem is if your SSO provider is an SCM, and org members are automatically granted scopes named after the orgs. If a user can create or add themselves to an org in the SCM, they can gain the permissions of the local user with the same name. - To avoid ambiguity, if you are using local users and SSO, it is recommended to assign permissions directly to local + To avoid ambiguity, if you are using local users and SSO, it is recommended to assign policies directly to local users, and not to assign roles to local users. In other words, instead of using `g, my-local-user, role:admin`, you - should explicitly assign permissions to `my-local-user`: + should explicitly assign policies to `my-local-user`: ```yaml p, my-local-user, *, *, *, allow @@ -247,20 +356,17 @@ g, my-local-user, role:admin ## Policy CSV Composition -It is possible to provide additional entries in the `argocd-rbac-cm` -configmap to compose the final policy csv. In this case the key must -follow the pattern `policy..csv`. Argo CD will concatenate -all additional policies it finds with this pattern below the main one -('policy.csv'). The order of additional provided policies are -determined by the key string. Example: if two additional policies are -provided with keys `policy.A.csv` and `policy.B.csv`, it will first -concatenate `policy.A.csv` and then `policy.B.csv`. +It is possible to provide additional entries in the `argocd-rbac-cm` configmap to compose the final policy csv. +In this case, the key must follow the pattern `policy..csv`. +Argo CD will concatenate all additional policies it finds with this pattern below the main one ('policy.csv'). +The order of additional provided policies are determined by the key string. -This is useful to allow composing policies in config management tools -like Kustomize, Helm, etc. +Example: if two additional policies are provided with keys `policy.A.csv` and `policy.B.csv`, +it will first concatenate `policy.A.csv` and then `policy.B.csv`. -The example below shows how a Kustomize patch can be provided in an -overlay to add additional configuration to an existing RBAC policy. +This is useful to allow composing policies in config management tools like Kustomize, Helm, etc. + +The example below shows how a Kustomize patch can be provided in an overlay to add additional configuration to an existing RBAC ConfigMap. ```yaml apiVersion: v1 @@ -275,96 +381,21 @@ data: g, my-org:team-qa, role:tester ``` -## Anonymous Access - -The anonymous access to Argo CD can be enabled using `users.anonymous.enabled` field in `argocd-cm` (see [argocd-cm.yaml](argocd-cm.yaml)). -The anonymous users get default role permissions specified by `policy.default` in `argocd-rbac-cm.yaml`. For read-only access you'll want `policy.default: role:readonly` as above - ## Validating and testing your RBAC policies If you want to ensure that your RBAC policies are working as expected, you can -use the `argocd admin settings rbac` command to validate them. This tool allows you to -test whether a certain role or subject can perform the requested action with a -policy that's not live yet in the system, i.e. from a local file or config map. -Additionally, it can be used against the live policy in the cluster your Argo -CD is running in. - -To check whether your new policy is valid and understood by Argo CD's RBAC -implementation, you can use the `argocd admin settings rbac validate` command. +use the [`argocd admin settings rbac` command](../user-guide/commands/argocd_admin_settings_rbac.md) to validate them. +This tool allows you to test whether a certain role or subject can perform the requested action with a policy +that's not live yet in the system, i.e. from a local file or config map. +Additionally, it can be used against the live RBAC configuration in the cluster your Argo CD is running in. ### Validating a policy -To validate a policy stored in a local text file: - -```shell -argocd admin settings rbac validate --policy-file somepolicy.csv -``` - -To validate a policy stored in a local K8s ConfigMap definition in a YAML file: - -```shell -argocd admin settings rbac validate --policy-file argocd-rbac-cm.yaml -``` - -To validate a policy stored in K8s, used by Argo CD in namespace `argocd`, -ensure that your current context in `~/.kube/config` is pointing to your -Argo CD cluster and give appropriate namespace: - -```shell -argocd admin settings rbac validate --namespace argocd -``` +To check whether your new policy configuration is valid and understood by Argo CD's RBAC implementation, +you can use the [`argocd admin settings rbac validate` command](../user-guide/commands/argocd_admin_settings_rbac_validate.md). ### Testing a policy To test whether a role or subject (group or local user) has sufficient permissions to execute certain actions on certain resources, you can -use the `argocd admin settings rbac can` command. Its general syntax is - -```shell -argocd admin settings rbac can SOMEROLE ACTION RESOURCE SUBRESOURCE [flags] -``` - -Given the example from the above ConfigMap, which defines the role -`role:org-admin`, and is stored on your local system as `argocd-rbac-cm-yaml`, -you can test whether that role can do something like follows: - -```console -$ argocd admin settings rbac can role:org-admin get applications --policy-file argocd-rbac-cm.yaml -Yes - -$ argocd admin settings rbac can role:org-admin get clusters --policy-file argocd-rbac-cm.yaml -Yes - -$ argocd admin settings rbac can role:org-admin create clusters 'somecluster' --policy-file argocd-rbac-cm.yaml -No - -$ argocd admin settings rbac can role:org-admin create applications 'someproj/someapp' --policy-file argocd-rbac-cm.yaml -Yes -``` - -Another example, given the policy above from `policy.csv`, which defines the -role `role:staging-db-admin` and associates the group `db-admins` with it. -Policy is stored locally as `policy.csv`: - -You can test against the role: - -```console -$ # Plain policy, without a default role defined -$ argocd admin settings rbac can role:staging-db-admin get applications --policy-file policy.csv -No - -$ argocd admin settings rbac can role:staging-db-admin get applications 'staging-db-project/*' --policy-file policy.csv -Yes - -$ # Argo CD augments a builtin policy with two roles defined, the default role -$ # being 'role:readonly' - You can include a named default role to use: -$ argocd admin settings rbac can role:staging-db-admin get applications --policy-file policy.csv --default-role role:readonly -Yes -``` - -Or against the group defined: - -```console -$ argocd admin settings rbac can db-admins get applications 'staging-db-project/*' --policy-file policy.csv -Yes -``` +use the [`argocd admin settings rbac can` command](../user-guide/commands/argocd_admin_settings_rbac_can.md). diff --git a/docs/user-guide/commands/argocd_admin_settings_rbac_validate.md b/docs/user-guide/commands/argocd_admin_settings_rbac_validate.md index b051c7c63694b..4be305e40a33c 100644 --- a/docs/user-guide/commands/argocd_admin_settings_rbac_validate.md +++ b/docs/user-guide/commands/argocd_admin_settings_rbac_validate.md @@ -26,8 +26,8 @@ argocd admin settings rbac validate --policy-file policy.csv # i.e. 'policy.csv' and (optionally) 'policy.default' argocd admin settings rbac validate --policy-file argocd-rbac-cm.yaml -# If --policy-file is not given, and instead --namespace is giventhe ConfigMap 'argocd-rbac-cm' -# from K8s is used. +# If --policy-file is not given, and instead --namespace is giventhe ConfigMap 'argocd-rbac-cm' +# from K8s is used. argocd admin settings rbac validate --namespace argocd # Either --policy-file or --namespace must be given. diff --git a/server/account/account_test.go b/server/account/account_test.go index d65c2e925b63d..367f3aa080767 100644 --- a/server/account/account_test.go +++ b/server/account/account_test.go @@ -82,7 +82,7 @@ func getAdminAccount(mgr *settings.SettingsManager) (*settings.Account, error) { func adminContext(ctx context.Context) context.Context { // nolint:staticcheck - return context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin", Issuer: sessionutil.SessionManagerClaimsIssuer}) + return context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin", Issuer: sessionutil.SessionManagerClaimsIssuer}) } func ssoAdminContext(ctx context.Context, iat time.Time) context.Context { diff --git a/server/application/application.go b/server/application/application.go index aa2ba1cbf1de7..62db428fecf8c 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -1324,9 +1324,15 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) { a, _, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) + if err == permissionDeniedErr && (action == rbacpolicy.ActionDelete || action == rbacpolicy.ActionUpdate) { + // If users dont have permission on the whole applications, maybe they have fine-grained access to the specific resources + action = fmt.Sprintf("%s/%s/%s/%s/%s", action, q.GetGroup(), q.GetKind(), q.GetNamespace(), q.GetResourceName()) + a, _, err = s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) + } if err != nil { return nil, nil, nil, err } + tree, err := s.getAppResources(ctx, a) if err != nil { return nil, nil, nil, fmt.Errorf("error getting app resources: %w", err) diff --git a/server/application/application_test.go b/server/application/application_test.go index a101f7d8f9479..d084022beea6f 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1578,6 +1578,132 @@ func TestDeleteApp(t *testing.T) { }) } +func TestDeleteResourcesRBAC(t *testing.T) { + ctx := context.Background() + // nolint:staticcheck + ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "test-user"}) + testApp := newTestApp() + appServer := newTestAppServer(t, testApp) + appServer.enf.SetDefaultRole("") + + req := application.ApplicationResourceDeleteRequest{ + Name: &testApp.Name, + AppNamespace: &testApp.Namespace, + Group: strToPtr("fake.io"), + Kind: strToPtr("PodTest"), + Namespace: strToPtr("fake-ns"), + ResourceName: strToPtr("my-pod-test"), + } + + expectedErrorWhenDeleteAllowed := "rpc error: code = InvalidArgument desc = PodTest fake.io my-pod-test not found as part of application test-app" + + t.Run("delete with application permission", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, delete, default/test-app, allow +`) + _, err := appServer.DeleteResource(ctx, &req) + assert.Equal(t, expectedErrorWhenDeleteAllowed, err.Error()) + }) + + t.Run("delete with application permission but deny subresource", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, delete, default/test-app, allow +p, test-user, applications, delete/*, default/test-app, deny +`) + _, err := appServer.DeleteResource(ctx, &req) + assert.Equal(t, expectedErrorWhenDeleteAllowed, err.Error()) + }) + + t.Run("delete with subresource", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, delete/*, default/test-app, allow +`) + _, err := appServer.DeleteResource(ctx, &req) + assert.Equal(t, expectedErrorWhenDeleteAllowed, err.Error()) + }) + + t.Run("delete with subresource but deny applications", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, delete, default/test-app, deny +p, test-user, applications, delete/*, default/test-app, allow +`) + _, err := appServer.DeleteResource(ctx, &req) + assert.Equal(t, expectedErrorWhenDeleteAllowed, err.Error()) + }) + + t.Run("delete with specific subresource denied", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, delete/*, default/test-app, allow +p, test-user, applications, delete/fake.io/PodTest/*, default/test-app, deny +`) + _, err := appServer.DeleteResource(ctx, &req) + assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) + }) +} + +func TestPatchResourcesRBAC(t *testing.T) { + ctx := context.Background() + // nolint:staticcheck + ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "test-user"}) + testApp := newTestApp() + appServer := newTestAppServer(t, testApp) + appServer.enf.SetDefaultRole("") + + req := application.ApplicationResourcePatchRequest{ + Name: &testApp.Name, + AppNamespace: &testApp.Namespace, + Group: strToPtr("fake.io"), + Kind: strToPtr("PodTest"), + Namespace: strToPtr("fake-ns"), + ResourceName: strToPtr("my-pod-test"), + } + + expectedErrorWhenUpdateAllowed := "rpc error: code = InvalidArgument desc = PodTest fake.io my-pod-test not found as part of application test-app" + + t.Run("patch with application permission", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, update, default/test-app, allow +`) + _, err := appServer.PatchResource(ctx, &req) + assert.Equal(t, expectedErrorWhenUpdateAllowed, err.Error()) + }) + + t.Run("patch with application permission but deny subresource", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, update, default/test-app, allow +p, test-user, applications, update/*, default/test-app, deny +`) + _, err := appServer.PatchResource(ctx, &req) + assert.Equal(t, expectedErrorWhenUpdateAllowed, err.Error()) + }) + + t.Run("patch with subresource", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, update/*, default/test-app, allow +`) + _, err := appServer.PatchResource(ctx, &req) + assert.Equal(t, expectedErrorWhenUpdateAllowed, err.Error()) + }) + + t.Run("patch with subresource but deny applications", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, update, default/test-app, deny +p, test-user, applications, update/*, default/test-app, allow +`) + _, err := appServer.PatchResource(ctx, &req) + assert.Equal(t, expectedErrorWhenUpdateAllowed, err.Error()) + }) + + t.Run("patch with specific subresource denied", func(t *testing.T) { + _ = appServer.enf.SetBuiltinPolicy(` +p, test-user, applications, update/*, default/test-app, allow +p, test-user, applications, update/fake.io/PodTest/*, default/test-app, deny +`) + _, err := appServer.PatchResource(ctx, &req) + assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) + }) +} + func TestSyncAndTerminate(t *testing.T) { ctx := context.Background() appServer := newTestAppServer(t) @@ -1696,7 +1822,7 @@ func TestUpdateAppProject(t *testing.T) { testApp := newTestApp() ctx := context.Background() // nolint:staticcheck - ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) + ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin"}) appServer := newTestAppServer(t, testApp) appServer.enf.SetDefaultRole("") @@ -1760,7 +1886,7 @@ func TestAppJsonPatch(t *testing.T) { testApp := newTestAppWithAnnotations() ctx := context.Background() // nolint:staticcheck - ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) + ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin"}) appServer := newTestAppServer(t, testApp) appServer.enf.SetDefaultRole("") @@ -1785,7 +1911,7 @@ func TestAppMergePatch(t *testing.T) { testApp := newTestApp() ctx := context.Background() // nolint:staticcheck - ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) + ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin"}) appServer := newTestAppServer(t, testApp) appServer.enf.SetDefaultRole("")