diff --git a/assets/model.conf b/assets/model.conf index 92664af6ff693..087406b2023b5 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) && keyMatch(r.res, p.res) && keyMatch(r.act, p.act) && keyMatch(r.obj, p.obj) +m = g(r.sub, p.sub) && globMatch(r.res, p.res) && globMatch(r.act, p.act) && globMatch(r.obj, p.obj) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 52ddb62181d69..8a8dff264eb62 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -47,9 +47,8 @@ import ( "github.com/argoproj/argo-cd/util/argo" appstatecache "github.com/argoproj/argo-cd/util/cache/appstate" "github.com/argoproj/argo-cd/util/db" + "github.com/argoproj/argo-cd/util/glob" settings_util "github.com/argoproj/argo-cd/util/settings" - - "github.com/gobwas/glob" ) const ( @@ -265,9 +264,9 @@ func isKnownOrphanedResourceExclusion(key kube.ResourceKey, proj *appv1.AppProje } list := proj.Spec.OrphanedResources.Ignore for _, item := range list { - if item.Kind == "" || match(item.Kind, key.Kind) { - if match(item.Group, key.Group) { - if item.Name == "" || match(item.Name, key.Name) { + if item.Kind == "" || glob.Match(item.Kind, key.Kind) { + if glob.Match(item.Group, key.Group) { + if item.Name == "" || glob.Match(item.Name, key.Name) { return true } } @@ -276,15 +275,6 @@ func isKnownOrphanedResourceExclusion(key kube.ResourceKey, proj *appv1.AppProje return false } -func match(pattern, text string) bool { - compiledGlob, err := glob.Compile(pattern) - if err != nil { - log.Warnf("failed to compile pattern %s due to error %v", pattern, err) - return false - } - return compiledGlob.Match(text) -} - func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managedResources []*appv1.ResourceDiff) (*appv1.ApplicationTree, error) { nodes := make([]appv1.ResourceNode, 0) diff --git a/util/argo/normalizers/diff_normalizer.go b/util/argo/normalizers/diff_normalizer.go index 637006dec9649..5082a6f1ed2f4 100644 --- a/util/argo/normalizers/diff_normalizer.go +++ b/util/argo/normalizers/diff_normalizer.go @@ -5,12 +5,12 @@ import ( "github.com/argoproj/gitops-engine/pkg/diff" jsonpatch "github.com/evanphx/json-patch" - "github.com/gobwas/glob" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/util/glob" ) type normalizerPatch struct { @@ -62,23 +62,14 @@ func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides return &ignoreNormalizer{patches: patches}, nil } -func match(pattern, text string) bool { - compiledGlob, err := glob.Compile(pattern) - if err != nil { - log.Warnf("failed to compile pattern %s due to error %v", pattern, err) - return false - } - return compiledGlob.Match(text) -} - // Normalize removes fields from supplied resource using json paths from matching items of specified resources ignored differences list func (n *ignoreNormalizer) Normalize(un *unstructured.Unstructured) error { matched := make([]normalizerPatch, 0) for _, patch := range n.patches { groupKind := un.GroupVersionKind().GroupKind() - if match(patch.groupKind.Group, groupKind.Group) && - match(patch.groupKind.Kind, groupKind.Kind) && + if glob.Match(patch.groupKind.Group, groupKind.Group) && + glob.Match(patch.groupKind.Kind, groupKind.Kind) && (patch.name == "" || patch.name == un.GetName()) && (patch.namespace == "" || patch.namespace == un.GetNamespace()) { diff --git a/util/glob/glob.go b/util/glob/glob.go new file mode 100644 index 0000000000000..41bb2b93648d1 --- /dev/null +++ b/util/glob/glob.go @@ -0,0 +1,15 @@ +package glob + +import ( + "github.com/gobwas/glob" + log "github.com/sirupsen/logrus" +) + +func Match(pattern, text string) bool { + compiledGlob, err := glob.Compile(pattern) + if err != nil { + log.Warnf("failed to compile pattern %s due to error %v", pattern, err) + return false + } + return compiledGlob.Match(text) +} diff --git a/util/rbac/rbac.go b/util/rbac/rbac.go index e26ab147f7387..dd8c45095adaf 100644 --- a/util/rbac/rbac.go +++ b/util/rbac/rbac.go @@ -9,6 +9,7 @@ import ( "time" "github.com/argoproj/argo-cd/util/assets" + "github.com/argoproj/argo-cd/util/glob" jwtutil "github.com/argoproj/argo-cd/util/jwt" "github.com/casbin/casbin" @@ -54,10 +55,37 @@ type Enforcer struct { // ClaimsEnforcerFunc is func template to enforce a JWT claims. The subject is replaced type ClaimsEnforcerFunc func(claims jwt.Claims, rvals ...interface{}) bool +func newEnforcerSafe(params ...interface{}) (e *casbin.Enforcer, err error) { + enfs, err := casbin.NewEnforcerSafe(params...) + 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 + }) + return enfs, nil +} + func NewEnforcer(clientset kubernetes.Interface, namespace, configmap string, claimsEnforcer ClaimsEnforcerFunc) *Enforcer { adapter := newAdapter("", "", "") builtInModel := newBuiltInModel() - enf := casbin.NewEnforcer(builtInModel, adapter) + enf, err := newEnforcerSafe(builtInModel, adapter) + if err != nil { + panic(err) + } enf.EnableLog(false) return &Enforcer{ Enforcer: enf, @@ -134,7 +162,7 @@ func (e *Enforcer) EnforceRuntimePolicy(policy string, rvals ...interface{}) boo if policy == "" { enf = e.Enforcer } else { - enf, err = casbin.NewEnforcerSafe(newBuiltInModel(), newAdapter(e.adapter.builtinPolicy, e.adapter.userDefinedPolicy, policy)) + enf, err = newEnforcerSafe(newBuiltInModel(), newAdapter(e.adapter.builtinPolicy, e.adapter.userDefinedPolicy, policy)) if err != nil { log.Warnf("invalid runtime policy: %s", policy) enf = e.Enforcer @@ -259,7 +287,7 @@ func (e *Enforcer) syncUpdate(cm *apiv1.ConfigMap, onUpdated func(cm *apiv1.Conf // ValidatePolicy verifies a policy string is acceptable to casbin func ValidatePolicy(policy string) error { - _, err := casbin.NewEnforcerSafe(newBuiltInModel(), newAdapter("", "", policy)) + _, err := newEnforcerSafe(newBuiltInModel(), newAdapter("", "", policy)) if err != nil { return fmt.Errorf("policy syntax error: %s", policy) } diff --git a/util/rbac/rbac_test.go b/util/rbac/rbac_test.go index 575c7ade73401..2331293248c2e 100644 --- a/util/rbac/rbac_test.go +++ b/util/rbac/rbac_test.go @@ -128,6 +128,8 @@ p, mike, *, *, foo/obj, deny p, trudy, applications, get, foo/obj, allow p, trudy, applications/*, get, foo/obj, allow p, trudy, applications/secrets, get, foo/obj, deny +p, danny, applications, get, */obj, allow +p, danny, applications, get, proj1/a*p1, allow ` _ = enf.SetUserPolicy(policy) @@ -171,6 +173,13 @@ p, trudy, applications/secrets, get, foo/obj, deny assert.True(t, enf.Enforce("trudy", "applications", "get", "foo/obj")) assert.True(t, enf.Enforce("trudy", "applications/logs", "get", "foo/obj")) assert.False(t, enf.Enforce("trudy", "applications/secrets", "get", "foo/obj")) + + // Verify trailing wildcards don't grant full access + assert.True(t, enf.Enforce("danny", "applications", "get", "foo/obj")) + assert.True(t, enf.Enforce("danny", "applications", "get", "bar/obj")) + assert.False(t, enf.Enforce("danny", "applications", "get", "foo/bar")) + assert.True(t, enf.Enforce("danny", "applications", "get", "proj1/app1")) + assert.False(t, enf.Enforce("danny", "applications", "get", "proj1/app2")) } // TestProjectIsolationEnforcement verifies the ability to create Project specific policies diff --git a/util/settings/filtered_resource.go b/util/settings/filtered_resource.go index 8ecdce5b9b348..f3a2d61ed560f 100644 --- a/util/settings/filtered_resource.go +++ b/util/settings/filtered_resource.go @@ -1,9 +1,6 @@ package settings -import ( - "github.com/gobwas/glob" - log "github.com/sirupsen/logrus" -) +import "github.com/argoproj/argo-cd/util/glob" type FilteredResource struct { APIGroups []string `json:"apiGroups,omitempty"` @@ -13,22 +10,13 @@ type FilteredResource struct { func (r FilteredResource) matchGroup(apiGroup string) bool { for _, excludedApiGroup := range r.APIGroups { - if match(excludedApiGroup, apiGroup) { + if glob.Match(excludedApiGroup, apiGroup) { return true } } return len(r.APIGroups) == 0 } -func match(pattern, text string) bool { - compiledGlob, err := glob.Compile(pattern) - if err != nil { - log.Warnf("failed to compile pattern %s due to error %v", pattern, err) - return false - } - return compiledGlob.Match(text) -} - func (r FilteredResource) matchKind(kind string) bool { for _, excludedKind := range r.Kinds { if excludedKind == "*" || excludedKind == kind { @@ -40,7 +28,7 @@ func (r FilteredResource) matchKind(kind string) bool { func (r FilteredResource) matchCluster(cluster string) bool { for _, excludedCluster := range r.Clusters { - if match(excludedCluster, cluster) { + if glob.Match(excludedCluster, cluster) { return true } }