fix(server): validate new project on update (#23970) (#23973)

Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
This commit is contained in:
Alexandre Gaudreault
2025-08-17 23:29:22 -04:00
committed by GitHub
parent 13c47ee244
commit 8c8902b93f
3 changed files with 146 additions and 25 deletions

View File

@@ -1317,6 +1317,12 @@ func (s *Server) validateAndNormalizeApp(ctx context.Context, app *v1alpha1.Appl
if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceApplications, rbac.ActionUpdate, currApp.RBACName(s.ns)); err != nil {
return err
}
// Validate that the new project exists and the application is allowed to use it
newProj, err := s.getAppProject(ctx, app, log.WithFields(applog.GetAppLogFields(app)))
if err != nil {
return err
}
proj = newProj
}
if _, err := argo.GetDestinationCluster(ctx, app.Spec.Destination, s.db); err != nil {

View File

@@ -1608,14 +1608,130 @@ func TestCreateAppUpsert(t *testing.T) {
}
func TestUpdateApp(t *testing.T) {
testApp := newTestApp()
appServer := newTestAppServer(t, testApp)
testApp.Spec.Project = ""
app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: testApp,
t.Parallel()
t.Run("Same spec", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
appServer := newTestAppServer(t, testApp)
testApp.Spec.Project = ""
app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: testApp,
})
require.NoError(t, err)
assert.Equal(t, "default", app.Spec.Project)
})
t.Run("Invalid existing app can be updated", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
testApp.Spec.Destination.Server = "https://invalid-cluster"
appServer := newTestAppServer(t, testApp)
updateApp := newTestAppWithDestName()
updateApp.TypeMeta = testApp.TypeMeta
updateApp.Spec.Source.Name = "updated"
app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: updateApp,
})
require.NoError(t, err)
require.NotNil(t, app)
assert.Equal(t, "updated", app.Spec.Source.Name)
})
t.Run("Can update application project from invalid", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
restrictedProj := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "restricted-proj", Namespace: "default"},
Spec: v1alpha1.AppProjectSpec{
SourceRepos: []string{"not-your-repo"},
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "not-your-namespace"}},
},
}
testApp.Spec.Project = restrictedProj.Name
appServer := newTestAppServer(t, testApp, restrictedProj)
updateApp := newTestAppWithDestName()
updateApp.TypeMeta = testApp.TypeMeta
updateApp.Spec.Project = "my-proj"
app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: updateApp,
})
require.NoError(t, err)
require.NotNil(t, app)
assert.Equal(t, "my-proj", app.Spec.Project)
})
t.Run("Cannot update application project to invalid", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
restrictedProj := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "restricted-proj", Namespace: "default"},
Spec: v1alpha1.AppProjectSpec{
SourceRepos: []string{"not-your-repo"},
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "not-your-namespace"}},
},
}
appServer := newTestAppServer(t, testApp, restrictedProj)
updateApp := newTestAppWithDestName()
updateApp.TypeMeta = testApp.TypeMeta
updateApp.Spec.Project = restrictedProj.Name
_, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: updateApp,
})
require.Error(t, err)
require.ErrorContains(t, err, "application repo https://github.com/argoproj/argocd-example-apps.git is not permitted in project 'restricted-proj'")
require.ErrorContains(t, err, "application destination server 'fake-cluster' and namespace 'fake-dest-ns' do not match any of the allowed destinations in project 'restricted-proj'")
})
t.Run("Cannot update application project to inexisting", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
appServer := newTestAppServer(t, testApp)
updateApp := newTestAppWithDestName()
updateApp.TypeMeta = testApp.TypeMeta
updateApp.Spec.Project = "i-do-not-exist"
_, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: updateApp,
})
require.Error(t, err)
require.ErrorContains(t, err, "app is not allowed in project \"i-do-not-exist\", or the project does not exist")
})
t.Run("Can update application project with project argument", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
appServer := newTestAppServer(t, testApp)
updateApp := newTestAppWithDestName()
updateApp.TypeMeta = testApp.TypeMeta
updateApp.Spec.Project = "my-proj"
app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: updateApp,
Project: ptr.To("default"),
})
require.NoError(t, err)
require.NotNil(t, app)
assert.Equal(t, "my-proj", app.Spec.Project)
})
t.Run("Existing label and annotations are replaced", func(t *testing.T) {
t.Parallel()
testApp := newTestApp()
testApp.Annotations = map[string]string{"test": "test-value", "update": "old"}
testApp.Labels = map[string]string{"test": "test-value", "update": "old"}
appServer := newTestAppServer(t, testApp)
updateApp := newTestAppWithDestName()
updateApp.TypeMeta = testApp.TypeMeta
updateApp.Annotations = map[string]string{"update": "new"}
updateApp.Labels = map[string]string{"update": "new"}
app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{
Application: updateApp,
})
require.NoError(t, err)
require.NotNil(t, app)
assert.Len(t, app.Annotations, 1)
assert.Equal(t, "new", app.GetAnnotations()["update"])
assert.Len(t, app.Labels, 1)
assert.Equal(t, "new", app.GetLabels()["update"])
})
require.NoError(t, err)
assert.Equal(t, "default", app.Spec.Project)
}
func TestUpdateAppSpec(t *testing.T) {

View File

@@ -589,7 +589,7 @@ func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, p
if !proj.IsSourcePermitted(spec.SourceHydrator.GetDrySource()) {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.GetSource().RepoURL, spec.Project),
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.SourceHydrator.GetDrySource().RepoURL, proj.Name),
})
}
case spec.HasMultipleSources():
@@ -603,7 +603,7 @@ func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, p
if !proj.IsSourcePermitted(source) {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", source.RepoURL, spec.Project),
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", source.RepoURL, proj.Name),
})
}
}
@@ -616,7 +616,7 @@ func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, p
if !proj.IsSourcePermitted(spec.GetSource()) {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.GetSource().RepoURL, spec.Project),
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.GetSource().RepoURL, proj.Name),
})
}
}
@@ -629,22 +629,21 @@ func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, p
})
return conditions, nil
}
if destCluster.Server != "" {
permitted, err := proj.IsDestinationPermitted(destCluster, spec.Destination.Namespace, func(project string) ([]*argoappv1.Cluster, error) {
return db.GetProjectClusters(ctx, project)
permitted, err := proj.IsDestinationPermitted(destCluster, spec.Destination.Namespace, func(project string) ([]*argoappv1.Cluster, error) {
return db.GetProjectClusters(ctx, project)
})
if err != nil {
return nil, err
}
if !permitted {
server := destCluster.Server
if spec.Destination.Name != "" {
server = destCluster.Name
}
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: fmt.Sprintf("application destination server '%s' and namespace '%s' do not match any of the allowed destinations in project '%s'", server, spec.Destination.Namespace, proj.Name),
})
if err != nil {
return nil, err
}
if !permitted {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: fmt.Sprintf("application destination server '%s' and namespace '%s' do not match any of the allowed destinations in project '%s'", spec.Destination.Server, spec.Destination.Namespace, spec.Project),
})
}
} else if destCluster.Server == "" {
conditions = append(conditions, argoappv1.ApplicationCondition{Type: argoappv1.ApplicationConditionInvalidSpecError, Message: ErrDestinationMissing})
}
return conditions, nil
}