diff --git a/server/application/application.go b/server/application/application.go index bead41916a41b..dc7c1dc83b909 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -67,7 +67,8 @@ const ( ) var ( - watchAPIBufferSize = env.ParseNumFromEnv(argocommon.EnvWatchAPIBufferSize, 1000, 0, math.MaxInt32) + watchAPIBufferSize = env.ParseNumFromEnv(argocommon.EnvWatchAPIBufferSize, 1000, 0, math.MaxInt32) + permissionDeniedErr = status.Error(codes.PermissionDenied, "permission denied") ) // Server provides an Application service @@ -77,7 +78,7 @@ type Server struct { appclientset appclientset.Interface appLister applisters.ApplicationNamespaceLister appInformer cache.SharedIndexInformer - appBroadcaster *broadcasterHandler + appBroadcaster Broadcaster repoClientset apiclient.Clientset kubectl kube.Kubectl db db.ArgoDB @@ -96,6 +97,7 @@ func NewServer( appclientset appclientset.Interface, appLister applisters.ApplicationNamespaceLister, appInformer cache.SharedIndexInformer, + appBroadcaster Broadcaster, repoClientset apiclient.Clientset, cache *servercache.Cache, kubectl kube.Kubectl, @@ -105,7 +107,9 @@ func NewServer( settingsMgr *settings.SettingsManager, projInformer cache.SharedIndexInformer, ) (application.ApplicationServiceServer, AppResourceTreeFn) { - appBroadcaster := &broadcasterHandler{} + if appBroadcaster == nil { + appBroadcaster = &broadcasterHandler{} + } appInformer.AddEventHandler(appBroadcaster) s := &Server{ ns: namespace, @@ -127,6 +131,57 @@ func NewServer( return s, s.GetAppResources } +// getAppEnforceRBAC gets the Application with the given name in the given namespace. If no namespace is +// specified, the Application is fetched from the default namespace (the one in which the API server is running). +// +// If the Application does not exist, then we have no way of determining if the user would have had access to get that +// Application. Verifying access requires knowing the Application's name, namespace, and project. The user may specify, +// at minimum, the Application name. +// +// So to prevent a malicious user from inferring the existence or absense of the Application or namespace, we respond +// "permission denied" if the Application does not exist. +func (s *Server) getAppEnforceRBAC(ctx context.Context, action, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, error) { + logCtx := log.WithFields(map[string]interface{}{ + "application": name, + }) + a, err := getApp() + if err != nil { + if apierr.IsNotFound(err) { + logCtx.Warn("application does not exist") + return nil, permissionDeniedErr + } + logCtx.Errorf("failed to get application: %s", err) + return nil, permissionDeniedErr + } + if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, action, apputil.AppRBACName(*a)); err != nil { + logCtx.WithFields(map[string]interface{}{ + "project": a.Spec.Project, + }).Warnf("user tried to %s application which they do not have access to: %s", action, err) + return nil, permissionDeniedErr + } + return a, 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, name string) (*appv1.Application, error) { + return s.getAppEnforceRBAC(ctx, action, name, func() (*appv1.Application, error) { + return s.appLister.Get(name) + }) +} + +// 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, name, resourceVersion string) (*appv1.Application, error) { + return s.getAppEnforceRBAC(ctx, action, name, func() (*appv1.Application, error) { + return s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, name, metav1.GetOptions{ + ResourceVersion: resourceVersion, + }) + }) +} + // List returns list of applications func (s *Server) List(ctx context.Context, q *application.ApplicationQuery) (*appv1.ApplicationList, error) { labelsMap, err := labels.ConvertSelectorToLabelsMap(q.GetSelector()) @@ -291,11 +346,11 @@ func (s *Server) queryRepoServer(ctx context.Context, a *v1alpha1.Application, a // GetManifests returns application manifests func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationManifestQuery) (*apiclient.ManifestResponse, error) { - a, err := s.appLister.Get(*q.Name) - if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) + if q.Name == nil || *q.Name == "" { + return nil, fmt.Errorf("invalid request: application name is missing") } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { + a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetName()) + if err != nil { return nil, err } @@ -381,17 +436,13 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan // Get returns an application by name func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*appv1.Application, error) { + appName := q.GetName() + // 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.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, q.GetName(), metav1.GetOptions{ - ResourceVersion: q.GetResourceVersion(), - }) - + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, appName, q.GetResourceVersion()) if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return nil, err } if q.Refresh == nil { @@ -469,11 +520,8 @@ 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.appLister.Get(*q.Name) + a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetName()) if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return nil, err } var ( @@ -533,13 +581,13 @@ func (s *Server) ListResourceEvents(ctx context.Context, q *application.Applicat return list, nil } -func (s *Server) validateAndUpdateApp(ctx context.Context, newApp *appv1.Application, merge bool, validate bool) (*appv1.Application, error) { +func (s *Server) validateAndUpdateApp(ctx context.Context, newApp *appv1.Application, merge bool, validate bool, action string) (*appv1.Application, error) { s.projectLock.RLock(newApp.Spec.GetProject()) defer s.projectLock.RUnlock(newApp.Spec.GetProject()) - app, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, newApp.Name, metav1.GetOptions{}) + app, err := s.getApplicationEnforceRBACClient(ctx, action, newApp.Name, "") if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) + return nil, err } err = s.validateAndNormalizeApp(ctx, newApp, validate) @@ -641,7 +689,7 @@ func (s *Server) Update(ctx context.Context, q *application.ApplicationUpdateReq if q.Validate != nil { validate = *q.Validate } - return s.validateAndUpdateApp(ctx, q.Application, false, validate) + return s.validateAndUpdateApp(ctx, q.Application, false, validate, rbacpolicy.ActionUpdate) } // UpdateSpec updates an application spec and filters out any invalid parameter overrides @@ -649,11 +697,8 @@ 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.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, *q.Name, metav1.GetOptions{}) + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetName(), "") if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionUpdate, apputil.AppRBACName(*a)); err != nil { return nil, err } a.Spec = *q.GetSpec() @@ -661,7 +706,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat if q.Validate != nil { validate = *q.Validate } - a, err = s.validateAndUpdateApp(ctx, a, false, validate) + a, err = s.validateAndUpdateApp(ctx, a, false, validate, rbacpolicy.ActionUpdate) if err != nil { return nil, fmt.Errorf("error validating and updating app: %w", err) } @@ -670,10 +715,9 @@ 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.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, *q.Name, metav1.GetOptions{}) + app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetName(), "") if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) + return nil, err } if err = s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionUpdate, apputil.AppRBACName(*app)); err != nil { @@ -711,14 +755,15 @@ func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchReque if err != nil { return nil, fmt.Errorf("error unmarshaling patched app: %w", err) } - return s.validateAndUpdateApp(ctx, newApp, false, true) + return s.validateAndUpdateApp(ctx, newApp, false, true, rbacpolicy.ActionUpdate) } // Delete removes an application and all associated resources func (s *Server) Delete(ctx context.Context, q *application.ApplicationDeleteRequest) (*application.ApplicationResponse, error) { - a, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, *q.Name, metav1.GetOptions{}) + appName := q.GetName() + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, appName, "") if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) + return nil, err } s.projectLock.RLock(a.Spec.Project) @@ -859,7 +904,9 @@ func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Applica proj, err := argo.GetAppProject(&app.Spec, 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", app.Spec.Project) + // 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) } @@ -966,20 +1013,16 @@ func (s *Server) GetAppResources(ctx context.Context, a *appv1.Application) (*ap return s.cache.GetAppResourcesTree(a.Name, &tree) }) if err != nil { - return &tree, fmt.Errorf("error getting cached app state: %w", err) + return &tree, fmt.Errorf("error getting cached app resource tree: %w", err) } return &tree, nil } func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) { - a, err := s.appLister.Get(*q.Name) + a, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetName()) if err != nil { - return nil, nil, nil, fmt.Errorf("error getting app by name: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, action, apputil.AppRBACName(*a)); err != nil { return nil, nil, nil, err } - tree, err := s.GetAppResources(ctx, a) if err != nil { return nil, nil, nil, fmt.Errorf("error getting app resources: %w", err) @@ -999,7 +1042,7 @@ func (s *Server) getAppLiveResource(ctx context.Context, action string, q *appli func (s *Server) GetResource(ctx context.Context, q *application.ApplicationResourceRequest) (*application.ApplicationResourceResponse, error) { res, config, _, err := s.getAppLiveResource(ctx, rbacpolicy.ActionGet, q) if err != nil { - return nil, fmt.Errorf("error getting app live resource: %w", err) + return nil, err } // make sure to use specified resource version if provided @@ -1045,9 +1088,6 @@ func (s *Server) PatchResource(ctx context.Context, q *application.ApplicationRe } res, config, a, err := s.getAppLiveResource(ctx, rbacpolicy.ActionUpdate, resourceRequest) if err != nil { - return nil, fmt.Errorf("error getting app live resource: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionUpdate, apputil.AppRBACName(*a)); err != nil { return nil, err } @@ -1059,6 +1099,9 @@ func (s *Server) PatchResource(ctx context.Context, q *application.ApplicationRe } return nil, fmt.Errorf("error patching resource: %w", err) } + if manifest == nil { + return nil, fmt.Errorf("failed to patch resource: manifest was nil") + } manifest, err = replaceSecretValues(manifest) if err != nil { return nil, fmt.Errorf("error replacing secret values: %w", err) @@ -1086,9 +1129,6 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR } res, config, a, err := s.getAppLiveResource(ctx, rbacpolicy.ActionDelete, resourceRequest) if err != nil { - return nil, fmt.Errorf("error getting live resource for delete: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionDelete, apputil.AppRBACName(*a)); err != nil { return nil, err } var deleteOption metav1.DeleteOptions @@ -1112,23 +1152,16 @@ 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.appLister.Get(q.GetApplicationName()) + a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetApplicationName()) if err != nil { - return nil, fmt.Errorf("error getting application by name: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return nil, err } return s.GetAppResources(ctx, a) } func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application.ApplicationService_WatchResourceTreeServer) error { - a, err := s.appLister.Get(q.GetApplicationName()) + _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetApplicationName()) if err != nil { - return fmt.Errorf("error getting application by name: %w", err) - } - - if err := s.enf.EnforceErr(ws.Context().Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return err } @@ -1143,11 +1176,8 @@ func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application } func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMetadataQuery) (*v1alpha1.RevisionMetadata, error) { - a, err := s.appLister.Get(q.GetName()) + a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetName()) if err != nil { - return nil, fmt.Errorf("error getting app by name: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return nil, err } repo, err := s.db.GetRepository(ctx, a.Spec.Source.RepoURL) @@ -1180,19 +1210,16 @@ 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.appLister.Get(*q.ApplicationName) + a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetApplicationName()) if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { - return nil, fmt.Errorf("error verifying rbac: %w", err) + return nil, err } items := make([]*appv1.ResourceDiff, 0) err = s.getCachedAppState(ctx, a, func() error { return s.cache.GetAppManagedResources(a.Name, &items) }) if err != nil { - return nil, fmt.Errorf("error getting cached app state: %w", err) + return nil, fmt.Errorf("error getting cached app managed resources: %w", err) } res := &application.ManagedResourcesResponse{} for i := range items { @@ -1239,12 +1266,8 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application. } } - a, err := s.appLister.Get(q.GetName()) + a, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetName()) if err != nil { - return fmt.Errorf("error getting application by name: %w", err) - } - - if err := s.enf.EnforceErr(ws.Context().Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return err } @@ -1436,10 +1459,9 @@ 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) { - appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns) - a, err := appIf.Get(ctx, *syncReq.Name, metav1.GetOptions{}) + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetName(), "") if err != nil { - return nil, fmt.Errorf("error getting application by name: %w", err) + return nil, err } proj, err := argo.GetAppProject(&a.Spec, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), a.Namespace, s.settingsMgr, s.db, ctx) @@ -1521,7 +1543,9 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR op.Retry = *retry } - a, err = argo.SetAppOperation(appIf, *syncReq.Name, &op) + appName := syncReq.GetName() + appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns) + a, err = argo.SetAppOperation(appIf, appName, &op) if err != nil { return nil, fmt.Errorf("error setting app operation: %w", err) } @@ -1538,12 +1562,8 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR } func (s *Server) Rollback(ctx context.Context, rollbackReq *application.ApplicationRollbackRequest) (*appv1.Application, error) { - appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns) - a, err := appIf.Get(ctx, *rollbackReq.Name, metav1.GetOptions{}) + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetName(), "") if err != nil { - return nil, fmt.Errorf("error getting application by name: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionSync, apputil.AppRBACName(*a)); err != nil { return nil, err } if a.DeletionTimestamp != nil { @@ -1585,7 +1605,9 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *application.Applicat Source: &deploymentInfo.Source, }, } - a, err = argo.SetAppOperation(appIf, *rollbackReq.Name, &op) + appName := rollbackReq.GetName() + appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns) + a, err = argo.SetAppOperation(appIf, appName, &op) if err != nil { return nil, fmt.Errorf("error setting app operation: %w", err) } @@ -1632,11 +1654,9 @@ func (s *Server) resolveRevision(ctx context.Context, app *appv1.Application, sy } func (s *Server) TerminateOperation(ctx context.Context, termOpReq *application.OperationTerminateRequest) (*application.OperationTerminateResponse, error) { - a, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(ctx, *termOpReq.Name, metav1.GetOptions{}) + appName := termOpReq.GetName() + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, appName, "") if err != nil { - return nil, fmt.Errorf("error getting application by name: %w", err) - } - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionSync, apputil.AppRBACName(*a)); err != nil { return nil, err } @@ -1687,7 +1707,7 @@ func (s *Server) logResourceEvent(res *appv1.ResourceNode, ctx context.Context, func (s *Server) ListResourceActions(ctx context.Context, q *application.ApplicationResourceRequest) (*application.ResourceActionsListResponse, error) { res, config, _, err := s.getAppLiveResource(ctx, rbacpolicy.ActionGet, q) if err != nil { - return nil, fmt.Errorf("error getting app live resource: %w", err) + return nil, err } obj, err := s.kubectl.GetResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace) if err != nil { @@ -1742,7 +1762,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA actionRequest := fmt.Sprintf("%s/%s/%s/%s", rbacpolicy.ActionAction, q.GetGroup(), q.GetKind(), q.GetAction()) res, config, a, err := s.getAppLiveResource(ctx, actionRequest, resourceRequest) if err != nil { - return nil, fmt.Errorf("error getting app live resource: %w", err) + return nil, err } liveObj, err := s.kubectl.GetResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace) if err != nil { @@ -1869,13 +1889,8 @@ func (s *Server) plugins() ([]*v1alpha1.ConfigManagementPlugin, error) { } func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.ApplicationSyncWindowsQuery) (*application.ApplicationSyncWindowsResponse, error) { - appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns) - a, err := appIf.Get(ctx, *q.Name, metav1.GetOptions{}) + a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetName(), "") if err != nil { - return nil, fmt.Errorf("error getting application by name: %w", err) - } - - if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, apputil.AppRBACName(*a)); err != nil { return nil, err } diff --git a/server/application/application_test.go b/server/application/application_test.go index ef694d566a581..a37c3d1c892fb 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -4,11 +4,13 @@ import ( "context" coreerrors "errors" "fmt" + "strconv" "sync/atomic" "testing" "time" synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" + "github.com/argoproj/gitops-engine/pkg/utils/kube" "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" "github.com/argoproj/pkg/sync" "github.com/ghodss/yaml" @@ -17,13 +19,17 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + k8sappsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" kubetesting "k8s.io/client-go/testing" k8scache "k8s.io/client-go/tools/cache" "k8s.io/utils/pointer" @@ -35,10 +41,13 @@ import ( appinformer "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks" + appmocks "github.com/argoproj/argo-cd/v2/server/application/mocks" + servercache "github.com/argoproj/argo-cd/v2/server/cache" "github.com/argoproj/argo-cd/v2/server/rbacpolicy" "github.com/argoproj/argo-cd/v2/test" "github.com/argoproj/argo-cd/v2/util/assets" "github.com/argoproj/argo-cd/v2/util/cache" + "github.com/argoproj/argo-cd/v2/util/cache/appstate" "github.com/argoproj/argo-cd/v2/util/db" "github.com/argoproj/argo-cd/v2/util/errors" "github.com/argoproj/argo-cd/v2/util/grpc" @@ -93,6 +102,7 @@ func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient { mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil) mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil) mockRepoServiceClient.On("TestRepository", mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil) + mockRepoServiceClient.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&appsv1.RevisionMetadata{}, nil) if isHelm { mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevesionResponseHelm(), nil) @@ -104,15 +114,15 @@ func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient { } // return an ApplicationServiceServer which returns fake data -func newTestAppServer(objects ...runtime.Object) *Server { +func newTestAppServer(t *testing.T, objects ...runtime.Object) *Server { f := func(enf *rbac.Enforcer) { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:admin") } - return newTestAppServerWithEnforcerConfigure(f, objects...) + return newTestAppServerWithEnforcerConfigure(f, t, objects...) } -func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), objects ...runtime.Object) *Server { +func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), t *testing.T, objects ...runtime.Object) *Server { kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNamespace, @@ -197,15 +207,83 @@ func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), objects ...ru panic("Timed out waiting for caches to sync") } + broadcaster := new(appmocks.Broadcaster) + broadcaster.On("Subscribe", mock.Anything, mock.Anything).Return(func() {}).Run(func(args mock.Arguments) { + // Simulate the broadcaster notifying the subscriber of an application update. + // The second parameter to Subscribe is filters. For the purposes of tests, we ignore the filters. Future tests + // might require implementing those. + go func() { + events := args.Get(0).(chan *appsv1.ApplicationWatchEvent) + for _, obj := range objects { + app, ok := obj.(*appsv1.Application) + if ok { + oldVersion, err := strconv.Atoi(app.ResourceVersion) + if err != nil { + oldVersion = 0 + } + clonedApp := app.DeepCopy() + clonedApp.ResourceVersion = fmt.Sprintf("%d", oldVersion+1) + events <- &appsv1.ApplicationWatchEvent{Type: watch.Added, Application: *clonedApp} + } + } + }() + }) + broadcaster.On("OnAdd", mock.Anything).Return() + broadcaster.On("OnUpdate", mock.Anything, mock.Anything).Return() + broadcaster.On("OnDelete", mock.Anything).Return() + + appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) + // pre-populate the app cache + for _, obj := range objects { + app, ok := obj.(*appsv1.Application) + if ok { + err := appStateCache.SetAppManagedResources(app.Name, []*appsv1.ResourceDiff{}) + require.NoError(t, err) + + // Pre-populate the resource tree based on the app's resources. + nodes := make([]appsv1.ResourceNode, len(app.Status.Resources)) + for i, res := range app.Status.Resources { + nodes[i] = appsv1.ResourceNode{ + ResourceRef: appsv1.ResourceRef{ + Group: res.Group, + Kind: res.Kind, + Version: res.Version, + Name: res.Name, + Namespace: res.Namespace, + UID: "fake", + }, + } + } + err = appStateCache.SetAppResourcesTree(app.Name, &appsv1.ApplicationTree{ + Nodes: nodes, + }) + require.NoError(t, err) + } + } + appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour, time.Hour) + + kubectl := &kubetest.MockKubectlCmd{} + kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { + for _, obj := range objects { + if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { + if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { + return obj, nil + } + } + } + return nil, nil + }) + server, _ := NewServer( testNamespace, kubeclientset, fakeAppsClientset, factory.Argoproj().V1alpha1().Applications().Lister().Applications(testNamespace), appInformer, + broadcaster, mockRepoClient, - nil, - &kubetest.MockKubectlCmd{}, + appCache, + kubectl, db, enforcer, sync.NewKeyLock(), @@ -295,8 +373,414 @@ func createTestApp(testApp string, opts ...func(app *appsv1.Application)) *appsv return &app } +type TestResourceTreeServer struct { + ctx context.Context +} + +func (t *TestResourceTreeServer) Send(tree *appsv1.ApplicationTree) error { + return nil +} + +func (t *TestResourceTreeServer) SetHeader(metadata.MD) error { + return nil +} + +func (t *TestResourceTreeServer) SendHeader(metadata.MD) error { + return nil +} + +func (t *TestResourceTreeServer) SetTrailer(metadata.MD) { + return +} + +func (t *TestResourceTreeServer) Context() context.Context { + return t.ctx +} + +func (t *TestResourceTreeServer) SendMsg(m interface{}) error { + return nil +} + +func (t *TestResourceTreeServer) RecvMsg(m interface{}) error { + return nil +} + +type TestPodLogsServer struct { + ctx context.Context +} + +func (t *TestPodLogsServer) Send(log *application.LogEntry) error { + return nil +} + +func (t *TestPodLogsServer) SetHeader(metadata.MD) error { + return nil +} + +func (t *TestPodLogsServer) SendHeader(metadata.MD) error { + return nil +} + +func (t *TestPodLogsServer) SetTrailer(metadata.MD) { + return +} + +func (t *TestPodLogsServer) Context() context.Context { + return t.ctx +} + +func (t *TestPodLogsServer) SendMsg(m interface{}) error { + return nil +} + +func (t *TestPodLogsServer) RecvMsg(m interface{}) error { + return nil +} + +func TestNoAppEnumeration(t *testing.T) { + // This test ensures that malicious users can't infer the existence or non-existence of Applications by inspecting + // error messages. The errors for "app does not exist" must be the same as errors for "you aren't allowed to + // interact with this app." + + // These tests are only important on API calls where the full app RBAC name (project, namespace, and name) is _not_ + // known based on the query parameters. For example, the Create call cannot leak existence of Applications, because + // the Application's project, namespace, and name are all specified in the API call. The call can be rejected + // immediately if the user does not have access. But the Delete endpoint may be called with just the Application + // name. So we cannot return a different error message for "does not exist" and "you don't have delete permissions," + // because the user could infer that the Application exists if they do not get the "does not exist" message. For + // endpoints that do not require the full RBAC name, we must return a generic "permission denied" for both "does not + // exist" and "no access." + + f := func(enf *rbac.Enforcer) { + _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) + enf.SetDefaultRole("role:none") + } + deployment := k8sappsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + testApp := newTestApp(func(app *appsv1.Application) { + app.Name = "test" + app.Status.Resources = []appsv1.ResourceStatus{ + { + Group: deployment.GroupVersionKind().Group, + Kind: deployment.GroupVersionKind().Kind, + Version: deployment.GroupVersionKind().Version, + Name: deployment.Name, + Namespace: deployment.Namespace, + Status: "Synced", + }, + } + app.Status.History = []appsv1.RevisionHistory{ + { + ID: 0, + Source: appsv1.ApplicationSource{ + TargetRevision: "something-old", + }, + }, + } + }) + testDeployment := kube.MustToUnstructured(&deployment) + appServer := newTestAppServerWithEnforcerConfigure(f, t, testApp, testDeployment) + + noRoleCtx := context.Background() + adminCtx := context.WithValue(noRoleCtx, "claims", &jwt.MapClaims{"groups": []string{"admin"}}) + + t.Run("Get", func(t *testing.T) { + _, err := appServer.Get(adminCtx, &application.ApplicationQuery{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.Get(noRoleCtx, &application.ApplicationQuery{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("GetManifests", func(t *testing.T) { + _, err := appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.GetManifests(noRoleCtx, &application.ApplicationManifestQuery{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("ListResourceEvents", func(t *testing.T) { + _, err := appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.ListResourceEvents(noRoleCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("UpdateSpec", func(t *testing.T) { + _, err := appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Source: appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, + }}) + assert.NoError(t, err) + _, err = appServer.UpdateSpec(noRoleCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Source: appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, + }}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("doest-not-exist"), Spec: &appsv1.ApplicationSpec{ + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Source: appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, + }}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("Patch", func(t *testing.T) { + _, err := appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) + assert.NoError(t, err) + _, err = appServer.Patch(noRoleCtx, &application.ApplicationPatchRequest{Name: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("GetResource", func(t *testing.T) { + _, err := appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.GetResource(noRoleCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("doest-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("PatchResource", func(t *testing.T) { + _, err := appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) + // This will always throw an error, because the kubectl mock for PatchResource is hard-coded to return nil. + // The best we can do is to confirm we get past the permission check. + assert.NotEqual(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.PatchResource(noRoleCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("doest-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("DeleteResource", func(t *testing.T) { + _, err := appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.DeleteResource(noRoleCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("doest-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("ResourceTree", func(t *testing.T) { + _, err := appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.ResourceTree(noRoleCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("RevisionMetadata", func(t *testing.T) { + _, err := appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.RevisionMetadata(noRoleCtx, &application.RevisionMetadataQuery{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("ManagedResources", func(t *testing.T) { + _, err := appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.ManagedResources(noRoleCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("Sync", func(t *testing.T) { + _, err := appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.Sync(noRoleCtx, &application.ApplicationSyncRequest{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("TerminateOperation", func(t *testing.T) { + // The sync operation is already started from the previous test. We just need to set the field that the + // controller would set if this were an actual Argo CD environment. + setSyncRunningOperationState(t, appServer) + _, err := appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.TerminateOperation(noRoleCtx, &application.OperationTerminateRequest{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("Rollback", func(t *testing.T) { + unsetSyncRunningOperationState(t, appServer) + _, err := appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.Rollback(noRoleCtx, &application.ApplicationRollbackRequest{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("ListResourceActions", func(t *testing.T) { + _, err := appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Group: pointer.String("argoproj.io"), Kind: pointer.String("Application"), Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("RunResourceAction", func(t *testing.T) { + _, err := appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Action: pointer.String("restart")}) + assert.NoError(t, err) + _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Group: pointer.String("argoproj.io"), Kind: pointer.String("Application"), Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("GetApplicationSyncWindows", func(t *testing.T) { + _, err := appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.GetApplicationSyncWindows(noRoleCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("WatchResourceTree", func(t *testing.T) { + err := appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("test")}, &TestResourceTreeServer{ctx: adminCtx}) + assert.NoError(t, err) + err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("test")}, &TestResourceTreeServer{ctx: noRoleCtx}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("does-not-exist")}, &TestResourceTreeServer{ctx: adminCtx}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + t.Run("PodLogs", func(t *testing.T) { + err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) + assert.NoError(t, err) + err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: noRoleCtx}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("does-not-exist")}, &TestPodLogsServer{ctx: adminCtx}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) + + // Do this last so other stuff doesn't fail. + t.Run("Delete", func(t *testing.T) { + _, err := appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: pointer.String("test")}) + assert.NoError(t, err) + _, err = appServer.Delete(noRoleCtx, &application.ApplicationDeleteRequest{Name: pointer.String("test")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: pointer.String("doest-not-exist")}) + assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") + }) +} + +// setSyncRunningOperationState simulates starting a sync operation on the given app. +func setSyncRunningOperationState(t *testing.T, appServer *Server) { + appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") + app, err := appIf.Get(context.Background(), "test", metav1.GetOptions{}) + require.NoError(t, err) + // This sets the status that would be set by the controller usually. + app.Status.OperationState = &appsv1.OperationState{Phase: synccommon.OperationRunning, Operation: appsv1.Operation{Sync: &appsv1.SyncOperation{}}} + _, err = appIf.Update(context.Background(), app, metav1.UpdateOptions{}) + require.NoError(t, err) +} + +// unsetSyncRunningOperationState simulates finishing a sync operation on the given app. +func unsetSyncRunningOperationState(t *testing.T, appServer *Server) { + appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") + app, err := appIf.Get(context.Background(), "test", metav1.GetOptions{}) + require.NoError(t, err) + app.Operation = nil + app.Status.OperationState = nil + _, err = appIf.Update(context.Background(), app, metav1.UpdateOptions{}) + require.NoError(t, err) +} + +func testListAppsWithLabels(t *testing.T, appQuery application.ApplicationQuery, appServer *Server) { + validTests := []struct { + testName string + label string + expectedResult []string + }{ + {testName: "Equality based filtering using '=' operator", + label: "key1=value1", + expectedResult: []string{"App1"}}, + {testName: "Equality based filtering using '==' operator", + label: "key1==value1", + expectedResult: []string{"App1"}}, + {testName: "Equality based filtering using '!=' operator", + label: "key1!=value1", + expectedResult: []string{"App2", "App3"}}, + {testName: "Set based filtering using 'in' operator", + label: "key1 in (value1, value3)", + expectedResult: []string{"App1", "App3"}}, + {testName: "Set based filtering using 'notin' operator", + label: "key1 notin (value1, value3)", + expectedResult: []string{"App2"}}, + {testName: "Set based filtering using 'exists' operator", + label: "key1", + expectedResult: []string{"App1", "App2", "App3"}}, + {testName: "Set based filtering using 'not exists' operator", + label: "!key2", + expectedResult: []string{"App2", "App3"}}, + } + //test valid scenarios + for _, validTest := range validTests { + t.Run(validTest.testName, func(t *testing.T) { + appQuery.Selector = &validTest.label + res, err := appServer.List(context.Background(), &appQuery) + assert.NoError(t, err) + apps := []string{} + for i := range res.Items { + apps = append(apps, res.Items[i].Name) + } + assert.Equal(t, validTest.expectedResult, apps) + }) + } + + invalidTests := []struct { + testName string + label string + errorMesage string + }{ + {testName: "Set based filtering using '>' operator", + label: "key1>value1", + errorMesage: "error parsing the selector"}, + {testName: "Set based filtering using '<' operator", + label: "key1