github.com/argoproj/argo-cd@v1.8.7/util/rbac/rbac.go (about)

     1  package rbac
     2  
     3  import (
     4  	"context"
     5  	"encoding/csv"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/argoproj/argo-cd/util/assets"
    12  	"github.com/argoproj/argo-cd/util/glob"
    13  	jwtutil "github.com/argoproj/argo-cd/util/jwt"
    14  
    15  	"github.com/casbin/casbin"
    16  	"github.com/casbin/casbin/model"
    17  	jwt "github.com/dgrijalva/jwt-go/v4"
    18  	log "github.com/sirupsen/logrus"
    19  	"google.golang.org/grpc/codes"
    20  	"google.golang.org/grpc/status"
    21  	apiv1 "k8s.io/api/core/v1"
    22  	apierr "k8s.io/apimachinery/pkg/api/errors"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/fields"
    25  	v1 "k8s.io/client-go/informers/core/v1"
    26  	"k8s.io/client-go/kubernetes"
    27  	"k8s.io/client-go/tools/cache"
    28  )
    29  
    30  const (
    31  	ConfigMapPolicyCSVKey     = "policy.csv"
    32  	ConfigMapPolicyDefaultKey = "policy.default"
    33  	ConfigMapScopesKey        = "scopes"
    34  
    35  	defaultRBACSyncPeriod = 10 * time.Minute
    36  )
    37  
    38  // Enforcer is a wrapper around an Casbin enforcer that:
    39  // * is backed by a kubernetes config map
    40  // * has a predefined RBAC model
    41  // * supports a built-in policy
    42  // * supports a user-defined policy
    43  // * supports a custom JWT claims enforce function
    44  type Enforcer struct {
    45  	*casbin.Enforcer
    46  	adapter            *argocdAdapter
    47  	clientset          kubernetes.Interface
    48  	namespace          string
    49  	configmap          string
    50  	claimsEnforcerFunc ClaimsEnforcerFunc
    51  	model              model.Model
    52  	defaultRole        string
    53  }
    54  
    55  // ClaimsEnforcerFunc is func template to enforce a JWT claims. The subject is replaced
    56  type ClaimsEnforcerFunc func(claims jwt.Claims, rvals ...interface{}) bool
    57  
    58  func newEnforcerSafe(params ...interface{}) (e *casbin.Enforcer, err error) {
    59  	enfs, err := casbin.NewEnforcerSafe(params...)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	enfs.AddFunction("globMatch", func(args ...interface{}) (interface{}, error) {
    64  		if len(args) < 2 {
    65  			return false, nil
    66  		}
    67  		val, ok := args[0].(string)
    68  		if !ok {
    69  			return false, nil
    70  		}
    71  
    72  		pattern, ok := args[1].(string)
    73  		if !ok {
    74  			return false, nil
    75  		}
    76  
    77  		return glob.Match(pattern, val), nil
    78  	})
    79  	return enfs, nil
    80  }
    81  
    82  func NewEnforcer(clientset kubernetes.Interface, namespace, configmap string, claimsEnforcer ClaimsEnforcerFunc) *Enforcer {
    83  	adapter := newAdapter("", "", "")
    84  	builtInModel := newBuiltInModel()
    85  	enf, err := newEnforcerSafe(builtInModel, adapter)
    86  	if err != nil {
    87  		panic(err)
    88  	}
    89  	enf.EnableLog(false)
    90  	return &Enforcer{
    91  		Enforcer:           enf,
    92  		adapter:            adapter,
    93  		clientset:          clientset,
    94  		namespace:          namespace,
    95  		configmap:          configmap,
    96  		model:              builtInModel,
    97  		claimsEnforcerFunc: claimsEnforcer,
    98  	}
    99  }
   100  
   101  // SetDefaultRole sets a default role to use during enforcement. Will fall back to this role if
   102  // normal enforcement fails
   103  func (e *Enforcer) SetDefaultRole(roleName string) {
   104  	e.defaultRole = roleName
   105  }
   106  
   107  // SetClaimsEnforcerFunc sets a claims enforce function during enforcement. The claims enforce function
   108  // can extract claims from JWT token and do the proper enforcement based on user, group or any information
   109  // available in the input parameter list
   110  func (e *Enforcer) SetClaimsEnforcerFunc(claimsEnforcer ClaimsEnforcerFunc) {
   111  	e.claimsEnforcerFunc = claimsEnforcer
   112  }
   113  
   114  // Enforce is a wrapper around casbin.Enforce to additionally enforce a default role and a custom
   115  // claims function
   116  func (e *Enforcer) Enforce(rvals ...interface{}) bool {
   117  	return enforce(e.Enforcer, e.defaultRole, e.claimsEnforcerFunc, rvals...)
   118  }
   119  
   120  // EnforceErr is a convenience helper to wrap a failed enforcement with a detailed error about the request
   121  func (e *Enforcer) EnforceErr(rvals ...interface{}) error {
   122  	if !e.Enforce(rvals...) {
   123  		errMsg := "permission denied"
   124  		if len(rvals) > 0 {
   125  			rvalsStrs := make([]string, len(rvals)-1)
   126  			for i, rval := range rvals[1:] {
   127  				rvalsStrs[i] = fmt.Sprintf("%s", rval)
   128  			}
   129  			switch s := rvals[0].(type) {
   130  			case jwt.Claims:
   131  				claims, err := jwtutil.MapClaims(s)
   132  				if err != nil {
   133  					break
   134  				}
   135  				if sub := jwtutil.StringField(claims, "sub"); sub != "" {
   136  					rvalsStrs = append(rvalsStrs, fmt.Sprintf("sub: %s", sub))
   137  				}
   138  				if issuedAtTime, err := jwtutil.IssuedAtTime(claims); err == nil {
   139  					rvalsStrs = append(rvalsStrs, fmt.Sprintf("iat: %s", issuedAtTime.Format(time.RFC3339)))
   140  				}
   141  			}
   142  			errMsg = fmt.Sprintf("%s: %s", errMsg, strings.Join(rvalsStrs, ", "))
   143  		}
   144  		return status.Error(codes.PermissionDenied, errMsg)
   145  	}
   146  	return nil
   147  }
   148  
   149  // EnforceRuntimePolicy enforces a policy defined at run-time which augments the built-in and
   150  // user-defined policy. This allows any explicit denies of the built-in, and user-defined policies
   151  // to override the run-time policy. Runs normal enforcement if run-time policy is empty.
   152  func (e *Enforcer) EnforceRuntimePolicy(policy string, rvals ...interface{}) bool {
   153  	var enf *casbin.Enforcer
   154  	var err error
   155  	if policy == "" {
   156  		enf = e.Enforcer
   157  	} else {
   158  		enf, err = newEnforcerSafe(newBuiltInModel(), newAdapter(e.adapter.builtinPolicy, e.adapter.userDefinedPolicy, policy))
   159  		if err != nil {
   160  			log.Warnf("invalid runtime policy: %s", policy)
   161  			enf = e.Enforcer
   162  		}
   163  	}
   164  	return enforce(enf, e.defaultRole, e.claimsEnforcerFunc, rvals...)
   165  }
   166  
   167  // enforce is a helper to additionally check a default role and invoke a custom claims enforcement function
   168  func enforce(enf *casbin.Enforcer, defaultRole string, claimsEnforcerFunc ClaimsEnforcerFunc, rvals ...interface{}) bool {
   169  	// check the default role
   170  	if defaultRole != "" && len(rvals) >= 2 {
   171  		if enf.Enforce(append([]interface{}{defaultRole}, rvals[1:]...)...) {
   172  			return true
   173  		}
   174  	}
   175  	if len(rvals) == 0 {
   176  		return false
   177  	}
   178  	// check if subject is jwt.Claims vs. a normal subject string and run custom claims
   179  	// enforcement func (if set)
   180  	sub := rvals[0]
   181  	switch s := sub.(type) {
   182  	case string:
   183  		// noop
   184  	case jwt.Claims:
   185  		if claimsEnforcerFunc != nil && claimsEnforcerFunc(s, rvals...) {
   186  			return true
   187  		}
   188  		rvals = append([]interface{}{""}, rvals[1:]...)
   189  	default:
   190  		rvals = append([]interface{}{""}, rvals[1:]...)
   191  	}
   192  	return enf.Enforce(rvals...)
   193  }
   194  
   195  // SetBuiltinPolicy sets a built-in policy, which augments any user defined policies
   196  func (e *Enforcer) SetBuiltinPolicy(policy string) error {
   197  	e.adapter.builtinPolicy = policy
   198  	return e.LoadPolicy()
   199  }
   200  
   201  // SetUserPolicy sets a user policy, augmenting the built-in policy
   202  func (e *Enforcer) SetUserPolicy(policy string) error {
   203  	e.adapter.userDefinedPolicy = policy
   204  	return e.LoadPolicy()
   205  }
   206  
   207  // newInformers returns an informer which watches updates on the rbac configmap
   208  func (e *Enforcer) newInformer() cache.SharedIndexInformer {
   209  	tweakConfigMap := func(options *metav1.ListOptions) {
   210  		cmFieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", e.configmap))
   211  		options.FieldSelector = cmFieldSelector.String()
   212  	}
   213  	indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
   214  	return v1.NewFilteredConfigMapInformer(e.clientset, e.namespace, defaultRBACSyncPeriod, indexers, tweakConfigMap)
   215  }
   216  
   217  // RunPolicyLoader runs the policy loader which watches policy updates from the configmap and reloads them
   218  func (e *Enforcer) RunPolicyLoader(ctx context.Context, onUpdated func(cm *apiv1.ConfigMap) error) error {
   219  	cm, err := e.clientset.CoreV1().ConfigMaps(e.namespace).Get(ctx, e.configmap, metav1.GetOptions{})
   220  	if err != nil {
   221  		if !apierr.IsNotFound(err) {
   222  			return err
   223  		}
   224  	} else {
   225  		err = e.syncUpdate(cm, onUpdated)
   226  		if err != nil {
   227  			return err
   228  		}
   229  	}
   230  	e.runInformer(ctx, onUpdated)
   231  	return nil
   232  }
   233  
   234  func (e *Enforcer) runInformer(ctx context.Context, onUpdated func(cm *apiv1.ConfigMap) error) {
   235  	cmInformer := e.newInformer()
   236  	cmInformer.AddEventHandler(
   237  		cache.ResourceEventHandlerFuncs{
   238  			AddFunc: func(obj interface{}) {
   239  				if cm, ok := obj.(*apiv1.ConfigMap); ok {
   240  					err := e.syncUpdate(cm, onUpdated)
   241  					if err != nil {
   242  						log.Error(err)
   243  					} else {
   244  						log.Infof("RBAC ConfigMap '%s' added", e.configmap)
   245  					}
   246  				}
   247  			},
   248  			UpdateFunc: func(old, new interface{}) {
   249  				oldCM := old.(*apiv1.ConfigMap)
   250  				newCM := new.(*apiv1.ConfigMap)
   251  				if oldCM.ResourceVersion == newCM.ResourceVersion {
   252  					return
   253  				}
   254  				err := e.syncUpdate(newCM, onUpdated)
   255  				if err != nil {
   256  					log.Error(err)
   257  				} else {
   258  					log.Infof("RBAC ConfigMap '%s' updated", e.configmap)
   259  				}
   260  			},
   261  		},
   262  	)
   263  	log.Info("Starting rbac config informer")
   264  	cmInformer.Run(ctx.Done())
   265  	log.Info("rbac configmap informer cancelled")
   266  }
   267  
   268  // syncUpdate updates the enforcer
   269  func (e *Enforcer) syncUpdate(cm *apiv1.ConfigMap, onUpdated func(cm *apiv1.ConfigMap) error) error {
   270  	e.SetDefaultRole(cm.Data[ConfigMapPolicyDefaultKey])
   271  	policyCSV, ok := cm.Data[ConfigMapPolicyCSVKey]
   272  	if !ok {
   273  		policyCSV = ""
   274  	}
   275  	if err := onUpdated(cm); err != nil {
   276  		return err
   277  	}
   278  	return e.SetUserPolicy(policyCSV)
   279  }
   280  
   281  // ValidatePolicy verifies a policy string is acceptable to casbin
   282  func ValidatePolicy(policy string) error {
   283  	_, err := newEnforcerSafe(newBuiltInModel(), newAdapter("", "", policy))
   284  	if err != nil {
   285  		return fmt.Errorf("policy syntax error: %s", policy)
   286  	}
   287  	return nil
   288  }
   289  
   290  // newBuiltInModel is a helper to return a brand new casbin model from the built-in model string.
   291  // This is needed because it is not safe to re-use the same casbin Model when instantiating new
   292  // casbin enforcers.
   293  func newBuiltInModel() model.Model {
   294  	return casbin.NewModel(assets.ModelConf)
   295  }
   296  
   297  // Casbin adapter which satisfies persist.Adapter interface
   298  type argocdAdapter struct {
   299  	builtinPolicy     string
   300  	userDefinedPolicy string
   301  	runtimePolicy     string
   302  }
   303  
   304  func newAdapter(builtinPolicy, userDefinedPolicy, runtimePolicy string) *argocdAdapter {
   305  	return &argocdAdapter{
   306  		builtinPolicy:     builtinPolicy,
   307  		userDefinedPolicy: userDefinedPolicy,
   308  		runtimePolicy:     runtimePolicy,
   309  	}
   310  }
   311  
   312  func (a *argocdAdapter) LoadPolicy(model model.Model) error {
   313  	for _, policyStr := range []string{a.builtinPolicy, a.userDefinedPolicy, a.runtimePolicy} {
   314  		for _, line := range strings.Split(policyStr, "\n") {
   315  			if err := loadPolicyLine(strings.TrimSpace(line), model); err != nil {
   316  				return err
   317  			}
   318  		}
   319  	}
   320  	return nil
   321  }
   322  
   323  // The modified version of LoadPolicyLine function defined in "persist" package of github.com/casbin/casbin.
   324  // Uses CVS parser to correctly handle quotes in policy line.
   325  func loadPolicyLine(line string, model model.Model) error {
   326  	if line == "" || strings.HasPrefix(line, "#") {
   327  		return nil
   328  	}
   329  
   330  	reader := csv.NewReader(strings.NewReader(line))
   331  	reader.TrimLeadingSpace = true
   332  	tokens, err := reader.Read()
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	key := tokens[0]
   338  	sec := key[:1]
   339  	model[sec][key].Policy = append(model[sec][key].Policy, tokens[1:])
   340  	return nil
   341  }
   342  
   343  func (a *argocdAdapter) SavePolicy(model model.Model) error {
   344  	return errors.New("not implemented")
   345  }
   346  
   347  func (a *argocdAdapter) AddPolicy(sec string, ptype string, rule []string) error {
   348  	return errors.New("not implemented")
   349  }
   350  
   351  func (a *argocdAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
   352  	return errors.New("not implemented")
   353  }
   354  
   355  func (a *argocdAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
   356  	return errors.New("not implemented")
   357  }