Moderate severityNVD Advisory· Published Apr 15, 2024· Updated Aug 2, 2024
Argo CD' API server does not enforce project sourceNamespaces
CVE-2024-31990
Description
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. The API server does not enforce project sourceNamespaces which allows attackers to use the UI to edit resources which should only be mutable via gitops. This vulenrability is fixed in 2.10.7, 2.9.12, and 2.8.16.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cd/v2Go | >= 2.4.0, < 2.8.16 | 2.8.16 |
github.com/argoproj/argo-cd/v2Go | >= 2.9.0, < 2.9.12 | 2.9.12 |
github.com/argoproj/argo-cd/v2Go | >= 2.10.0, < 2.10.7 | 2.10.7 |
Affected products
1Patches
3e0ff56d89fbdMerge pull request from GHSA-2gvw-w6fj-7m3c
5 files changed · +223 −103
pkg/apis/application/v1alpha1/app_project_types.go+18 −0 modified@@ -17,6 +17,24 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +type ErrApplicationNotAllowedToUseProject struct { + application string + namespace string + project string +} + +func NewErrApplicationNotAllowedToUseProject(application, namespace, project string) error { + return &ErrApplicationNotAllowedToUseProject{ + application: application, + namespace: namespace, + project: project, + } +} + +func (err *ErrApplicationNotAllowedToUseProject) Error() string { + return fmt.Sprintf("application '%s' in namespace '%s' is not allowed to use project %s", err.application, err.namespace, err.project) +} + // AppProjectList is list of AppProject resources // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type AppProjectList struct {
server/application/application.go+89 −96 modified@@ -148,7 +148,7 @@ func NewServer( // // If the user does provide a "project," we can respond more specifically. If the user does not have access to the given // app name in the given project, we return "permission denied." If the app exists, but the project is different from -func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, error) { +func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, *appv1.AppProject, error) { logCtx := log.WithFields(map[string]interface{}{ "application": name, "namespace": namespace, @@ -165,23 +165,23 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa // but the app is in a different project" response. We don't want the user inferring the existence of the // app from response time. _, _ = getApp() - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } } a, err := getApp() if err != nil { if apierr.IsNotFound(err) { if project != "" { // We know that the user was allowed to get the Application, but the Application does not exist. Return 404. - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } // We don't know if the user was allowed to get the Application, and we don't want to leak information about // the Application's existence. Return 403. logCtx.Warn("application does not exist") - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } logCtx.Errorf("failed to get application: %s", err) - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } // Even if we performed an initial RBAC check (because the request was fully parameterized), we still need to // perform a second RBAC check to ensure that the user has access to the actual Application's project (not just the @@ -195,11 +195,11 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa // The user specified a project. We would have returned a 404 if the user had access to the app, but the app // did not exist. So we have to return a 404 when the app does exist, but the user does not have access. // Otherwise, they could infer that the app exists based on the error code. - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } // The user didn't specify a project. We always return permission denied for both lack of access and lack of // existence. - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } effectiveProject := "default" if a.Spec.Project != "" { @@ -212,15 +212,20 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa }).Warnf("user tried to %s application in project %s, but the application is in project %s", action, project, effectiveProject) // The user has access to the app, but the app is in a different project. Return 404, meaning "app doesn't // exist in that project". - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } - return a, nil + // Get the app's associated project, and make sure all project restrictions are enforced. + proj, err := s.getAppProject(ctx, a, logCtx) + if err != nil { + return a, nil, err + } + return a, proj, nil } // getApplicationEnforceRBACInformer uses an informer to get an Application. If the app does not exist, permission is // denied, or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive // information. -func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, error) { +func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, *appv1.AppProject, error) { namespaceOrDefault := s.appNamespaceOrDefault(namespace) return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) { return s.appLister.Applications(namespaceOrDefault).Get(name) @@ -230,7 +235,7 @@ func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, // getApplicationEnforceRBACClient uses a client to get an Application. If the app does not exist, permission is denied, // or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive // information. -func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, error) { +func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, *appv1.AppProject, error) { namespaceOrDefault := s.appNamespaceOrDefault(namespace) return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) { if !s.isNamespaceEnabled(namespaceOrDefault) { @@ -314,7 +319,13 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq if q.Validate != nil { validate = *q.Validate } - err := s.validateAndNormalizeApp(ctx, a, validate) + + proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name)) + if err != nil { + return nil, err + } + + err = s.validateAndNormalizeApp(ctx, a, proj, validate) if err != nil { return nil, fmt.Errorf("error while validating and normalizing app: %w", err) } @@ -370,7 +381,7 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq return updated, nil } -func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, action func( +func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, proj *appv1.AppProject, action func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, @@ -397,13 +408,6 @@ func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, acti if err != nil { return fmt.Errorf("error getting kustomize settings options: %w", err) } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project) - } - return fmt.Errorf("error getting application's project: %w", err) - } helmRepos, err := s.db.ListHelmRepositories(ctx) if err != nil { @@ -438,7 +442,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan if q.Name == nil || *q.Name == "" { return nil, fmt.Errorf("invalid request: application name is missing") } - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -450,7 +454,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan } var manifestInfo *apiclient.ManifestResponse - err = s.queryRepoServer(ctx, a, func( + err = s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error { revision := source.TargetRevision if q.GetRevision() != "" { @@ -476,11 +480,6 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan return fmt.Errorf("error getting API resources: %w", err) } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return fmt.Errorf("error getting app project: %w", err) - } - manifestInfo, err = client.GenerateManifest(ctx, &apiclient.ManifestRequest{ Repo: repo, Revision: revision, @@ -543,13 +542,13 @@ func (s *Server) GetManifestsWithFiles(stream application.ApplicationService_Get return fmt.Errorf("invalid request: application name is missing") } - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName()) if err != nil { return err } var manifestInfo *apiclient.ManifestResponse - err = s.queryRepoServer(ctx, a, func( + err = s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error { appInstanceLabelKey, err := s.settingsMgr.GetAppInstanceLabelKey() @@ -660,7 +659,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app // We must use a client Get instead of an informer Get, because it's common to call Get immediately // following a Watch (which is not yet powered by an informer), and the Get must reflect what was // previously seen by the client. - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion()) + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion()) if err != nil { return nil, err } @@ -691,7 +690,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app if refreshType == appv1.RefreshTypeHard { // force refresh cached application details - if err := s.queryRepoServer(ctx, a, func( + if err := s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, @@ -743,7 +742,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app // ListResourceEvents returns a list of event resources func (s *Server) ListResourceEvents(ctx context.Context, q *application.ApplicationResourceEventsQuery) (*v1.EventList, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -811,12 +810,12 @@ func (s *Server) validateAndUpdateApp(ctx context.Context, newApp *appv1.Applica s.projectLock.RLock(newApp.Spec.GetProject()) defer s.projectLock.RUnlock(newApp.Spec.GetProject()) - app, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "") + app, proj, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "") if err != nil { return nil, err } - err = s.validateAndNormalizeApp(ctx, newApp, validate) + err = s.validateAndNormalizeApp(ctx, newApp, proj, validate) if err != nil { return nil, fmt.Errorf("error validating and normalizing app: %w", err) } @@ -915,7 +914,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat if q.GetSpec() == nil { return nil, fmt.Errorf("error updating application spec: spec is nil in request") } - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } @@ -934,7 +933,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat // Patch patches an application func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchRequest) (*appv1.Application, error) { - app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + app, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } @@ -977,11 +976,35 @@ func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchReque return s.validateAndUpdateApp(ctx, newApp, false, true, rbacpolicy.ActionUpdate, q.GetProject()) } +func (s *Server) getAppProject(ctx context.Context, a *appv1.Application, logCtx *log.Entry) (*appv1.AppProject, error) { + proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) + if err == nil { + return proj, nil + } + + // If there's a permission issue or the app doesn't exist, return a vague error to avoid letting the user enumerate project names. + vagueError := status.Errorf(codes.InvalidArgument, "app is not allowed in project %q, or the project does not exist", a.Spec.Project) + + if apierr.IsNotFound(err) { + return nil, vagueError + } + + if _, ok := err.(*appv1.ErrApplicationNotAllowedToUseProject); ok { + logCtx.WithFields(map[string]interface{}{ + "project": a.Spec.Project, + argocommon.SecurityField: argocommon.SecurityMedium, + }).Warnf("error getting app project: %s", err) + return nil, vagueError + } + + return nil, vagueError +} + // Delete removes an application and all associated resources func (s *Server) Delete(ctx context.Context, q *application.ApplicationDeleteRequest) (*application.ApplicationResponse, error) { appName := q.GetName() appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "") if err != nil { return nil, err } @@ -1136,16 +1159,7 @@ func (s *Server) Watch(q *application.ApplicationQuery, ws application.Applicati } } -func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, validate bool) error { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - // Offer no hint that the project does not exist. - log.Warnf("User attempted to create/update application in non-existent project %q", app.Spec.Project) - return permissionDeniedErr - } - return fmt.Errorf("error getting application's project: %w", err) - } +func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, validate bool) error { if app.GetName() == "" { return fmt.Errorf("resource name may not be empty") } @@ -1249,7 +1263,7 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap } func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, nil, nil, err } @@ -1386,7 +1400,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR } func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery) (*appv1.ApplicationTree, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return nil, err } @@ -1395,7 +1409,7 @@ func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery } func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application.ApplicationService_WatchResourceTreeServer) error { - _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + _, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return err } @@ -1412,7 +1426,7 @@ func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application } func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.RevisionMetadata, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -1422,12 +1436,6 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe if err != nil { return nil, fmt.Errorf("error getting repository by URL: %w", err) } - // We need to get some information with the project associated to the app, - // so we'll know whether GPG signatures are enforced. - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, fmt.Errorf("error getting app project: %w", err) - } conn, repoClient, err := s.repoClientset.NewRepoServerClient() if err != nil { return nil, fmt.Errorf("error creating repo server client: %w", err) @@ -1442,7 +1450,7 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe // RevisionChartDetails returns the helm chart metadata, as fetched from the reposerver func (s *Server) RevisionChartDetails(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.ChartDetails, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -1473,7 +1481,7 @@ func isMatchingResource(q *application.ResourcesQuery, key kube.ResourceKey) boo } func (s *Server) ManagedResources(ctx context.Context, q *application.ResourcesQuery) (*application.ManagedResourcesResponse, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return nil, err } @@ -1530,7 +1538,7 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application. } } - a, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return err } @@ -1722,19 +1730,11 @@ func isTheSelectedOne(currentNode *appv1.ResourceNode, q *application.Applicatio // Sync syncs an application to its target state func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncRequest) (*appv1.Application, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "") if err != nil { return nil, err } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return a, status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project) - } - return a, fmt.Errorf("error getting app project: %w", err) - } - s.inferResourcesStatusHealth(a) if !proj.Spec.SyncWindows.Matches(a).CanSync(true) { @@ -1831,7 +1831,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR } func (s *Server) Rollback(ctx context.Context, rollbackReq *application.ApplicationRollbackRequest) (*appv1.Application, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "") if err != nil { return nil, err } @@ -1890,7 +1890,7 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *application.Applicat } func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksRequest) (*application.LinksResponse, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "") if err != nil { return nil, err } @@ -1905,7 +1905,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq return nil, fmt.Errorf("failed to read application deep links from configmap: %w", err) } - clstObj, _, err := s.getObjectsForDeepLinks(ctx, a) + clstObj, _, err := s.getObjectsForDeepLinks(ctx, a, proj) if err != nil { return nil, err } @@ -1920,12 +1920,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq return finalList, nil } -func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, nil, fmt.Errorf("error getting app project: %w", err) - } - +func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application, proj *appv1.AppProject) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) { // sanitize project jwt tokens proj.Status = appv1.AppProjectStatus{} @@ -1988,7 +1983,12 @@ func (s *Server) ListResourceLinks(ctx context.Context, req *application.Applica return nil, err } - clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app) + proj, err := s.getAppProject(ctx, app, log.WithField("application", app.GetName())) + if err != nil { + return nil, err + } + + clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app, proj) if err != nil { return nil, err } @@ -2044,7 +2044,7 @@ func (s *Server) resolveRevision(ctx context.Context, app *appv1.Application, sy func (s *Server) TerminateOperation(ctx context.Context, termOpReq *application.OperationTerminateRequest) (*application.OperationTerminateResponse, error) { appName := termOpReq.GetName() appNs := s.appNamespaceOrDefault(termOpReq.GetAppNamespace()) - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "") if err != nil { return nil, err } @@ -2117,7 +2117,7 @@ func (s *Server) ListResourceActions(ctx context.Context, q *application.Applica func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacRequest string, q *application.ApplicationResourceRequest) (obj *unstructured.Unstructured, res *appv1.ResourceNode, app *appv1.Application, config *rest.Config, err error) { if q.GetKind() == applicationType.ApplicationKind && q.GetGroup() == applicationType.Group && q.GetName() == q.GetResourceName() { - app, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName()) + app, _, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, nil, nil, nil, err } @@ -2213,14 +2213,19 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA } } + proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name)) + if err != nil { + return nil, err + } + // First, make sure all the returned resources are permitted, for each operation. // Also perform create with dry-runs for all create-operation resources. // This is performed separately to reduce the risk of only some of the resources being successfully created later. // TODO: when apply/delete operations would be supported for custom actions, // the dry-run for relevant apply/delete operation would have to be invoked as well. for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj - err := s.verifyResourcePermitted(ctx, app, newObj) + err := s.verifyResourcePermitted(ctx, app, proj, newObj) if err != nil { return nil, err } @@ -2314,14 +2319,7 @@ func (s *Server) patchResource(ctx context.Context, config *rest.Config, liveObj return &application.ApplicationResponse{}, nil } -func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, obj *unstructured.Unstructured) error { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return fmt.Errorf("application references project %s which does not exist", app.Spec.Project) - } - return fmt.Errorf("failed to get project %s: %w", app.Spec.Project, err) - } +func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, obj *unstructured.Unstructured) error { permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: obj.GroupVersionKind().Group, Kind: obj.GroupVersionKind().Kind}, obj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { clusters, err := s.db.GetProjectClusters(context.TODO(), project) if err != nil { @@ -2381,16 +2379,11 @@ func splitStatusPatch(patch []byte) ([]byte, []byte, error) { } func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.ApplicationSyncWindowsQuery) (*application.ApplicationSyncWindowsResponse, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, fmt.Errorf("error getting app project: %w", err) - } - windows := proj.Spec.SyncWindows.Matches(a) sync := windows.CanSync(true)
server/application/application_test.go+114 −4 modified@@ -1818,7 +1818,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) { appServer := newTestAppServer(t, testApp) active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) - assert.Contains(t, err.Error(), "not found") + assert.Contains(t, err.Error(), "not exist") assert.Nil(t, active) }) } @@ -2428,7 +2428,16 @@ func TestAppNamespaceRestrictions(t *testing.T) { t.Run("Get application in other namespace when allowed", func(t *testing.T) { testApp := newTestApp() testApp.Namespace = "argocd-1" - appServer := newTestAppServer(t, testApp) + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ Name: pointer.String("test-app"), @@ -2439,6 +2448,28 @@ func TestAppNamespaceRestrictions(t *testing.T) { require.Equal(t, "argocd-1", app.Namespace) require.Equal(t, "test-app", app.Name) }) + t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ + Name: pointer.String("test-app"), + AppNamespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.Nil(t, app) + require.ErrorContains(t, err, "app is not allowed in project") + }) t.Run("Create application in other namespace when allowed", func(t *testing.T) { testApp := newTestApp() testApp.Namespace = "argocd-1" @@ -2481,7 +2512,7 @@ func TestAppNamespaceRestrictions(t *testing.T) { }) require.Error(t, err) require.Nil(t, app) - require.ErrorContains(t, err, "not allowed to use project") + require.ErrorContains(t, err, "app is not allowed in project") }) t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) { @@ -2505,5 +2536,84 @@ func TestAppNamespaceRestrictions(t *testing.T) { require.Nil(t, app) require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted") }) - + t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) + assert.NoError(t, err) + assert.Equal(t, 0, len(active.ActiveWindows)) + }) + t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) + require.Error(t, err) + require.Nil(t, active) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ + Name: pointer.String("test-app"), + Namespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.Nil(t, links) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ + Name: pointer.String("test-app"), + Namespace: pointer.String("argocd-1"), + }) + require.NoError(t, err) + assert.Equal(t, 0, len(links.Items)) + }) }
util/argo/argo.go+1 −2 modified@@ -694,8 +694,7 @@ func GetAppProject(app *argoappv1.Application, projLister applicationsv1.AppProj return nil, err } if !proj.IsAppNamespacePermitted(app, ns) { - return nil, fmt.Errorf("application '%s' in namespace '%s' is not allowed to use project '%s'", - app.Name, app.Namespace, proj.Name) + return nil, argoappv1.NewErrApplicationNotAllowedToUseProject(app.Name, app.Namespace, proj.Name) } return proj, nil }
util/session/sessionmanager.go+1 −1 modified@@ -312,7 +312,7 @@ func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAtte return expiredCount } -// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache. +// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache. func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string { idx := rand.Intn(len(failures) - 1) i := 0
c514105af739Merge pull request from GHSA-2gvw-w6fj-7m3c
5 files changed · +223 −103
pkg/apis/application/v1alpha1/app_project_types.go+18 −0 modified@@ -17,6 +17,24 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +type ErrApplicationNotAllowedToUseProject struct { + application string + namespace string + project string +} + +func NewErrApplicationNotAllowedToUseProject(application, namespace, project string) error { + return &ErrApplicationNotAllowedToUseProject{ + application: application, + namespace: namespace, + project: project, + } +} + +func (err *ErrApplicationNotAllowedToUseProject) Error() string { + return fmt.Sprintf("application '%s' in namespace '%s' is not allowed to use project %s", err.application, err.namespace, err.project) +} + // AppProjectList is list of AppProject resources // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type AppProjectList struct {
server/application/application.go+89 −96 modified@@ -151,7 +151,7 @@ func NewServer( // // If the user does provide a "project," we can respond more specifically. If the user does not have access to the given // app name in the given project, we return "permission denied." If the app exists, but the project is different from -func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, error) { +func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, *appv1.AppProject, error) { user := session.Username(ctx) if user == "" { user = "Unknown user" @@ -173,23 +173,23 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa // but the app is in a different project" response. We don't want the user inferring the existence of the // app from response time. _, _ = getApp() - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } } a, err := getApp() if err != nil { if apierr.IsNotFound(err) { if project != "" { // We know that the user was allowed to get the Application, but the Application does not exist. Return 404. - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } // We don't know if the user was allowed to get the Application, and we don't want to leak information about // the Application's existence. Return 403. logCtx.Warn("application does not exist") - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } logCtx.Errorf("failed to get application: %s", err) - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } // Even if we performed an initial RBAC check (because the request was fully parameterized), we still need to // perform a second RBAC check to ensure that the user has access to the actual Application's project (not just the @@ -203,11 +203,11 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa // The user specified a project. We would have returned a 404 if the user had access to the app, but the app // did not exist. So we have to return a 404 when the app does exist, but the user does not have access. // Otherwise, they could infer that the app exists based on the error code. - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } // The user didn't specify a project. We always return permission denied for both lack of access and lack of // existence. - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } effectiveProject := "default" if a.Spec.Project != "" { @@ -220,15 +220,20 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa }).Warnf("user tried to %s application in project %s, but the application is in project %s", action, project, effectiveProject) // The user has access to the app, but the app is in a different project. Return 404, meaning "app doesn't // exist in that project". - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } - return a, nil + // Get the app's associated project, and make sure all project restrictions are enforced. + proj, err := s.getAppProject(ctx, a, logCtx) + if err != nil { + return a, nil, err + } + return a, proj, nil } // getApplicationEnforceRBACInformer uses an informer to get an Application. If the app does not exist, permission is // denied, or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive // information. -func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, error) { +func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, *appv1.AppProject, error) { namespaceOrDefault := s.appNamespaceOrDefault(namespace) return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) { return s.appLister.Applications(namespaceOrDefault).Get(name) @@ -238,7 +243,7 @@ func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, // getApplicationEnforceRBACClient uses a client to get an Application. If the app does not exist, permission is denied, // or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive // information. -func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, error) { +func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, *appv1.AppProject, error) { namespaceOrDefault := s.appNamespaceOrDefault(namespace) return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) { if !s.isNamespaceEnabled(namespaceOrDefault) { @@ -322,7 +327,13 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq if q.Validate != nil { validate = *q.Validate } - err := s.validateAndNormalizeApp(ctx, a, validate) + + proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name)) + if err != nil { + return nil, err + } + + err = s.validateAndNormalizeApp(ctx, a, proj, validate) if err != nil { return nil, fmt.Errorf("error while validating and normalizing app: %w", err) } @@ -378,7 +389,7 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq return updated, nil } -func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, action func( +func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, proj *appv1.AppProject, action func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, @@ -405,13 +416,6 @@ func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, acti if err != nil { return fmt.Errorf("error getting kustomize settings options: %w", err) } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project) - } - return fmt.Errorf("error getting application's project: %w", err) - } helmRepos, err := s.db.ListHelmRepositories(ctx) if err != nil { @@ -446,7 +450,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan if q.Name == nil || *q.Name == "" { return nil, fmt.Errorf("invalid request: application name is missing") } - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -458,7 +462,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan } var manifestInfo *apiclient.ManifestResponse - err = s.queryRepoServer(ctx, a, func( + err = s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error { revision := source.TargetRevision if q.GetRevision() != "" { @@ -484,11 +488,6 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan return fmt.Errorf("error getting API resources: %w", err) } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return fmt.Errorf("error getting app project: %w", err) - } - manifestInfo, err = client.GenerateManifest(ctx, &apiclient.ManifestRequest{ Repo: repo, Revision: revision, @@ -551,13 +550,13 @@ func (s *Server) GetManifestsWithFiles(stream application.ApplicationService_Get return fmt.Errorf("invalid request: application name is missing") } - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName()) if err != nil { return err } var manifestInfo *apiclient.ManifestResponse - err = s.queryRepoServer(ctx, a, func( + err = s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error { appInstanceLabelKey, err := s.settingsMgr.GetAppInstanceLabelKey() @@ -668,7 +667,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app // We must use a client Get instead of an informer Get, because it's common to call Get immediately // following a Watch (which is not yet powered by an informer), and the Get must reflect what was // previously seen by the client. - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion()) + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion()) if err != nil { return nil, err } @@ -699,7 +698,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app if refreshType == appv1.RefreshTypeHard { // force refresh cached application details - if err := s.queryRepoServer(ctx, a, func( + if err := s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, @@ -751,7 +750,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app // ListResourceEvents returns a list of event resources func (s *Server) ListResourceEvents(ctx context.Context, q *application.ApplicationResourceEventsQuery) (*v1.EventList, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -819,12 +818,12 @@ func (s *Server) validateAndUpdateApp(ctx context.Context, newApp *appv1.Applica s.projectLock.RLock(newApp.Spec.GetProject()) defer s.projectLock.RUnlock(newApp.Spec.GetProject()) - app, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "") + app, proj, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "") if err != nil { return nil, err } - err = s.validateAndNormalizeApp(ctx, newApp, validate) + err = s.validateAndNormalizeApp(ctx, newApp, proj, validate) if err != nil { return nil, fmt.Errorf("error validating and normalizing app: %w", err) } @@ -923,7 +922,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat if q.GetSpec() == nil { return nil, fmt.Errorf("error updating application spec: spec is nil in request") } - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } @@ -942,7 +941,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat // Patch patches an application func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchRequest) (*appv1.Application, error) { - app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + app, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } @@ -985,11 +984,35 @@ func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchReque return s.validateAndUpdateApp(ctx, newApp, false, true, rbacpolicy.ActionUpdate, q.GetProject()) } +func (s *Server) getAppProject(ctx context.Context, a *appv1.Application, logCtx *log.Entry) (*appv1.AppProject, error) { + proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) + if err == nil { + return proj, nil + } + + // If there's a permission issue or the app doesn't exist, return a vague error to avoid letting the user enumerate project names. + vagueError := status.Errorf(codes.InvalidArgument, "app is not allowed in project %q, or the project does not exist", a.Spec.Project) + + if apierr.IsNotFound(err) { + return nil, vagueError + } + + if _, ok := err.(*appv1.ErrApplicationNotAllowedToUseProject); ok { + logCtx.WithFields(map[string]interface{}{ + "project": a.Spec.Project, + argocommon.SecurityField: argocommon.SecurityMedium, + }).Warnf("error getting app project: %s", err) + return nil, vagueError + } + + return nil, vagueError +} + // Delete removes an application and all associated resources func (s *Server) Delete(ctx context.Context, q *application.ApplicationDeleteRequest) (*application.ApplicationResponse, error) { appName := q.GetName() appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "") if err != nil { return nil, err } @@ -1144,16 +1167,7 @@ func (s *Server) Watch(q *application.ApplicationQuery, ws application.Applicati } } -func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, validate bool) error { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - // Offer no hint that the project does not exist. - log.Warnf("User attempted to create/update application in non-existent project %q", app.Spec.Project) - return permissionDeniedErr - } - return fmt.Errorf("error getting application's project: %w", err) - } +func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, validate bool) error { if app.GetName() == "" { return fmt.Errorf("resource name may not be empty") } @@ -1257,7 +1271,7 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap } func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, nil, nil, err } @@ -1394,7 +1408,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR } func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery) (*appv1.ApplicationTree, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return nil, err } @@ -1403,7 +1417,7 @@ func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery } func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application.ApplicationService_WatchResourceTreeServer) error { - _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + _, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return err } @@ -1420,7 +1434,7 @@ func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application } func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.RevisionMetadata, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -1430,12 +1444,6 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe if err != nil { return nil, fmt.Errorf("error getting repository by URL: %w", err) } - // We need to get some information with the project associated to the app, - // so we'll know whether GPG signatures are enforced. - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, fmt.Errorf("error getting app project: %w", err) - } conn, repoClient, err := s.repoClientset.NewRepoServerClient() if err != nil { return nil, fmt.Errorf("error creating repo server client: %w", err) @@ -1450,7 +1458,7 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe // RevisionChartDetails returns the helm chart metadata, as fetched from the reposerver func (s *Server) RevisionChartDetails(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.ChartDetails, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -1481,7 +1489,7 @@ func isMatchingResource(q *application.ResourcesQuery, key kube.ResourceKey) boo } func (s *Server) ManagedResources(ctx context.Context, q *application.ResourcesQuery) (*application.ManagedResourcesResponse, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return nil, err } @@ -1538,7 +1546,7 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application. } } - a, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return err } @@ -1730,19 +1738,11 @@ func isTheSelectedOne(currentNode *appv1.ResourceNode, q *application.Applicatio // Sync syncs an application to its target state func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncRequest) (*appv1.Application, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "") if err != nil { return nil, err } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return a, status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project) - } - return a, fmt.Errorf("error getting app project: %w", err) - } - s.inferResourcesStatusHealth(a) if !proj.Spec.SyncWindows.Matches(a).CanSync(true) { @@ -1839,7 +1839,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR } func (s *Server) Rollback(ctx context.Context, rollbackReq *application.ApplicationRollbackRequest) (*appv1.Application, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "") if err != nil { return nil, err } @@ -1898,7 +1898,7 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *application.Applicat } func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksRequest) (*application.LinksResponse, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "") if err != nil { return nil, err } @@ -1913,7 +1913,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq return nil, fmt.Errorf("failed to read application deep links from configmap: %w", err) } - clstObj, _, err := s.getObjectsForDeepLinks(ctx, a) + clstObj, _, err := s.getObjectsForDeepLinks(ctx, a, proj) if err != nil { return nil, err } @@ -1928,12 +1928,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq return finalList, nil } -func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, nil, fmt.Errorf("error getting app project: %w", err) - } - +func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application, proj *appv1.AppProject) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) { // sanitize project jwt tokens proj.Status = appv1.AppProjectStatus{} @@ -1996,7 +1991,12 @@ func (s *Server) ListResourceLinks(ctx context.Context, req *application.Applica return nil, err } - clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app) + proj, err := s.getAppProject(ctx, app, log.WithField("application", app.GetName())) + if err != nil { + return nil, err + } + + clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app, proj) if err != nil { return nil, err } @@ -2052,7 +2052,7 @@ func (s *Server) resolveRevision(ctx context.Context, app *appv1.Application, sy func (s *Server) TerminateOperation(ctx context.Context, termOpReq *application.OperationTerminateRequest) (*application.OperationTerminateResponse, error) { appName := termOpReq.GetName() appNs := s.appNamespaceOrDefault(termOpReq.GetAppNamespace()) - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "") if err != nil { return nil, err } @@ -2125,7 +2125,7 @@ func (s *Server) ListResourceActions(ctx context.Context, q *application.Applica func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacRequest string, q *application.ApplicationResourceRequest) (obj *unstructured.Unstructured, res *appv1.ResourceNode, app *appv1.Application, config *rest.Config, err error) { if q.GetKind() == applicationType.ApplicationKind && q.GetGroup() == applicationType.Group && q.GetName() == q.GetResourceName() { - app, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName()) + app, _, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, nil, nil, nil, err } @@ -2221,14 +2221,19 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA } } + proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name)) + if err != nil { + return nil, err + } + // First, make sure all the returned resources are permitted, for each operation. // Also perform create with dry-runs for all create-operation resources. // This is performed separately to reduce the risk of only some of the resources being successfully created later. // TODO: when apply/delete operations would be supported for custom actions, // the dry-run for relevant apply/delete operation would have to be invoked as well. for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj - err := s.verifyResourcePermitted(ctx, app, newObj) + err := s.verifyResourcePermitted(ctx, app, proj, newObj) if err != nil { return nil, err } @@ -2322,14 +2327,7 @@ func (s *Server) patchResource(ctx context.Context, config *rest.Config, liveObj return &application.ApplicationResponse{}, nil } -func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, obj *unstructured.Unstructured) error { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return fmt.Errorf("application references project %s which does not exist", app.Spec.Project) - } - return fmt.Errorf("failed to get project %s: %w", app.Spec.Project, err) - } +func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, obj *unstructured.Unstructured) error { permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: obj.GroupVersionKind().Group, Kind: obj.GroupVersionKind().Kind}, obj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { clusters, err := s.db.GetProjectClusters(context.TODO(), project) if err != nil { @@ -2389,16 +2387,11 @@ func splitStatusPatch(patch []byte) ([]byte, []byte, error) { } func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.ApplicationSyncWindowsQuery) (*application.ApplicationSyncWindowsResponse, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, fmt.Errorf("error getting app project: %w", err) - } - windows := proj.Spec.SyncWindows.Matches(a) sync := windows.CanSync(true)
server/application/application_test.go+114 −4 modified@@ -1818,7 +1818,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) { appServer := newTestAppServer(t, testApp) active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) - assert.Contains(t, err.Error(), "not found") + assert.Contains(t, err.Error(), "not exist") assert.Nil(t, active) }) } @@ -2428,7 +2428,16 @@ func TestAppNamespaceRestrictions(t *testing.T) { t.Run("Get application in other namespace when allowed", func(t *testing.T) { testApp := newTestApp() testApp.Namespace = "argocd-1" - appServer := newTestAppServer(t, testApp) + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ Name: pointer.String("test-app"), @@ -2439,6 +2448,28 @@ func TestAppNamespaceRestrictions(t *testing.T) { require.Equal(t, "argocd-1", app.Namespace) require.Equal(t, "test-app", app.Name) }) + t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ + Name: pointer.String("test-app"), + AppNamespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.Nil(t, app) + require.ErrorContains(t, err, "app is not allowed in project") + }) t.Run("Create application in other namespace when allowed", func(t *testing.T) { testApp := newTestApp() testApp.Namespace = "argocd-1" @@ -2481,7 +2512,7 @@ func TestAppNamespaceRestrictions(t *testing.T) { }) require.Error(t, err) require.Nil(t, app) - require.ErrorContains(t, err, "not allowed to use project") + require.ErrorContains(t, err, "app is not allowed in project") }) t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) { @@ -2505,5 +2536,84 @@ func TestAppNamespaceRestrictions(t *testing.T) { require.Nil(t, app) require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted") }) - + t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) + assert.NoError(t, err) + assert.Equal(t, 0, len(active.ActiveWindows)) + }) + t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) + require.Error(t, err) + require.Nil(t, active) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ + Name: pointer.String("test-app"), + Namespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.Nil(t, links) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ + Name: pointer.String("test-app"), + Namespace: pointer.String("argocd-1"), + }) + require.NoError(t, err) + assert.Equal(t, 0, len(links.Items)) + }) }
util/argo/argo.go+1 −2 modified@@ -694,8 +694,7 @@ func GetAppProject(app *argoappv1.Application, projLister applicationsv1.AppProj return nil, err } if !proj.IsAppNamespacePermitted(app, ns) { - return nil, fmt.Errorf("application '%s' in namespace '%s' is not allowed to use project '%s'", - app.Name, app.Namespace, proj.Name) + return nil, argoappv1.NewErrApplicationNotAllowedToUseProject(app.Name, app.Namespace, proj.Name) } return proj, nil }
util/session/sessionmanager.go+1 −1 modified@@ -312,7 +312,7 @@ func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAtte return expiredCount } -// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache. +// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache. func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string { idx := rand.Intn(len(failures) - 1) i := 0
c5a252c4cc26Merge pull request from GHSA-2gvw-w6fj-7m3c
4 files changed · +364 −95
pkg/apis/application/v1alpha1/app_project_types.go+18 −0 modified@@ -17,6 +17,24 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +type ErrApplicationNotAllowedToUseProject struct { + application string + namespace string + project string +} + +func NewErrApplicationNotAllowedToUseProject(application, namespace, project string) error { + return &ErrApplicationNotAllowedToUseProject{ + application: application, + namespace: namespace, + project: project, + } +} + +func (err *ErrApplicationNotAllowedToUseProject) Error() string { + return fmt.Sprintf("application '%s' in namespace '%s' is not allowed to use project %s", err.application, err.namespace, err.project) +} + // AppProjectList is list of AppProject resources // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type AppProjectList struct {
server/application/application.go+92 −92 modified@@ -147,7 +147,7 @@ func NewServer( // // If the user does provide a "project," we can respond more specifically. If the user does not have access to the given // app name in the given project, we return "permission denied." If the app exists, but the project is different from -func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, error) { +func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, *appv1.AppProject, error) { logCtx := log.WithFields(map[string]interface{}{ "application": name, "namespace": namespace, @@ -164,23 +164,23 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa // but the app is in a different project" response. We don't want the user inferring the existence of the // app from response time. _, _ = getApp() - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } } a, err := getApp() if err != nil { if apierr.IsNotFound(err) { if project != "" { // We know that the user was allowed to get the Application, but the Application does not exist. Return 404. - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } // We don't know if the user was allowed to get the Application, and we don't want to leak information about // the Application's existence. Return 403. logCtx.Warn("application does not exist") - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } logCtx.Errorf("failed to get application: %s", err) - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } // Even if we performed an initial RBAC check (because the request was fully parameterized), we still need to // perform a second RBAC check to ensure that the user has access to the actual Application's project (not just the @@ -194,11 +194,11 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa // The user specified a project. We would have returned a 404 if the user had access to the app, but the app // did not exist. So we have to return a 404 when the app does exist, but the user does not have access. // Otherwise, they could infer that the app exists based on the error code. - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } // The user didn't specify a project. We always return permission denied for both lack of access and lack of // existence. - return nil, permissionDeniedErr + return nil, nil, permissionDeniedErr } effectiveProject := "default" if a.Spec.Project != "" { @@ -211,15 +211,20 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa }).Warnf("user tried to %s application in project %s, but the application is in project %s", action, project, effectiveProject) // The user has access to the app, but the app is in a different project. Return 404, meaning "app doesn't // exist in that project". - return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) + return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error()) } - return a, nil + // Get the app's associated project, and make sure all project restrictions are enforced. + proj, err := s.getAppProject(ctx, a, logCtx) + if err != nil { + return a, nil, err + } + return a, proj, nil } // getApplicationEnforceRBACInformer uses an informer to get an Application. If the app does not exist, permission is // denied, or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive // information. -func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, error) { +func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, *appv1.AppProject, error) { namespaceOrDefault := s.appNamespaceOrDefault(namespace) return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) { return s.appLister.Applications(namespaceOrDefault).Get(name) @@ -229,9 +234,12 @@ func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, // getApplicationEnforceRBACClient uses a client to get an Application. If the app does not exist, permission is denied, // or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive // information. -func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, error) { +func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, *appv1.AppProject, error) { namespaceOrDefault := s.appNamespaceOrDefault(namespace) return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) { + if !s.isNamespaceEnabled(namespaceOrDefault) { + return nil, security.NamespaceNotPermittedError(namespaceOrDefault) + } return s.appclientset.ArgoprojV1alpha1().Applications(namespaceOrDefault).Get(ctx, name, metav1.GetOptions{ ResourceVersion: resourceVersion, }) @@ -310,7 +318,13 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq if q.Validate != nil { validate = *q.Validate } - err := s.validateAndNormalizeApp(ctx, a, validate) + + proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name)) + if err != nil { + return nil, err + } + + err = s.validateAndNormalizeApp(ctx, a, proj, validate) if err != nil { return nil, fmt.Errorf("error while validating and normalizing app: %w", err) } @@ -366,7 +380,7 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq return updated, nil } -func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, action func( +func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, proj *appv1.AppProject, action func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, @@ -393,14 +407,6 @@ func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, acti if err != nil { return fmt.Errorf("error getting kustomize settings options: %w", err) } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project) - } - return fmt.Errorf("error getting application's project: %w", err) - } - helmRepos, err := s.db.ListHelmRepositories(ctx) if err != nil { return fmt.Errorf("error listing helm repositories: %w", err) @@ -434,7 +440,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan if q.Name == nil || *q.Name == "" { return nil, fmt.Errorf("invalid request: application name is missing") } - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -446,7 +452,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan } var manifestInfo *apiclient.ManifestResponse - err = s.queryRepoServer(ctx, a, func( + err = s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error { revision := source.TargetRevision if q.GetRevision() != "" { @@ -532,13 +538,13 @@ func (s *Server) GetManifestsWithFiles(stream application.ApplicationService_Get return fmt.Errorf("invalid request: application name is missing") } - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName()) if err != nil { return err } var manifestInfo *apiclient.ManifestResponse - err = s.queryRepoServer(ctx, a, func( + err = s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error { appInstanceLabelKey, err := s.settingsMgr.GetAppInstanceLabelKey() @@ -640,7 +646,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app // We must use a client Get instead of an informer Get, because it's common to call Get immediately // following a Watch (which is not yet powered by an informer), and the Get must reflect what was // previously seen by the client. - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion()) + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion()) if err != nil { return nil, err } @@ -671,7 +677,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app if refreshType == appv1.RefreshTypeHard { // force refresh cached application details - if err := s.queryRepoServer(ctx, a, func( + if err := s.queryRepoServer(ctx, a, proj, func( client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, @@ -723,7 +729,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app // ListResourceEvents returns a list of event resources func (s *Server) ListResourceEvents(ctx context.Context, q *application.ApplicationResourceEventsQuery) (*v1.EventList, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -791,12 +797,12 @@ func (s *Server) validateAndUpdateApp(ctx context.Context, newApp *appv1.Applica s.projectLock.RLock(newApp.Spec.GetProject()) defer s.projectLock.RUnlock(newApp.Spec.GetProject()) - app, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "") + app, proj, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "") if err != nil { return nil, err } - err = s.validateAndNormalizeApp(ctx, newApp, validate) + err = s.validateAndNormalizeApp(ctx, newApp, proj, validate) if err != nil { return nil, fmt.Errorf("error validating and normalizing app: %w", err) } @@ -908,7 +914,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat if q.GetSpec() == nil { return nil, fmt.Errorf("error updating application spec: spec is nil in request") } - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } @@ -927,7 +933,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat // Patch patches an application func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchRequest) (*appv1.Application, error) { - app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + app, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } @@ -970,11 +976,35 @@ func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchReque return s.validateAndUpdateApp(ctx, newApp, false, true, rbacpolicy.ActionUpdate, q.GetProject()) } +func (s *Server) getAppProject(ctx context.Context, a *appv1.Application, logCtx *log.Entry) (*appv1.AppProject, error) { + proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) + if err == nil { + return proj, nil + } + + // If there's a permission issue or the app doesn't exist, return a vague error to avoid letting the user enumerate project names. + vagueError := status.Errorf(codes.InvalidArgument, "app is not allowed in project %q, or the project does not exist", a.Spec.Project) + + if apierr.IsNotFound(err) { + return nil, vagueError + } + + if _, ok := err.(*appv1.ErrApplicationNotAllowedToUseProject); ok { + logCtx.WithFields(map[string]interface{}{ + "project": a.Spec.Project, + argocommon.SecurityField: argocommon.SecurityMedium, + }).Warnf("error getting app project: %s", err) + return nil, vagueError + } + + return nil, vagueError +} + // Delete removes an application and all associated resources func (s *Server) Delete(ctx context.Context, q *application.ApplicationDeleteRequest) (*application.ApplicationResponse, error) { appName := q.GetName() appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "") if err != nil { return nil, err } @@ -1129,16 +1159,7 @@ func (s *Server) Watch(q *application.ApplicationQuery, ws application.Applicati } } -func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, validate bool) error { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - // Offer no hint that the project does not exist. - log.Warnf("User attempted to create/update application in non-existent project %q", app.Spec.Project) - return permissionDeniedErr - } - return fmt.Errorf("error getting application's project: %w", err) - } +func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, validate bool) error { if app.GetName() == "" { return fmt.Errorf("resource name may not be empty") } @@ -1241,7 +1262,7 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap } func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, nil, nil, err } @@ -1378,7 +1399,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR } func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery) (*appv1.ApplicationTree, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return nil, err } @@ -1387,7 +1408,7 @@ func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery } func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application.ApplicationService_WatchResourceTreeServer) error { - _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + _, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return err } @@ -1404,7 +1425,7 @@ func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application } func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.RevisionMetadata, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -1414,12 +1435,6 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe if err != nil { return nil, fmt.Errorf("error getting repository by URL: %w", err) } - // We need to get some information with the project associated to the app, - // so we'll know whether GPG signatures are enforced. - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, fmt.Errorf("error getting app project: %w", err) - } conn, repoClient, err := s.repoClientset.NewRepoServerClient() if err != nil { return nil, fmt.Errorf("error creating repo server client: %w", err) @@ -1434,7 +1449,7 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe // RevisionChartDetails returns the helm chart metadata, as fetched from the reposerver func (s *Server) RevisionChartDetails(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.ChartDetails, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, err } @@ -1465,7 +1480,7 @@ func isMatchingResource(q *application.ResourcesQuery, key kube.ResourceKey) boo } func (s *Server) ManagedResources(ctx context.Context, q *application.ResourcesQuery) (*application.ManagedResourcesResponse, error) { - a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) + a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName()) if err != nil { return nil, err } @@ -1522,7 +1537,7 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application. } } - a, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) + a, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return err } @@ -1714,19 +1729,11 @@ func isTheSelectedOne(currentNode *appv1.ResourceNode, q *application.Applicatio // Sync syncs an application to its target state func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncRequest) (*appv1.Application, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "") if err != nil { return nil, err } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return a, status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project) - } - return a, fmt.Errorf("error getting app project: %w", err) - } - s.inferResourcesStatusHealth(a) if !proj.Spec.SyncWindows.Matches(a).CanSync(true) { @@ -1823,7 +1830,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR } func (s *Server) Rollback(ctx context.Context, rollbackReq *application.ApplicationRollbackRequest) (*appv1.Application, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "") if err != nil { return nil, err } @@ -1882,7 +1889,7 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *application.Applicat } func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksRequest) (*application.LinksResponse, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "") if err != nil { return nil, err } @@ -1897,7 +1904,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq return nil, fmt.Errorf("failed to read application deep links from configmap: %w", err) } - clstObj, _, err := s.getObjectsForDeepLinks(ctx, a) + clstObj, _, err := s.getObjectsForDeepLinks(ctx, a, proj) if err != nil { return nil, err } @@ -1912,12 +1919,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq return finalList, nil } -func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, nil, fmt.Errorf("error getting app project: %w", err) - } - +func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application, proj *appv1.AppProject) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) { // sanitize project jwt tokens proj.Status = appv1.AppProjectStatus{} @@ -1980,7 +1982,12 @@ func (s *Server) ListResourceLinks(ctx context.Context, req *application.Applica return nil, err } - clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app) + proj, err := s.getAppProject(ctx, app, log.WithField("application", app.GetName())) + if err != nil { + return nil, err + } + + clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app, proj) if err != nil { return nil, err } @@ -2036,7 +2043,7 @@ func (s *Server) resolveRevision(ctx context.Context, app *appv1.Application, sy func (s *Server) TerminateOperation(ctx context.Context, termOpReq *application.OperationTerminateRequest) (*application.OperationTerminateResponse, error) { appName := termOpReq.GetName() appNs := s.appNamespaceOrDefault(termOpReq.GetAppNamespace()) - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "") + a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "") if err != nil { return nil, err } @@ -2109,7 +2116,7 @@ func (s *Server) ListResourceActions(ctx context.Context, q *application.Applica func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacRequest string, q *application.ApplicationResourceRequest) (obj *unstructured.Unstructured, res *appv1.ResourceNode, app *appv1.Application, config *rest.Config, err error) { if q.GetKind() == applicationType.ApplicationKind && q.GetGroup() == applicationType.Group && q.GetName() == q.GetResourceName() { - app, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName()) + app, _, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName()) if err != nil { return nil, nil, nil, nil, err } @@ -2205,14 +2212,19 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA } } + proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name)) + if err != nil { + return nil, err + } + // First, make sure all the returned resources are permitted, for each operation. // Also perform create with dry-runs for all create-operation resources. // This is performed separately to reduce the risk of only some of the resources being successfully created later. // TODO: when apply/delete operations would be supported for custom actions, // the dry-run for relevant apply/delete operation would have to be invoked as well. for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj - err := s.verifyResourcePermitted(ctx, app, newObj) + err := s.verifyResourcePermitted(ctx, app, proj, newObj) if err != nil { return nil, err } @@ -2306,14 +2318,7 @@ func (s *Server) patchResource(ctx context.Context, config *rest.Config, liveObj return &application.ApplicationResponse{}, nil } -func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, obj *unstructured.Unstructured) error { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return fmt.Errorf("application references project %s which does not exist", app.Spec.Project) - } - return fmt.Errorf("failed to get project %s: %w", app.Spec.Project, err) - } +func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, obj *unstructured.Unstructured) error { permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: obj.GroupVersionKind().Group, Kind: obj.GroupVersionKind().Kind}, obj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { clusters, err := s.db.GetProjectClusters(context.TODO(), project) if err != nil { @@ -2373,16 +2378,11 @@ func splitStatusPatch(patch []byte) ([]byte, []byte, error) { } func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.ApplicationSyncWindowsQuery) (*application.ApplicationSyncWindowsResponse, error) { - a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") + a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "") if err != nil { return nil, err } - proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - return nil, fmt.Errorf("error getting app project: %w", err) - } - windows := proj.Spec.SyncWindows.Matches(a) sync := windows.CanSync(true)
server/application/application_test.go+253 −1 modified@@ -1817,7 +1817,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) { appServer := newTestAppServer(t, testApp) active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) - assert.Contains(t, err.Error(), "not found") + assert.Contains(t, err.Error(), "not exist") assert.Nil(t, active) }) } @@ -2364,3 +2364,255 @@ func TestIsApplicationPermitted(t *testing.T) { assert.True(t, permitted) }) } + +func TestAppNamespaceRestrictions(t *testing.T) { + t.Run("List applications in controller namespace", func(t *testing.T) { + testApp := newTestApp() + appServer := newTestAppServer(t, testApp) + apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{}) + require.NoError(t, err) + require.Len(t, apps.Items, 1) + }) + + t.Run("List applications with non-allowed apps existing", func(t *testing.T) { + testApp1 := newTestApp() + testApp1.Namespace = "argocd-1" + appServer := newTestAppServer(t, testApp1) + apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{}) + require.NoError(t, err) + require.Len(t, apps.Items, 0) + }) + + t.Run("List applications with non-allowed apps existing and explicit ns request", func(t *testing.T) { + testApp1 := newTestApp() + testApp2 := newTestApp() + testApp2.Namespace = "argocd-1" + appServer := newTestAppServer(t, testApp1, testApp2) + apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{AppNamespace: pointer.String("argocd-1")}) + require.NoError(t, err) + require.Len(t, apps.Items, 0) + }) + + t.Run("List applications with allowed apps in other namespaces", func(t *testing.T) { + testApp1 := newTestApp() + testApp1.Namespace = "argocd-1" + appServer := newTestAppServer(t, testApp1) + appServer.enabledNamespaces = []string{"argocd-1"} + apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{}) + require.NoError(t, err) + require.Len(t, apps.Items, 1) + }) + + t.Run("Get application in control plane namespace", func(t *testing.T) { + testApp := newTestApp() + appServer := newTestAppServer(t, testApp) + app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ + Name: pointer.String("test-app"), + }) + require.NoError(t, err) + assert.Equal(t, "test-app", app.GetName()) + }) + t.Run("Get application in other namespace when forbidden", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + appServer := newTestAppServer(t, testApp) + app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ + Name: pointer.String("test-app"), + AppNamespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.ErrorContains(t, err, "permission denied") + require.Nil(t, app) + }) + t.Run("Get application in other namespace when allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ + Name: pointer.String("test-app"), + AppNamespace: pointer.String("argocd-1"), + }) + require.NoError(t, err) + require.NotNil(t, app) + require.Equal(t, "argocd-1", app.Namespace) + require.Equal(t, "test-app", app.Name) + }) + t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ + Name: pointer.String("test-app"), + AppNamespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.Nil(t, app) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Create application in other namespace when allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + app, err := appServer.Create(context.TODO(), &application.ApplicationCreateRequest{ + Application: testApp, + }) + require.NoError(t, err) + require.NotNil(t, app) + assert.Equal(t, "test-app", app.Name) + assert.Equal(t, "argocd-1", app.Namespace) + }) + + t.Run("Create application in other namespace when not allowed by project", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{}, + }, + } + appServer := newTestAppServer(t, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + app, err := appServer.Create(context.TODO(), &application.ApplicationCreateRequest{ + Application: testApp, + }) + require.Error(t, err) + require.Nil(t, app) + require.ErrorContains(t, err, "app is not allowed in project") + }) + + t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-2"} + app, err := appServer.Create(context.TODO(), &application.ApplicationCreateRequest{ + Application: testApp, + }) + require.Error(t, err) + require.Nil(t, app) + require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted") + }) + t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) + assert.NoError(t, err) + assert.Equal(t, 0, len(active.ActiveWindows)) + }) + t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) + require.Error(t, err) + require.Nil(t, active) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-2"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ + Name: pointer.String("test-app"), + Namespace: pointer.String("argocd-1"), + }) + require.Error(t, err) + require.Nil(t, links) + require.ErrorContains(t, err, "app is not allowed in project") + }) + t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) { + testApp := newTestApp() + testApp.Namespace = "argocd-1" + testApp.Spec.Project = "other-ns" + otherNsProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + SourceNamespaces: []string{"argocd-1"}, + }, + } + appServer := newTestAppServer(t, testApp, otherNsProj) + appServer.enabledNamespaces = []string{"argocd-1"} + links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ + Name: pointer.String("test-app"), + Namespace: pointer.String("argocd-1"), + }) + require.NoError(t, err) + assert.Equal(t, 0, len(links.Items)) + }) +}
util/argo/argo.go+1 −2 modified@@ -689,8 +689,7 @@ func GetAppProject(app *argoappv1.Application, projLister applicationsv1.AppProj return nil, err } if !proj.IsAppNamespacePermitted(app, ns) { - return nil, fmt.Errorf("application '%s' in namespace '%s' is not allowed to use project '%s'", - app.Name, app.Namespace, proj.Name) + return nil, argoappv1.NewErrApplicationNotAllowedToUseProject(app.Name, app.Namespace, proj.Name) } return proj, nil }
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
6- github.com/advisories/GHSA-2gvw-w6fj-7m3cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-31990ghsaADVISORY
- github.com/argoproj/argo-cd/commit/c514105af739eebedb9dbe89d8a6dd8dfc30bb2cghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/c5a252c4cc260e240e2074794aedb861d07e9ca5ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/e0ff56d89fbd7d066e9c862b30337f6520f13f17ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-2gvw-w6fj-7m3cghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.