Compare commits

...

7 Commits

Author SHA1 Message Date
Justin Marquis
eef1ddfd8b chore: disable codeql workflow on cherry-pick branches (#12893)
Signed-off-by: Justin Marquis <34fathombelow@protonmail.com>
2023-04-04 16:45:34 -04:00
gcp-cherry-pick-bot[bot]
38e5b676c9 docs: fix broken version selector (#13102) (#13108)
Signed-off-by: Harold Cheng <niuchangcun@gmail.com>
Co-authored-by: cjc7373 <niuchangcun@gmail.com>
2023-04-04 16:22:42 -04:00
Nobuo Takizawa
bea956674d chore: Bump dex from v2.35.3 to v2.36.0 (#12933)
Signed-off-by: nobuyo <longzechangsheng@gmail.com>
2023-03-24 09:57:05 -04:00
Michael Crenshaw
ed0682d2b5 chore: fix lint (#12972)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2023-03-23 13:29:28 -04:00
argo-bot
598f79236a Bump version to 2.4.28 2023-03-23 14:44:58 +00:00
argo-bot
bdc043cadc Bump version to 2.4.28 2023-03-23 14:44:53 +00:00
Michael Crenshaw
63f9622b00 Merge pull request from GHSA-2q5c-qw9c-fmvq
* fix: prevent app enumeration

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* more tests, fix incorrect param use

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

similar requests

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix merge issue

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix CLI to understand permission denied is not a fatal error

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix test to expect permission denied instead of validation error

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* upgrade notes

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* remove duplicate test

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

---------

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2023-03-23 09:22:05 -04:00
20 changed files with 783 additions and 217 deletions

View File

@@ -432,7 +432,7 @@ jobs:
git config --global user.email "john.doe@example.com"
- name: Pull Docker image required for tests
run: |
docker pull ghcr.io/dexidp/dex:v2.35.3
docker pull ghcr.io/dexidp/dex:v2.36.0
docker pull argoproj/argo-cd-ci-builder:v1.0.0
docker pull redis:7.0.8-alpine
- name: Create target directory for binaries in the build-process

View File

@@ -5,6 +5,7 @@ on:
# Secrets aren't available for dependabot on push. https://docs.github.com/en/enterprise-cloud@latest/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/troubleshooting-the-codeql-workflow#error-403-resource-not-accessible-by-integration-when-using-dependabot
branches-ignore:
- 'dependabot/**'
- 'cherry-pick-*'
pull_request:
schedule:
- cron: '0 19 * * 0'

View File

@@ -1 +1 @@
2.4.27
2.4.28

View File

@@ -9,16 +9,6 @@ setTimeout(function() {
caret.innerHTML = "<i class='fa fa-caret-down dropdown-caret'></i>"
caret.classList.add('dropdown-caret')
div.querySelector('.rst-current-version').appendChild(caret);
div.querySelector('.rst-current-version').addEventListener('click', function() {
const classes = container.className.split(' ');
const index = classes.indexOf('shift-up');
if (index === -1) {
classes.push('shift-up');
} else {
classes.splice(index, 1);
}
container.className = classes.join(' ');
});
}
var CSSLink = document.createElement('link');

View File

@@ -35,7 +35,7 @@ spec:
runAsNonRoot: true
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.35.3
image: ghcr.io/dexidp/dex:v2.36.0
imagePullPolicy: Always
command: [/shared/argocd-dex, rundex]
securityContext:

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.4.27
newTag: v2.4.28
resources:
- ./application-controller
- ./dex

View File

@@ -9384,7 +9384,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -9614,7 +9614,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -9663,7 +9663,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -9850,7 +9850,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.4.27
newTag: v2.4.28

View File

@@ -11,7 +11,7 @@ patchesStrategicMerge:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.4.27
newTag: v2.4.28
resources:
- ../../base/application-controller
- ../../base/applicationset-controller

View File

@@ -10319,7 +10319,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -10391,7 +10391,7 @@ spec:
- command:
- /shared/argocd-dex
- rundex
image: ghcr.io/dexidp/dex:v2.35.3
image: ghcr.io/dexidp/dex:v2.36.0
imagePullPolicy: Always
name: dex
ports:
@@ -10416,7 +10416,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -10456,7 +10456,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -10713,7 +10713,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -10762,7 +10762,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -11009,7 +11009,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -11217,7 +11217,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -1244,7 +1244,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1316,7 +1316,7 @@ spec:
- command:
- /shared/argocd-dex
- rundex
image: ghcr.io/dexidp/dex:v2.35.3
image: ghcr.io/dexidp/dex:v2.36.0
imagePullPolicy: Always
name: dex
ports:
@@ -1341,7 +1341,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1381,7 +1381,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1638,7 +1638,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1687,7 +1687,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1934,7 +1934,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2142,7 +2142,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -9691,7 +9691,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -9763,7 +9763,7 @@ spec:
- command:
- /shared/argocd-dex
- rundex
image: ghcr.io/dexidp/dex:v2.35.3
image: ghcr.io/dexidp/dex:v2.36.0
imagePullPolicy: Always
name: dex
ports:
@@ -9788,7 +9788,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -9828,7 +9828,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -10053,7 +10053,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -10102,7 +10102,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -10345,7 +10345,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -10547,7 +10547,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -616,7 +616,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -688,7 +688,7 @@ spec:
- command:
- /shared/argocd-dex
- rundex
image: ghcr.io/dexidp/dex:v2.35.3
image: ghcr.io/dexidp/dex:v2.36.0
imagePullPolicy: Always
name: dex
ports:
@@ -713,7 +713,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -753,7 +753,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -978,7 +978,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1027,7 +1027,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1270,7 +1270,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -1472,7 +1472,7 @@ spec:
key: otlp.address
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.4.27
image: quay.io/argoproj/argocd:v2.4.28
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -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
}

View File

@@ -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,411 @@ 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) {}
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) {}
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()
// nolint:staticcheck
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<value1",
errorMesage: "error parsing the selector"},
}
//test invalid scenarios
for _, invalidTest := range invalidTests {
t.Run(invalidTest.testName, func(t *testing.T) {
appQuery.Selector = &invalidTest.label
_, err := appServer.List(context.Background(), &appQuery)
assert.ErrorContains(t, err, invalidTest.errorMesage)
})
}
}
func TestListAppWithProjects(t *testing.T) {
appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
appServer := newTestAppServer(t, newTestApp(func(app *appsv1.Application) {
app.Name = "App1"
app.Spec.Project = "test-project1"
}), newTestApp(func(app *appsv1.Application) {
@@ -347,7 +828,7 @@ func TestListAppWithProjects(t *testing.T) {
}
func TestListApps(t *testing.T) {
appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
appServer := newTestAppServer(t, newTestApp(func(app *appsv1.Application) {
app.Name = "bcd"
}), newTestApp(func(app *appsv1.Application) {
app.Name = "abc"
@@ -395,7 +876,7 @@ g, group-49, role:test3
`
_ = enf.SetUserPolicy(policy)
}
appServer := newTestAppServerWithEnforcerConfigure(f, objects...)
appServer := newTestAppServerWithEnforcerConfigure(f, t, objects...)
res, err := appServer.List(ctx, &application.ApplicationQuery{})
@@ -409,7 +890,7 @@ g, group-49, role:test3
func TestCreateApp(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer()
appServer := newTestAppServer(t)
testApp.Spec.Project = ""
createReq := application.ApplicationCreateRequest{
Application: testApp,
@@ -422,7 +903,7 @@ func TestCreateApp(t *testing.T) {
}
func TestCreateAppWithDestName(t *testing.T) {
appServer := newTestAppServer()
appServer := newTestAppServer(t)
testApp := newTestAppWithDestName()
createReq := application.ApplicationCreateRequest{
Application: testApp,
@@ -435,7 +916,7 @@ func TestCreateAppWithDestName(t *testing.T) {
func TestUpdateApp(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
testApp.Spec.Project = ""
app, err := appServer.Update(context.Background(), &application.ApplicationUpdateRequest{
Application: testApp,
@@ -446,7 +927,7 @@ func TestUpdateApp(t *testing.T) {
func TestUpdateAppSpec(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
testApp.Spec.Project = ""
spec, err := appServer.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{
Name: &testApp.Name,
@@ -461,7 +942,7 @@ func TestUpdateAppSpec(t *testing.T) {
func TestDeleteApp(t *testing.T) {
ctx := context.Background()
appServer := newTestAppServer()
appServer := newTestAppServer(t)
createReq := application.ApplicationCreateRequest{
Application: newTestApp(),
}
@@ -546,20 +1027,9 @@ func TestDeleteApp(t *testing.T) {
})
}
func TestDeleteApp_InvalidName(t *testing.T) {
appServer := newTestAppServer()
_, err := appServer.Delete(context.Background(), &application.ApplicationDeleteRequest{
Name: pointer.StringPtr("foo"),
})
if !assert.Error(t, err) {
return
}
assert.True(t, apierrors.IsNotFound(err))
}
func TestSyncAndTerminate(t *testing.T) {
ctx := context.Background()
appServer := newTestAppServer()
appServer := newTestAppServer(t)
testApp := newTestApp()
testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git"
createReq := application.ApplicationCreateRequest{
@@ -599,7 +1069,7 @@ func TestSyncAndTerminate(t *testing.T) {
func TestSyncHelm(t *testing.T) {
ctx := context.Background()
appServer := newTestAppServer()
appServer := newTestAppServer(t)
testApp := newTestApp()
testApp.Spec.Source.RepoURL = "https://argoproj.github.io/argo-helm"
testApp.Spec.Source.Path = ""
@@ -623,7 +1093,7 @@ func TestSyncHelm(t *testing.T) {
func TestSyncGit(t *testing.T) {
ctx := context.Background()
appServer := newTestAppServer()
appServer := newTestAppServer(t)
testApp := newTestApp()
testApp.Spec.Source.RepoURL = "https://github.com/org/test"
testApp.Spec.Source.Path = "deploy"
@@ -656,7 +1126,7 @@ func TestRollbackApp(t *testing.T) {
Revision: "abc",
Source: *testApp.Spec.Source.DeepCopy(),
}}
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
updatedApp, err := appServer.Rollback(context.Background(), &application.ApplicationRollbackRequest{
Name: &testApp.Name,
@@ -676,56 +1146,63 @@ func TestUpdateAppProject(t *testing.T) {
ctx := context.Background()
// nolint:staticcheck
ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
appServer.enf.SetDefaultRole("")
// Verify normal update works (without changing project)
_ = appServer.enf.SetBuiltinPolicy(`p, admin, applications, update, default/test-app, allow`)
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.NoError(t, err)
t.Run("update without changing project", func(t *testing.T) {
_ = appServer.enf.SetBuiltinPolicy(`p, admin, applications, update, default/test-app, allow`)
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.NoError(t, err)
})
// Verify caller cannot update to another project
testApp.Spec.Project = "my-proj"
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
t.Run("cannot update to another project", func(t *testing.T) {
testApp.Spec.Project = "my-proj"
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
})
// Verify inability to change projects without create privileges in new project
_ = appServer.enf.SetBuiltinPolicy(`
t.Run("cannot change projects without create privileges", func(t *testing.T) {
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, update, default/test-app, allow
p, admin, applications, update, my-proj/test-app, allow
`)
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
statusErr := grpc.UnwrapGRPCStatus(err)
assert.NotNil(t, statusErr)
assert.Equal(t, codes.PermissionDenied, statusErr.Code())
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
statusErr := grpc.UnwrapGRPCStatus(err)
assert.NotNil(t, statusErr)
assert.Equal(t, codes.PermissionDenied, statusErr.Code())
})
// Verify inability to change projects without update privileges in new project
_ = appServer.enf.SetBuiltinPolicy(`
t.Run("cannot change projects without update privileges in new project", func(t *testing.T) {
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, update, default/test-app, allow
p, admin, applications, create, my-proj/test-app, allow
`)
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.Equal(t, status.Code(err), codes.PermissionDenied)
})
// Verify inability to change projects without update privileges in old project
_ = appServer.enf.SetBuiltinPolicy(`
t.Run("cannot change projects without update privileges in old project", func(t *testing.T) {
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, create, my-proj/test-app, allow
p, admin, applications, update, my-proj/test-app, allow
`)
_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
statusErr = grpc.UnwrapGRPCStatus(err)
assert.NotNil(t, statusErr)
assert.Equal(t, codes.PermissionDenied, statusErr.Code())
_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
statusErr := grpc.UnwrapGRPCStatus(err)
assert.NotNil(t, statusErr)
assert.Equal(t, codes.PermissionDenied, statusErr.Code())
})
// Verify can update project with proper permissions
_ = appServer.enf.SetBuiltinPolicy(`
t.Run("can update project with proper permissions", func(t *testing.T) {
// Verify can update project with proper permissions
_ = appServer.enf.SetBuiltinPolicy(`
p, admin, applications, update, default/test-app, allow
p, admin, applications, create, my-proj/test-app, allow
p, admin, applications, update, my-proj/test-app, allow
`)
updatedApp, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.NoError(t, err)
assert.Equal(t, "my-proj", updatedApp.Spec.Project)
updatedApp, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
assert.NoError(t, err)
assert.Equal(t, "my-proj", updatedApp.Spec.Project)
})
}
func TestAppJsonPatch(t *testing.T) {
@@ -733,7 +1210,7 @@ func TestAppJsonPatch(t *testing.T) {
ctx := context.Background()
// nolint:staticcheck
ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
appServer.enf.SetDefaultRole("")
app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: pointer.String("garbage")})
@@ -758,7 +1235,7 @@ func TestAppMergePatch(t *testing.T) {
ctx := context.Background()
// nolint:staticcheck
ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
appServer.enf.SetDefaultRole("")
app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{
@@ -771,7 +1248,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
t.Run("Active", func(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "proj-maint"
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
assert.NoError(t, err)
@@ -780,7 +1257,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
t.Run("Inactive", func(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "default"
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
assert.NoError(t, err)
@@ -789,7 +1266,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
t.Run("ProjectDoesNotExist", func(t *testing.T) {
testApp := newTestApp()
testApp.Spec.Project = "none"
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
assert.Contains(t, err.Error(), "not found")
@@ -800,8 +1277,14 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
func TestGetCachedAppState(t *testing.T) {
testApp := newTestApp()
testApp.ObjectMeta.ResourceVersion = "1"
testApp.Spec.Project = "none"
appServer := newTestAppServer(testApp)
testApp.Spec.Project = "test-proj"
testProj := &appsv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "test-proj",
Namespace: testNamespace,
},
}
appServer := newTestAppServer(t, testApp, testProj)
fakeClientSet := appServer.appclientset.(*apps.Clientset)
t.Run("NoError", func(t *testing.T) {
err := appServer.getCachedAppState(context.Background(), testApp, func() error {
@@ -973,7 +1456,7 @@ func TestGetAppRefresh_NormalRefresh(t *testing.T) {
defer cancel()
testApp := newTestApp()
testApp.ObjectMeta.ResourceVersion = "1"
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
var patched int32
@@ -1001,7 +1484,7 @@ func TestGetAppRefresh_HardRefresh(t *testing.T) {
defer cancel()
testApp := newTestApp()
testApp.ObjectMeta.ResourceVersion = "1"
appServer := newTestAppServer(testApp)
appServer := newTestAppServer(t, testApp)
var getAppDetailsQuery *apiclient.RepoServerAppDetailsQuery
mockRepoServiceClient := mocks.RepoServerServiceClient{}

View File

@@ -23,6 +23,14 @@ func (s *subscriber) matches(event *appv1.ApplicationWatchEvent) bool {
return true
}
// Broadcaster is an interface for broadcasting application informer watch events to multiple subscribers.
type Broadcaster interface {
Subscribe(ch chan *appv1.ApplicationWatchEvent, filters ...func(event *appv1.ApplicationWatchEvent) bool) func()
OnAdd(interface{})
OnUpdate(interface{}, interface{})
OnDelete(interface{})
}
type broadcasterHandler struct {
lock sync.Mutex
subscribers []*subscriber

View File

@@ -0,0 +1,66 @@
// Code generated by mockery v2.13.1. DO NOT EDIT.
package mocks
import (
v1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
mock "github.com/stretchr/testify/mock"
)
// Broadcaster is an autogenerated mock type for the Broadcaster type
type Broadcaster struct {
mock.Mock
}
// OnAdd provides a mock function with given fields: _a0
func (_m *Broadcaster) OnAdd(_a0 interface{}) {
_m.Called(_a0)
}
// OnDelete provides a mock function with given fields: _a0
func (_m *Broadcaster) OnDelete(_a0 interface{}) {
_m.Called(_a0)
}
// OnUpdate provides a mock function with given fields: _a0, _a1
func (_m *Broadcaster) OnUpdate(_a0 interface{}, _a1 interface{}) {
_m.Called(_a0, _a1)
}
// Subscribe provides a mock function with given fields: ch, filters
func (_m *Broadcaster) Subscribe(ch chan *v1alpha1.ApplicationWatchEvent, filters ...func(*v1alpha1.ApplicationWatchEvent) bool) func() {
_va := make([]interface{}, len(filters))
for _i := range filters {
_va[_i] = filters[_i]
}
var _ca []interface{}
_ca = append(_ca, ch)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 func()
if rf, ok := ret.Get(0).(func(chan *v1alpha1.ApplicationWatchEvent, ...func(*v1alpha1.ApplicationWatchEvent) bool) func()); ok {
r0 = rf(ch, filters...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(func())
}
}
return r0
}
type mockConstructorTestingTNewBroadcaster interface {
mock.TestingT
Cleanup(func())
}
// NewBroadcaster creates a new instance of Broadcaster. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewBroadcaster(t mockConstructorTestingTNewBroadcaster) *Broadcaster {
mock := &Broadcaster{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -681,6 +681,7 @@ func (a *ArgoCDServer) newGRPCServer() (*grpc.Server, application.AppResourceTre
a.AppClientset,
a.appLister,
a.appInformer,
nil,
a.RepoClientset,
a.Cache,
kubectl,

View File

@@ -1,6 +1,6 @@
controller: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
api-server: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} "
dex: sh -c "test $ARGOCD_IN_CI = true && exit 0; ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.35.3 serve /dex.yaml"
controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} "
dex: sh -c "test $ARGOCD_IN_CI = true && exit 0; ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.36.0 serve /dex.yaml"
redis: sh -c "/usr/local/bin/redis-server --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}"
repo-server: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_BINARY_NAME=argocd-repo-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
ui: sh -c "test $ARGOCD_IN_CI = true && exit 0; cd ui && ARGOCD_E2E_YARN_HOST=0.0.0.0 ${ARGOCD_E2E_YARN_CMD:-yarn} start"

View File

@@ -416,7 +416,9 @@ func TestInvalidAppProject(t *testing.T) {
IgnoreErrors().
CreateApp().
Then().
Expect(Error("", "application references project does-not-exist which does not exist"))
// We're not allowed to infer whether the project exists based on this error message. Instead, we get a generic
// permission denied error.
Expect(Error("", "permission denied"))
}
func TestAppDeletion(t *testing.T) {