VYPR
Critical severityNVD Advisory· Published Mar 23, 2022· Updated Apr 23, 2025

Improper access control allows admin privilege escalation in Argo CD

CVE-2022-24768

Description

Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. All unpatched versions of Argo CD starting with 1.0.0 are vulnerable to an improper access control bug, allowing a malicious user to potentially escalate their privileges to admin-level. Versions starting with 0.8.0 and 0.5.0 contain limited versions of this issue. To perform exploits, an authorized Argo CD user must have push access to an Application's source git or Helm repository or sync and override access to an Application. Once a user has that access, different exploitation levels are possible depending on their other RBAC privileges. A patch for this vulnerability has been released in Argo CD versions 2.3.2, 2.2.8, and 2.1.14. Some mitigation measures are available but do not serve as a substitute for upgrading. To avoid privilege escalation, limit who has push access to Application source repositories or sync + override access to Applications; and limit which repositories are available in projects where users have update access to Applications. To avoid unauthorized resource inspection/tampering, limit who has delete, get, or action access to Applications.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/argoproj/argo-cdGo
>= 0.5.0, < 2.1.142.1.14
github.com/argoproj/argo-cdGo
>= 2.2.0, < 2.2.82.2.8
github.com/argoproj/argo-cdGo
>= 2.3.0, < 2.3.22.3.2

Affected products

1

Patches

1
af03b291d4b7

Merge pull request from GHSA-2f5v-8r3f-8pww

https://github.com/argoproj/argo-cdAlexander MatyushentsevMar 22, 2022via ghsa
10 files changed · +197 76
  • controller/appcontroller.go+18 4 modified
    @@ -28,6 +28,7 @@ import (
     	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
     	"k8s.io/apimachinery/pkg/labels"
     	apiruntime "k8s.io/apimachinery/pkg/runtime"
    +	"k8s.io/apimachinery/pkg/runtime/schema"
     	"k8s.io/apimachinery/pkg/types"
     	"k8s.io/apimachinery/pkg/util/runtime"
     	"k8s.io/apimachinery/pkg/util/wait"
    @@ -421,8 +422,12 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
     				},
     			})
     		} else {
    -			err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) {
    +			err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) bool {
    +				if !proj.IsResourcePermitted(schema.GroupKind{Group: child.ResourceRef.Group, Kind: child.ResourceRef.Kind}, child.Namespace, a.Spec.Destination) {
    +					return false
    +				}
     				nodes = append(nodes, child)
    +				return true
     			})
     			if err != nil {
     				return nil, err
    @@ -432,16 +437,18 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
     	orphanedNodes := make([]appv1.ResourceNode, 0)
     	for k := range orphanedNodesMap {
     		if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k, proj) {
    -			err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) {
    +			err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) bool {
     				belongToAnotherApp := false
     				if appName != "" {
     					if _, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName); exists && err == nil {
     						belongToAnotherApp = true
     					}
     				}
    -				if !belongToAnotherApp {
    -					orphanedNodes = append(orphanedNodes, child)
    +				if belongToAnotherApp || !proj.IsResourcePermitted(schema.GroupKind{Group: child.ResourceRef.Group, Kind: child.ResourceRef.Kind}, child.Namespace, a.Spec.Destination) {
    +					return false
     				}
    +				orphanedNodes = append(orphanedNodes, child)
    +				return true
     			})
     			if err != nil {
     				return nil, err
    @@ -1291,6 +1298,13 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
     		app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
     		app.Status.Health.Status = health.HealthStatusUnknown
     		ctrl.persistAppStatus(origApp, &app.Status)
    +
    +		if err := ctrl.cache.SetAppResourcesTree(app.Name, &appv1.ApplicationTree{}); err != nil {
    +			log.Warnf("failed to set app resource tree: %v", err)
    +		}
    +		if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil {
    +			log.Warnf("failed to set app managed resources tree: %v", err)
    +		}
     		return
     	}
     
    
  • controller/appcontroller_test.go+2 2 modified
    @@ -136,12 +136,12 @@ func newFakeController(data *fakeData) *ApplicationController {
     	mockStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCacheMock, nil)
     	mockStateCache.On("IterateHierarchy", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
     		key := args[1].(kube.ResourceKey)
    -		action := args[2].(func(child argoappv1.ResourceNode, appName string))
    +		action := args[2].(func(child argoappv1.ResourceNode, appName string) bool)
     		appName := ""
     		if res, ok := data.namespacedResources[key]; ok {
     			appName = res.AppName
     		}
    -		action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
    +		_ = action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
     	}).Return(nil)
     	return ctrl
     }
    
  • controller/cache/cache.go+4 4 modified
    @@ -108,7 +108,7 @@ type LiveStateCache interface {
     	// Returns synced cluster cache
     	GetClusterCache(server string) (clustercache.ClusterCache, error)
     	// Executes give callback against resource specified by the key and all its children
    -	IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
    +	IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error
     	// Returns state of live nodes which correspond for target nodes of specified application.
     	GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
     	// IterateResources iterates all resource stored in cache
    @@ -481,13 +481,13 @@ func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool,
     	return clusterInfo.IsNamespaced(gk)
     }
     
    -func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
    +func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error {
     	clusterInfo, err := c.getSyncedCluster(server)
     	if err != nil {
     		return err
     	}
    -	clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
    -		action(asResourceNode(resource), getApp(resource, namespaceResources))
    +	clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) bool {
    +		return action(asResourceNode(resource), getApp(resource, namespaceResources))
     	})
     	return nil
     }
    
  • controller/cache/mocks/LiveStateCache.go+2 2 modified
    @@ -176,11 +176,11 @@ func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool
     }
     
     // IterateHierarchy provides a mock function with given fields: server, key, action
    -func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string)) error {
    +func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string) bool) error {
     	ret := _m.Called(server, key, action)
     
     	var r0 error
    -	if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string)) error); ok {
    +	if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string) bool) error); ok {
     		r0 = rf(server, key, action)
     	} else {
     		r0 = ret.Error(0)
    
  • go.mod+1 1 modified
    @@ -8,7 +8,7 @@ require (
     	github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
     	github.com/alicebob/miniredis v2.5.0+incompatible
     	github.com/alicebob/miniredis/v2 v2.14.2
    -	github.com/argoproj/gitops-engine v0.6.0
    +	github.com/argoproj/gitops-engine v0.6.1-0.20220316000647-723667dff7d5
     	github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998
     	github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0
     	github.com/bombsimon/logrusr/v2 v2.0.1
    
  • go.sum+2 2 modified
    @@ -130,8 +130,8 @@ github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH
     github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
     github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
     github.com/appscode/go v0.0.0-20190808133642-1d4ef1f1c1e0/go.mod h1:iy07dV61Z7QQdCKJCIvUoDL21u6AIceRhZzyleh2ymc=
    -github.com/argoproj/gitops-engine v0.6.0 h1:Tnh6kUUVuBV0m3gueYIymAeErWl9XNN9O9JcOoNM0vU=
    -github.com/argoproj/gitops-engine v0.6.0/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c=
    +github.com/argoproj/gitops-engine v0.6.1-0.20220316000647-723667dff7d5 h1:QZvSvXHKx/hKTvbw3EK+u9JDVlo6UvC4+IVqNIGbcUo=
    +github.com/argoproj/gitops-engine v0.6.1-0.20220316000647-723667dff7d5/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c=
     github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998 h1:V9RDg+IZeebnm3XjkfkbN07VM21Fu1Cy/RJNoHO++VM=
     github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998/go.mod h1:5mKv7zEgI3NO0L+fsuRSwBSY9EIXSuyIsDND8O8TTIw=
     github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0 h1:Cfp7rO/HpVxnwlRqJe0jHiBbZ77ZgXhB6HWlYD02Xdc=
    
  • pkg/apis/application/v1alpha1/app_project_types.go+7 3 modified
    @@ -313,11 +313,15 @@ func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool
     
     // IsLiveResourcePermitted returns whether a live resource found in the cluster is permitted by an AppProject
     func (proj AppProject) IsLiveResourcePermitted(un *unstructured.Unstructured, server string, name string) bool {
    -	if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace() != "") {
    +	return proj.IsResourcePermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace(), ApplicationDestination{Server: server, Name: name})
    +}
    +
    +func (proj AppProject) IsResourcePermitted(groupKind schema.GroupKind, namespace string, dest ApplicationDestination) bool {
    +	if !proj.IsGroupKindPermitted(groupKind, namespace != "") {
     		return false
     	}
    -	if un.GetNamespace() != "" {
    -		return proj.IsDestinationPermitted(ApplicationDestination{Server: server, Namespace: un.GetNamespace(), Name: name})
    +	if namespace != "" {
    +		return proj.IsDestinationPermitted(ApplicationDestination{Server: dest.Server, Name: dest.Name, Namespace: namespace})
     	}
     	return true
     }
    
  • server/application/application.go+23 8 modified
    @@ -488,6 +488,21 @@ func (s *Server) ListResourceEvents(ctx context.Context, q *application.Applicat
     			"involvedObject.namespace": a.Namespace,
     		}).String()
     	} else {
    +		tree, err := s.getAppResources(ctx, a)
    +		if err != nil {
    +			return nil, err
    +		}
    +		found := false
    +		for _, n := range append(tree.Nodes, tree.OrphanedNodes...) {
    +			if n.ResourceRef.UID == q.ResourceUID && n.ResourceRef.Name == q.ResourceName && n.ResourceRef.Namespace == q.ResourceNamespace {
    +				found = true
    +				break
    +			}
    +		}
    +		if !found {
    +			return nil, status.Errorf(codes.InvalidArgument, "%s not found as part of application %s", q.ResourceName, *q.Name)
    +		}
    +
     		namespace = q.ResourceNamespace
     		var config *rest.Config
     		config, err = s.getApplicationClusterConfig(ctx, a)
    @@ -937,7 +952,7 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap
     	return &tree, err
     }
     
    -func (s *Server) getAppResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) {
    +func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) {
     	a, err := s.appLister.Get(*q.Name)
     	if err != nil {
     		return nil, nil, nil, err
    @@ -952,7 +967,7 @@ func (s *Server) getAppResource(ctx context.Context, action string, q *applicati
     	}
     
     	found := tree.FindNode(q.Group, q.Kind, q.Namespace, q.ResourceName)
    -	if found == nil {
    +	if found == nil || found.ResourceRef.UID == "" {
     		return nil, nil, nil, status.Errorf(codes.InvalidArgument, "%s %s %s not found as part of application %s", q.Kind, q.Group, q.ResourceName, *q.Name)
     	}
     	config, err := s.getApplicationClusterConfig(ctx, a)
    @@ -963,7 +978,7 @@ func (s *Server) getAppResource(ctx context.Context, action string, q *applicati
     }
     
     func (s *Server) GetResource(ctx context.Context, q *application.ApplicationResourceRequest) (*application.ApplicationResourceResponse, error) {
    -	res, config, _, err := s.getAppResource(ctx, rbacpolicy.ActionGet, q)
    +	res, config, _, err := s.getAppLiveResource(ctx, rbacpolicy.ActionGet, q)
     	if err != nil {
     		return nil, err
     	}
    @@ -1008,7 +1023,7 @@ func (s *Server) PatchResource(ctx context.Context, q *application.ApplicationRe
     		Version:      q.Version,
     		Group:        q.Group,
     	}
    -	res, config, a, err := s.getAppResource(ctx, rbacpolicy.ActionUpdate, resourceRequest)
    +	res, config, a, err := s.getAppLiveResource(ctx, rbacpolicy.ActionUpdate, resourceRequest)
     	if err != nil {
     		return nil, err
     	}
    @@ -1048,7 +1063,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR
     		Version:      q.Version,
     		Group:        q.Group,
     	}
    -	res, config, a, err := s.getAppResource(ctx, rbacpolicy.ActionDelete, resourceRequest)
    +	res, config, a, err := s.getAppLiveResource(ctx, rbacpolicy.ActionDelete, resourceRequest)
     	if err != nil {
     		return nil, err
     	}
    @@ -1335,7 +1350,7 @@ func getSelectedPods(treeNodes []appv1.ResourceNode, q *application.ApplicationP
     	var pods []appv1.ResourceNode
     	isTheOneMap := make(map[string]bool)
     	for _, treeNode := range treeNodes {
    -		if treeNode.Kind == kube.PodKind && treeNode.Group == "" {
    +		if treeNode.Kind == kube.PodKind && treeNode.Group == "" && treeNode.UID != "" {
     			if isTheSelectedOne(&treeNode, q, treeNodes, isTheOneMap) {
     				pods = append(pods, treeNode)
     			}
    @@ -1625,7 +1640,7 @@ func (s *Server) logResourceEvent(res *appv1.ResourceNode, ctx context.Context,
     }
     
     func (s *Server) ListResourceActions(ctx context.Context, q *application.ApplicationResourceRequest) (*application.ResourceActionsListResponse, error) {
    -	res, config, _, err := s.getAppResource(ctx, rbacpolicy.ActionGet, q)
    +	res, config, _, err := s.getAppLiveResource(ctx, rbacpolicy.ActionGet, q)
     	if err != nil {
     		return nil, err
     	}
    @@ -1676,7 +1691,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA
     		Group:        q.Group,
     	}
     	actionRequest := fmt.Sprintf("%s/%s/%s/%s", rbacpolicy.ActionAction, q.Group, q.Kind, q.Action)
    -	res, config, a, err := s.getAppResource(ctx, actionRequest, resourceRequest)
    +	res, config, a, err := s.getAppLiveResource(ctx, actionRequest, resourceRequest)
     	if err != nil {
     		return nil, err
     	}
    
  • test/e2e/app_management_test.go+111 50 modified
    @@ -1046,64 +1046,125 @@ func TestSyncAsync(t *testing.T) {
     		Expect(SyncStatusIs(SyncStatusCodeSynced))
     }
     
    -func TestPermissions(t *testing.T) {
    -	EnsureCleanState(t)
    -	appName := Name()
    -	_, err := RunCli("proj", "create", "test")
    -	assert.NoError(t, err)
    +// assertResourceActions verifies if view/modify resource actions are successful/failing for given application
    +func assertResourceActions(t *testing.T, appName string, successful bool) {
    +	assertError := func(err error, message string) {
    +		if successful {
    +			assert.NoError(t, err)
    +		} else {
    +			if assert.Error(t, err) {
    +				assert.Contains(t, err.Error(), message)
    +			}
    +		}
    +	}
     
    -	// make sure app cannot be created without permissions in project
    -	_, err = RunCli("app", "create", appName, "--repo", RepoURL(RepoURLTypeFile),
    -		"--path", guestbookPath, "--project", "test", "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
    -	assert.Error(t, err)
    -	sourceError := fmt.Sprintf("application repo %s is not permitted in project 'test'", RepoURL(RepoURLTypeFile))
    -	destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'test'", KubernetesInternalAPIServerAddr, DeploymentNamespace())
    +	closer, cdClient := ArgoCDClientset.NewApplicationClientOrDie()
    +	defer io.Close(closer)
     
    -	assert.Contains(t, err.Error(), sourceError)
    -	assert.Contains(t, err.Error(), destinationError)
    +	deploymentResource, err := KubeClientset.AppsV1().Deployments(DeploymentNamespace()).Get(context.Background(), "guestbook-ui", metav1.GetOptions{})
    +	require.NoError(t, err)
     
    -	proj, err := AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Get(context.Background(), "test", metav1.GetOptions{})
    -	assert.NoError(t, err)
    +	logs, err := cdClient.PodLogs(context.Background(), &applicationpkg.ApplicationPodLogsQuery{
    +		Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Name: &appName, Namespace: DeploymentNamespace(),
    +	})
    +	require.NoError(t, err)
    +	_, err = logs.Recv()
    +	assertError(err, "EOF")
     
    -	proj.Spec.Destinations = []ApplicationDestination{{Server: "*", Namespace: "*"}}
    -	proj.Spec.SourceRepos = []string{"*"}
    -	proj, err = AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Update(context.Background(), proj, metav1.UpdateOptions{})
    -	assert.NoError(t, err)
    +	expectedError := fmt.Sprintf("Deployment apps guestbook-ui not found as part of application %s", appName)
     
    -	// make sure controller report permissions issues in conditions
    -	_, err = RunCli("app", "create", appName, "--repo", RepoURL(RepoURLTypeFile),
    -		"--path", guestbookPath, "--project", "test", "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
    -	assert.NoError(t, err)
    -	defer func() {
    -		err = AppClientset.ArgoprojV1alpha1().Applications(ArgoCDNamespace).Delete(context.Background(), appName, metav1.DeleteOptions{})
    -		assert.NoError(t, err)
    -	}()
    +	_, err = cdClient.ListResourceEvents(context.Background(), &applicationpkg.ApplicationResourceEventsQuery{
    +		Name: &appName, ResourceName: "guestbook-ui", ResourceNamespace: DeploymentNamespace(), ResourceUID: string(deploymentResource.UID)})
    +	assertError(err, fmt.Sprintf("%s not found as part of application %s", "guestbook-ui", appName))
     
    -	proj.Spec.Destinations = []ApplicationDestination{}
    -	proj.Spec.SourceRepos = []string{}
    -	_, err = AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Update(context.Background(), proj, metav1.UpdateOptions{})
    -	assert.NoError(t, err)
    -	time.Sleep(1 * time.Second)
    -	closer, client, err := ArgoCDClientset.NewApplicationClient()
    -	assert.NoError(t, err)
    -	defer io.Close(closer)
    +	_, err = cdClient.GetResource(context.Background(), &applicationpkg.ApplicationResourceRequest{
    +		Name: &appName, ResourceName: "guestbook-ui", Namespace: DeploymentNamespace(), Version: "v1", Group: "apps", Kind: "Deployment"})
    +	assertError(err, expectedError)
     
    -	refresh := string(RefreshTypeNormal)
    -	app, err := client.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName, Refresh: &refresh})
    -	assert.NoError(t, err)
    +	_, err = cdClient.DeleteResource(context.Background(), &applicationpkg.ApplicationResourceDeleteRequest{
    +		Name: &appName, ResourceName: "guestbook-ui", Namespace: DeploymentNamespace(), Version: "v1", Group: "apps", Kind: "Deployment",
    +	})
    +	assertError(err, expectedError)
     
    -	destinationErrorExist := false
    -	sourceErrorExist := false
    -	for i := range app.Status.Conditions {
    -		if strings.Contains(app.Status.Conditions[i].Message, destinationError) {
    -			destinationErrorExist = true
    -		}
    -		if strings.Contains(app.Status.Conditions[i].Message, sourceError) {
    -			sourceErrorExist = true
    -		}
    -	}
    -	assert.True(t, destinationErrorExist)
    -	assert.True(t, sourceErrorExist)
    +	_, err = cdClient.RunResourceAction(context.Background(), &applicationpkg.ResourceActionRunRequest{
    +		Name: &appName, ResourceName: "guestbook-ui", Namespace: DeploymentNamespace(), Version: "v1", Group: "apps", Kind: "Deployment", Action: "restart",
    +	})
    +	assertError(err, expectedError)
    +}
    +
    +func TestPermissions(t *testing.T) {
    +	appCtx := Given(t)
    +	projName := "argo-project"
    +	projActions := projectFixture.
    +		Given(t).
    +		Name(projName).
    +		When().
    +		Create()
    +
    +	sourceError := fmt.Sprintf("application repo %s is not permitted in project 'argo-project'", RepoURL(RepoURLTypeFile))
    +	destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'argo-project'", KubernetesInternalAPIServerAddr, DeploymentNamespace())
    +
    +	appCtx.
    +		Path("guestbook-logs").
    +		Project(projName).
    +		When().
    +		IgnoreErrors().
    +		// ensure app is not created if project permissions are missing
    +		CreateApp().
    +		Then().
    +		Expect(Error("", sourceError)).
    +		Expect(Error("", destinationError)).
    +		When().
    +		DoNotIgnoreErrors().
    +		// add missing permissions, create and sync app
    +		And(func() {
    +			projActions.AddDestination("*", "*")
    +			projActions.AddSource("*")
    +		}).
    +		CreateApp().
    +		Sync().
    +		Then().
    +		// make sure application resource actiions are successful
    +		And(func(app *Application) {
    +			assertResourceActions(t, app.Name, true)
    +		}).
    +		When().
    +		// remove projet permissions and "refresh" app
    +		And(func() {
    +			projActions.UpdateProject(func(proj *AppProject) {
    +				proj.Spec.Destinations = nil
    +				proj.Spec.SourceRepos = nil
    +			})
    +		}).
    +		Refresh(RefreshTypeNormal).
    +		Then().
    +		// ensure app resource tree is empty when source/destination permissions are missing
    +		Expect(Condition(ApplicationConditionInvalidSpecError, destinationError)).
    +		Expect(Condition(ApplicationConditionInvalidSpecError, sourceError)).
    +		And(func(app *Application) {
    +			closer, cdClient := ArgoCDClientset.NewApplicationClientOrDie()
    +			defer io.Close(closer)
    +			tree, err := cdClient.ResourceTree(context.Background(), &applicationpkg.ResourcesQuery{ApplicationName: &app.Name})
    +			require.NoError(t, err)
    +			assert.Len(t, tree.Nodes, 0)
    +			assert.Len(t, tree.OrphanedNodes, 0)
    +		}).
    +		When().
    +		// add missing permissions but deny management of Deployment kind
    +		And(func() {
    +			projActions.
    +				AddDestination("*", "*").
    +				AddSource("*").
    +				UpdateProject(func(proj *AppProject) {
    +					proj.Spec.NamespaceResourceBlacklist = []metav1.GroupKind{{Group: "*", Kind: "Deployment"}}
    +				})
    +		}).
    +		Refresh(RefreshTypeNormal).
    +		Then().
    +		// make sure application resource actiions are failing
    +		And(func(app *Application) {
    +			assertResourceActions(t, "test-permissions", false)
    +		})
     }
     
     func TestPermissionWithScopedRepo(t *testing.T) {
    
  • test/e2e/fixture/project/actions.go+27 0 modified
    @@ -1,7 +1,12 @@
     package project
     
     import (
    +	"context"
    +
    +	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
    +	"github.com/stretchr/testify/require"
    +	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     )
     
     // this implements the "when" part of given/when/then
    @@ -34,6 +39,25 @@ func (a *Actions) Create(args ...string) *Actions {
     	return a
     }
     
    +func (a *Actions) AddDestination(cluster string, namespace string) *Actions {
    +	a.runCli("proj", "add-destination", a.context.name, cluster, namespace)
    +	return a
    +}
    +
    +func (a *Actions) AddSource(repo string) *Actions {
    +	a.runCli("proj", "add-source", a.context.name, repo)
    +	return a
    +}
    +
    +func (a *Actions) UpdateProject(updater func(project *v1alpha1.AppProject)) *Actions {
    +	proj, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Get(context.TODO(), a.context.name, v1.GetOptions{})
    +	require.NoError(a.context.t, err)
    +	updater(proj)
    +	_, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Update(context.TODO(), proj, v1.UpdateOptions{})
    +	require.NoError(a.context.t, err)
    +	return a
    +}
    +
     func (a *Actions) Name(name string) *Actions {
     	a.context.name = name
     	return a
    @@ -72,4 +96,7 @@ func (a *Actions) Then() *Consequences {
     func (a *Actions) runCli(args ...string) {
     	a.context.t.Helper()
     	a.lastOutput, a.lastError = fixture.RunCli(args...)
    +	if !a.ignoreErrors {
    +		require.Empty(a.context.t, a.lastError)
    +	}
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.