github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/settings_rbac.go (about) 1 package admin 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 9 log "github.com/sirupsen/logrus" 10 "github.com/spf13/cobra" 11 corev1 "k8s.io/api/core/v1" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/client-go/kubernetes" 14 "k8s.io/client-go/tools/clientcmd" 15 "sigs.k8s.io/yaml" 16 17 "github.com/argoproj/argo-cd/v3/common" 18 "github.com/argoproj/argo-cd/v3/util/assets" 19 "github.com/argoproj/argo-cd/v3/util/cli" 20 "github.com/argoproj/argo-cd/v3/util/rbac" 21 ) 22 23 type actionTraitMap map[string]rbacTrait 24 25 type rbacTrait struct { 26 allowPath bool 27 } 28 29 // Provide a mapping of shorthand resource names to their RBAC counterparts 30 var resourceMap = map[string]string{ 31 "account": rbac.ResourceAccounts, 32 "app": rbac.ResourceApplications, 33 "apps": rbac.ResourceApplications, 34 "application": rbac.ResourceApplications, 35 "applicationsets": rbac.ResourceApplicationSets, 36 "cert": rbac.ResourceCertificates, 37 "certs": rbac.ResourceCertificates, 38 "certificate": rbac.ResourceCertificates, 39 "cluster": rbac.ResourceClusters, 40 "extension": rbac.ResourceExtensions, 41 "gpgkey": rbac.ResourceGPGKeys, 42 "key": rbac.ResourceGPGKeys, 43 "log": rbac.ResourceLogs, 44 "logs": rbac.ResourceLogs, 45 "exec": rbac.ResourceExec, 46 "proj": rbac.ResourceProjects, 47 "projs": rbac.ResourceProjects, 48 "project": rbac.ResourceProjects, 49 "repo": rbac.ResourceRepositories, 50 "repos": rbac.ResourceRepositories, 51 "repository": rbac.ResourceRepositories, 52 } 53 54 // List of allowed RBAC resources 55 var validRBACResourcesActions = map[string]actionTraitMap{ 56 rbac.ResourceAccounts: accountsActions, 57 rbac.ResourceApplications: applicationsActions, 58 rbac.ResourceApplicationSets: defaultCRUDActions, 59 rbac.ResourceCertificates: defaultCRDActions, 60 rbac.ResourceClusters: defaultCRUDActions, 61 rbac.ResourceExtensions: extensionActions, 62 rbac.ResourceGPGKeys: defaultCRDActions, 63 rbac.ResourceLogs: logsActions, 64 rbac.ResourceExec: execActions, 65 rbac.ResourceProjects: defaultCRUDActions, 66 rbac.ResourceRepositories: defaultCRUDActions, 67 } 68 69 // List of allowed RBAC actions 70 var defaultCRUDActions = actionTraitMap{ 71 rbac.ActionCreate: rbacTrait{}, 72 rbac.ActionGet: rbacTrait{}, 73 rbac.ActionUpdate: rbacTrait{}, 74 rbac.ActionDelete: rbacTrait{}, 75 } 76 77 var defaultCRDActions = actionTraitMap{ 78 rbac.ActionCreate: rbacTrait{}, 79 rbac.ActionGet: rbacTrait{}, 80 rbac.ActionDelete: rbacTrait{}, 81 } 82 83 var applicationsActions = actionTraitMap{ 84 rbac.ActionCreate: rbacTrait{}, 85 rbac.ActionGet: rbacTrait{}, 86 rbac.ActionUpdate: rbacTrait{allowPath: true}, 87 rbac.ActionDelete: rbacTrait{allowPath: true}, 88 rbac.ActionAction: rbacTrait{allowPath: true}, 89 rbac.ActionOverride: rbacTrait{}, 90 rbac.ActionSync: rbacTrait{}, 91 } 92 93 var accountsActions = actionTraitMap{ 94 rbac.ActionCreate: rbacTrait{}, 95 rbac.ActionUpdate: rbacTrait{}, 96 } 97 98 var execActions = actionTraitMap{ 99 rbac.ActionCreate: rbacTrait{}, 100 } 101 102 var logsActions = actionTraitMap{ 103 rbac.ActionGet: rbacTrait{}, 104 } 105 106 var extensionActions = actionTraitMap{ 107 rbac.ActionInvoke: rbacTrait{}, 108 } 109 110 // NewRBACCommand is the command for 'rbac' 111 func NewRBACCommand() *cobra.Command { 112 command := &cobra.Command{ 113 Use: "rbac", 114 Short: "Validate and test RBAC configuration", 115 Run: func(c *cobra.Command, args []string) { 116 c.HelpFunc()(c, args) 117 }, 118 } 119 command.AddCommand(NewRBACCanCommand()) 120 command.AddCommand(NewRBACValidateCommand()) 121 return command 122 } 123 124 // NewRBACCanCommand is the command for 'rbac can' 125 func NewRBACCanCommand() *cobra.Command { 126 var ( 127 policyFile string 128 defaultRole string 129 useBuiltin bool 130 strict bool 131 quiet bool 132 subject string 133 action string 134 resource string 135 subResource string 136 clientConfig clientcmd.ClientConfig 137 ) 138 command := &cobra.Command{ 139 Use: "can ROLE/SUBJECT ACTION RESOURCE [SUB-RESOURCE]", 140 Short: "Check RBAC permissions for a role or subject", 141 Long: ` 142 Check whether a given role or subject has appropriate RBAC permissions to do 143 something. 144 `, 145 Example: ` 146 # Check whether role some:role has permissions to create an application in the 147 # 'default' project, using a local policy.csv file 148 argocd admin settings rbac can some:role create application 'default/app' --policy-file policy.csv 149 150 # Policy file can also be K8s config map with data keys like argocd-rbac-cm, 151 # i.e. 'policy.csv' and (optionally) 'policy.default' 152 argocd admin settings rbac can some:role create application 'default/app' --policy-file argocd-rbac-cm.yaml 153 154 # If --policy-file is not given, the ConfigMap 'argocd-rbac-cm' from K8s is 155 # used. You need to specify the argocd namespace, and make sure that your 156 # current Kubernetes context is pointing to the cluster Argo CD is running in 157 argocd admin settings rbac can some:role create application 'default/app' --namespace argocd 158 159 # You can override a possibly configured default role 160 argocd admin settings rbac can someuser create application 'default/app' --default-role role:readonly 161 162 `, 163 Run: func(c *cobra.Command, args []string) { 164 ctx := c.Context() 165 166 if len(args) < 3 || len(args) > 4 { 167 c.HelpFunc()(c, args) 168 os.Exit(1) 169 } 170 subject = args[0] 171 action = args[1] 172 resource = args[2] 173 if len(args) > 3 { 174 subResource = args[3] 175 } 176 177 namespace, nsOverride, err := clientConfig.Namespace() 178 if err != nil { 179 log.Fatalf("could not create k8s client: %v", err) 180 } 181 182 // Exactly one of --namespace or --policy-file must be given. 183 if (!nsOverride && policyFile == "") || (nsOverride && policyFile != "") { 184 c.HelpFunc()(c, args) 185 log.Fatalf("please provide exactly one of --policy-file or --namespace") 186 } 187 188 restConfig, err := clientConfig.ClientConfig() 189 if err != nil { 190 log.Fatalf("could not create k8s client: %v", err) 191 } 192 realClientset, err := kubernetes.NewForConfig(restConfig) 193 if err != nil { 194 log.Fatalf("could not create k8s client: %v", err) 195 } 196 197 userPolicy, newDefaultRole, matchMode := getPolicy(ctx, policyFile, realClientset, namespace) 198 199 // Use built-in policy as augmentation if requested 200 builtinPolicy := "" 201 if useBuiltin { 202 builtinPolicy = assets.BuiltinPolicyCSV 203 } 204 205 // If no explicit default role was given, but we have one defined from 206 // a policy, use this to check for enforce. 207 if newDefaultRole != "" && defaultRole == "" { 208 defaultRole = newDefaultRole 209 } 210 211 res := checkPolicy(subject, action, resource, subResource, builtinPolicy, userPolicy, defaultRole, matchMode, strict) 212 if res { 213 if !quiet { 214 fmt.Println("Yes") 215 } 216 os.Exit(0) 217 } 218 if !quiet { 219 fmt.Println("No") 220 } 221 os.Exit(1) 222 }, 223 } 224 clientConfig = cli.AddKubectlFlagsToCmd(command) 225 command.Flags().StringVar(&policyFile, "policy-file", "", "path to the policy file to use") 226 command.Flags().StringVar(&defaultRole, "default-role", "", "name of the default role to use") 227 command.Flags().BoolVar(&useBuiltin, "use-builtin-policy", true, "whether to also use builtin-policy") 228 command.Flags().BoolVar(&strict, "strict", true, "whether to perform strict check on action and resource names") 229 command.Flags().BoolVarP(&quiet, "quiet", "q", false, "quiet mode - do not print results to stdout") 230 return command 231 } 232 233 // NewRBACValidateCommand returns a new rbac validate command 234 func NewRBACValidateCommand() *cobra.Command { 235 var ( 236 policyFile string 237 namespace string 238 clientConfig clientcmd.ClientConfig 239 ) 240 241 command := &cobra.Command{ 242 Use: "validate [--policy-file POLICYFILE] [--namespace NAMESPACE]", 243 Short: "Validate RBAC policy", 244 Long: ` 245 Validates an RBAC policy for being syntactically correct. The policy must be 246 a local file or a K8s ConfigMap in the provided namespace, and in either CSV or K8s ConfigMap format. 247 `, 248 Example: ` 249 # Check whether a given policy file is valid using a local policy.csv file. 250 argocd admin settings rbac validate --policy-file policy.csv 251 252 # Policy file can also be K8s config map with data keys like argocd-rbac-cm, 253 # i.e. 'policy.csv' and (optionally) 'policy.default' 254 argocd admin settings rbac validate --policy-file argocd-rbac-cm.yaml 255 256 # If --policy-file is not given, and instead --namespace is giventhe ConfigMap 'argocd-rbac-cm' 257 # from K8s is used. 258 argocd admin settings rbac validate --namespace argocd 259 260 # Either --policy-file or --namespace must be given. 261 `, 262 Run: func(c *cobra.Command, args []string) { 263 ctx := c.Context() 264 265 if len(args) > 0 { 266 c.HelpFunc()(c, args) 267 log.Fatalf("too many arguments") 268 } 269 270 if (namespace == "" && policyFile == "") || (namespace != "" && policyFile != "") { 271 c.HelpFunc()(c, args) 272 log.Fatalf("please provide exactly one of --policy-file or --namespace") 273 } 274 275 restConfig, err := clientConfig.ClientConfig() 276 if err != nil { 277 log.Fatalf("could not get config to create k8s client: %v", err) 278 } 279 realClientset, err := kubernetes.NewForConfig(restConfig) 280 if err != nil { 281 log.Fatalf("could not create k8s client: %v", err) 282 } 283 284 userPolicy, _, _ := getPolicy(ctx, policyFile, realClientset, namespace) 285 if userPolicy != "" { 286 if err := rbac.ValidatePolicy(userPolicy); err == nil { 287 fmt.Printf("Policy is valid.\n") 288 os.Exit(0) 289 } 290 fmt.Printf("Policy is invalid: %v\n", err) 291 os.Exit(1) 292 } 293 log.Fatalf("Policy is empty or could not be loaded.") 294 }, 295 } 296 clientConfig = cli.AddKubectlFlagsToCmd(command) 297 command.Flags().StringVar(&policyFile, "policy-file", "", "path to the policy file to use") 298 command.Flags().StringVar(&namespace, "namespace", "", "namespace to get argo rbac configmap from") 299 300 return command 301 } 302 303 // Load user policy file if requested or use Kubernetes client to get the 304 // appropriate ConfigMap from the current context 305 func getPolicy(ctx context.Context, policyFile string, kubeClient kubernetes.Interface, namespace string) (userPolicy string, defaultRole string, matchMode string) { 306 var err error 307 if policyFile != "" { 308 // load from file 309 userPolicy, defaultRole, matchMode, err = getPolicyFromFile(policyFile) 310 if err != nil { 311 log.Fatalf("could not read policy file: %v", err) 312 } 313 } else { 314 cm, err := getPolicyConfigMap(ctx, kubeClient, namespace) 315 if err != nil { 316 log.Fatalf("could not get configmap: %v", err) 317 } 318 userPolicy, defaultRole, matchMode = getPolicyFromConfigMap(cm) 319 } 320 321 return userPolicy, defaultRole, matchMode 322 } 323 324 // getPolicyFromFile loads a RBAC policy from given path 325 func getPolicyFromFile(policyFile string) (string, string, string, error) { 326 var ( 327 userPolicy string 328 defaultRole string 329 matchMode string 330 ) 331 332 upol, err := os.ReadFile(policyFile) 333 if err != nil { 334 log.Fatalf("error opening policy file: %v", err) 335 return "", "", "", err 336 } 337 338 // Try to unmarshal the input file as ConfigMap first. If it succeeds, we 339 // assume config map input. Otherwise, we treat it as 340 var upolCM *corev1.ConfigMap 341 err = yaml.Unmarshal(upol, &upolCM) 342 if err != nil { 343 userPolicy = string(upol) 344 } else { 345 userPolicy, defaultRole, matchMode = getPolicyFromConfigMap(upolCM) 346 } 347 348 return userPolicy, defaultRole, matchMode, nil 349 } 350 351 // Retrieve policy information from a ConfigMap 352 func getPolicyFromConfigMap(cm *corev1.ConfigMap) (string, string, string) { 353 var ( 354 defaultRole string 355 ok bool 356 ) 357 358 defaultRole, ok = cm.Data[rbac.ConfigMapPolicyDefaultKey] 359 if !ok { 360 defaultRole = "" 361 } 362 363 return rbac.PolicyCSV(cm.Data), defaultRole, cm.Data[rbac.ConfigMapMatchModeKey] 364 } 365 366 // getPolicyConfigMap fetches the RBAC config map from K8s cluster 367 func getPolicyConfigMap(ctx context.Context, client kubernetes.Interface, namespace string) (*corev1.ConfigMap, error) { 368 cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, common.ArgoCDRBACConfigMapName, metav1.GetOptions{}) 369 if err != nil { 370 return nil, err 371 } 372 return cm, nil 373 } 374 375 // checkPolicy checks whether given subject is allowed to execute specified 376 // action against specified resource 377 func checkPolicy(subject, action, resource, subResource, builtinPolicy, userPolicy, defaultRole, matchMode string, strict bool) bool { 378 enf := rbac.NewEnforcer(nil, "argocd", "argocd-rbac-cm", nil) 379 enf.SetDefaultRole(defaultRole) 380 enf.SetMatchMode(matchMode) 381 if builtinPolicy != "" { 382 if err := enf.SetBuiltinPolicy(builtinPolicy); err != nil { 383 log.Fatalf("could not set built-in policy: %v", err) 384 return false 385 } 386 } 387 if userPolicy != "" { 388 if err := rbac.ValidatePolicy(userPolicy); err != nil { 389 log.Fatalf("invalid user policy: %v", err) 390 return false 391 } 392 if err := enf.SetUserPolicy(userPolicy); err != nil { 393 log.Fatalf("could not set user policy: %v", err) 394 return false 395 } 396 } 397 398 // User could have used a mutation of the resource name (i.e. 'cert' for 399 // 'certificate') - let's resolve it to the valid resource. 400 realResource := resolveRBACResourceName(resource) 401 402 // If in strict mode, validate that given RBAC resource and action are 403 // actually valid tokens. 404 if strict { 405 if err := validateRBACResourceAction(realResource, action); err != nil { 406 log.Fatalf("error in RBAC request: %v", err) 407 return false 408 } 409 } 410 411 // Some project scoped resources have a special notation - for simplicity's sake, 412 // if user gives no sub-resource (or specifies simple '*'), we construct 413 // the required notation by setting subresource to '*/*'. 414 if rbac.ProjectScoped[realResource] { 415 if subResource == "*" || subResource == "" { 416 subResource = "*/*" 417 } 418 } 419 return enf.Enforce(subject, realResource, action, subResource) 420 } 421 422 // resolveRBACResourceName resolves a user supplied value to a valid RBAC 423 // resource name. If no mapping is found, returns the value verbatim. 424 func resolveRBACResourceName(name string) string { 425 if res, ok := resourceMap[name]; ok { 426 return res 427 } 428 return name 429 } 430 431 // validateRBACResourceAction checks whether a given resource is a valid RBAC resource. 432 // If it is, it validates that the action is a valid RBAC action for this resource. 433 func validateRBACResourceAction(resource, action string) error { 434 validActions, ok := validRBACResourcesActions[resource] 435 if !ok { 436 return fmt.Errorf("'%s' is not a valid resource name", resource) 437 } 438 439 realAction, _, hasPath := strings.Cut(action, "/") 440 actionTrait, ok := validActions[realAction] 441 if !ok || hasPath && !actionTrait.allowPath { 442 return fmt.Errorf("'%s' is not a valid action for %s", action, resource) 443 } 444 return nil 445 }