diff --git a/assets/model.conf b/assets/model.conf index 087406b202..e53d9fe89d 100644 --- a/assets/model.conf +++ b/assets/model.conf @@ -11,4 +11,4 @@ g = _, _ e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) [matchers] -m = g(r.sub, p.sub) && globMatch(r.res, p.res) && globMatch(r.act, p.act) && globMatch(r.obj, p.obj) +m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) diff --git a/docs/operator-manual/argocd-rbac-cm.yaml b/docs/operator-manual/argocd-rbac-cm.yaml index 75ceb09377..12ec17f8e9 100644 --- a/docs/operator-manual/argocd-rbac-cm.yaml +++ b/docs/operator-manual/argocd-rbac-cm.yaml @@ -28,3 +28,8 @@ data: # If omitted, defaults to: '[groups]'. The scope value can be a string, or a list of strings. scopes: '[cognito:groups, email]' + # matchMode configures the matchers function for casbin. + # There are two options for this, 'glob' for glob matcher or 'regex' for regex matcher. If omitted or mis-configured, + # will be set to 'glob' as default. + policy.matchMode: 'glob' + diff --git a/util/rbac/rbac.go b/util/rbac/rbac.go index 0828543c8c..f9e0959629 100644 --- a/util/rbac/rbac.go +++ b/util/rbac/rbac.go @@ -14,6 +14,7 @@ import ( "github.com/casbin/casbin" "github.com/casbin/casbin/model" + "github.com/casbin/casbin/util" jwt "github.com/dgrijalva/jwt-go/v4" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -31,8 +32,10 @@ const ( ConfigMapPolicyCSVKey = "policy.csv" ConfigMapPolicyDefaultKey = "policy.default" ConfigMapScopesKey = "scopes" - - defaultRBACSyncPeriod = 10 * time.Minute + ConfigMapMatchModeKey = "policy.matchMode" + GlobMatchMode = "glob" + RegexMatchMode = "regex" + defaultRBACSyncPeriod = 10 * time.Minute ) // Enforcer is a wrapper around an Casbin enforcer that: @@ -50,6 +53,7 @@ type Enforcer struct { claimsEnforcerFunc ClaimsEnforcerFunc model model.Model defaultRole string + matchMode string } // ClaimsEnforcerFunc is func template to enforce a JWT claims. The subject is replaced @@ -60,22 +64,8 @@ func newEnforcerSafe(params ...interface{}) (e *casbin.Enforcer, err error) { if err != nil { return nil, err } - enfs.AddFunction("globMatch", func(args ...interface{}) (interface{}, error) { - if len(args) < 2 { - return false, nil - } - val, ok := args[0].(string) - if !ok { - return false, nil - } - - pattern, ok := args[1].(string) - if !ok { - return false, nil - } - - return glob.Match(pattern, val), nil - }) + // Default glob match mode + enfs.AddFunction("globOrRegexMatch", globMatchFunc) return enfs, nil } @@ -98,6 +88,40 @@ func NewEnforcer(clientset kubernetes.Interface, namespace, configmap string, cl } } +// Glob match func +func globMatchFunc(args ...interface{}) (interface{}, error) { + if len(args) < 2 { + return false, nil + } + val, ok := args[0].(string) + if !ok { + return false, nil + } + + pattern, ok := args[1].(string) + if !ok { + return false, nil + } + + return glob.Match(pattern, val), nil +} + +// SetMatchMode set match mode on runtime, glob match or regex match +func (e *Enforcer) SetMatchMode(mode string) { + if mode == RegexMatchMode { + e.matchMode = RegexMatchMode + } else { + e.matchMode = GlobMatchMode + } + e.Enforcer.AddFunction("globOrRegexMatch", func(args ...interface{}) (interface{}, error) { + if mode == RegexMatchMode { + return util.RegexMatchFunc(args...) + } else { + return globMatchFunc(args...) + } + }) +} + // SetDefaultRole sets a default role to use during enforcement. Will fall back to this role if // normal enforcement fails func (e *Enforcer) SetDefaultRole(roleName string) { @@ -281,6 +305,7 @@ func (e *Enforcer) runInformer(ctx context.Context, onUpdated func(cm *apiv1.Con // syncUpdate updates the enforcer func (e *Enforcer) syncUpdate(cm *apiv1.ConfigMap, onUpdated func(cm *apiv1.ConfigMap) error) error { e.SetDefaultRole(cm.Data[ConfigMapPolicyDefaultKey]) + e.SetMatchMode(cm.Data[ConfigMapMatchModeKey]) policyCSV, ok := cm.Data[ConfigMapPolicyCSVKey] if !ok { policyCSV = "" diff --git a/util/rbac/rbac_test.go b/util/rbac/rbac_test.go index 43f03aec0f..68844ee007 100644 --- a/util/rbac/rbac_test.go +++ b/util/rbac/rbac_test.go @@ -336,3 +336,66 @@ func TestEnforceErrorMessage(t *testing.T) { assert.Equal(t, "rpc error: code = PermissionDenied desc = permission denied: project, sub: proj:default:admin", err.Error()) } + +func TestDefaultGlobMatchMode(t *testing.T) { + kubeclientset := fake.NewSimpleClientset() + enf := NewEnforcer(kubeclientset, fakeNamespace, fakeConfigMapName, nil) + err := enf.syncUpdate(fakeConfigMap(), noOpUpdate) + assert.Nil(t, err) + policy := ` +p, alice, clusters, get, "https://github.com/*/*.git", allow +` + _ = enf.SetUserPolicy(policy) + + assert.True(t, enf.Enforce("alice", "clusters", "get", "https://github.com/argoproj/argo-cd.git")) + assert.False(t, enf.Enforce("alice", "repositories", "get", "https://github.com/argoproj/argo-cd.git")) + +} + +func TestGlobMatchMode(t *testing.T) { + cm := fakeConfigMap() + cm.Data[ConfigMapMatchModeKey] = GlobMatchMode + kubeclientset := fake.NewSimpleClientset() + enf := NewEnforcer(kubeclientset, fakeNamespace, fakeConfigMapName, nil) + err := enf.syncUpdate(cm, noOpUpdate) + assert.Nil(t, err) + policy := ` +p, alice, clusters, get, "https://github.com/*/*.git", allow +` + _ = enf.SetUserPolicy(policy) + + assert.True(t, enf.Enforce("alice", "clusters", "get", "https://github.com/argoproj/argo-cd.git")) + assert.False(t, enf.Enforce("alice", "clusters", "get", "https://github.com/argo-cd.git")) + +} + +func TestRegexMatchMode(t *testing.T) { + cm := fakeConfigMap() + cm.Data[ConfigMapMatchModeKey] = RegexMatchMode + kubeclientset := fake.NewSimpleClientset() + enf := NewEnforcer(kubeclientset, fakeNamespace, fakeConfigMapName, nil) + err := enf.syncUpdate(cm, noOpUpdate) + assert.Nil(t, err) + policy := ` +p, alice, clusters, get, "https://github.com/argo[a-z]{4}/argo-[a-z]+.git", allow +` + _ = enf.SetUserPolicy(policy) + + assert.True(t, enf.Enforce("alice", "clusters", "get", "https://github.com/argoproj/argo-cd.git")) + assert.False(t, enf.Enforce("alice", "clusters", "get", "https://github.com/argoproj/1argo-cd.git")) + +} + +func TestGlobMatchFunc(t *testing.T) { + ok, _ := globMatchFunc("arg1") + assert.False(t, ok.(bool)) + + ok, _ = globMatchFunc(time.Now(), "arg2") + assert.False(t, ok.(bool)) + + ok, _ = globMatchFunc("arg1", time.Now()) + assert.False(t, ok.(bool)) + + ok, _ = globMatchFunc("arg/123", "arg/*") + assert.True(t, ok.(bool)) +}