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  }