diff --git a/pkg/apis/application/v1alpha1/app_project_types.go b/pkg/apis/application/v1alpha1/app_project_types.go index 0f2d268a866ce..af52730a1542c 100644 --- a/pkg/apis/application/v1alpha1/app_project_types.go +++ b/pkg/apis/application/v1alpha1/app_project_types.go @@ -479,6 +479,35 @@ func (proj AppProject) IsSourcePermitted(src ApplicationSource) bool { return anySourceMatched } +// IsCredentialPermittedForAnySource checks whether a credential template URL is a prefix of any +// permitted source repo. Credential URLs are prefix-based by design (e.g. "reg.example.com" +// provides auth for "reg.example.com/org/chart"), so a credential should be permitted if any +// sourceRepo falls under its URL prefix. +func (proj AppProject) IsCredentialPermittedForAnySource(credURL string) bool { + credNormalized := git.NormalizeGitURL(credURL) + if credNormalized == "" { + return false + } + + for _, repoURL := range proj.Spec.SourceRepos { + if repoURL == "*" { + return true + } + if isDenyPattern(repoURL) { + continue + } + normalized := git.NormalizeGitURL(repoURL) + // Strip oci:// scheme for comparison since credential URLs are typically stored without it + normalizedNoScheme := strings.TrimPrefix(normalized, "oci://") + credNoScheme := strings.TrimPrefix(credNormalized, "oci://") + // Check if the sourceRepo URL starts with the credential URL prefix + if strings.HasPrefix(normalizedNoScheme, credNoScheme+"/") || normalizedNoScheme == credNoScheme { + return true + } + } + return false +} + // IsDestinationPermitted validates if the provided application's destination is one of the allowed destinations for the project func (proj AppProject) IsDestinationPermitted(destCluster *Cluster, destNamespace string, projectClusters func(project string) ([]*Cluster, error)) (bool, error) { if destCluster == nil { diff --git a/pkg/apis/application/v1alpha1/types_test.go b/pkg/apis/application/v1alpha1/types_test.go index 66f22bcecf8de..c279ec27dc036 100644 --- a/pkg/apis/application/v1alpha1/types_test.go +++ b/pkg/apis/application/v1alpha1/types_test.go @@ -104,6 +104,81 @@ func TestAppProject_IsNegatedSourcePermitted(t *testing.T) { } } +func TestAppProject_IsCredentialPermittedForAnySource(t *testing.T) { + testData := []struct { + name string + projSources []string + credURL string + isPermitted bool + }{{ + name: "wildcard permits any credential", + projSources: []string{"*"}, + credURL: "reg.example.com", + isPermitted: true, + }, { + name: "exact match permits credential", + projSources: []string{"reg.example.com"}, + credURL: "reg.example.com", + isPermitted: true, + }, { + name: "credential URL is prefix of sourceRepo", + projSources: []string{"reg.example.com/org/charts"}, + credURL: "reg.example.com", + isPermitted: true, + }, { + name: "credential URL is prefix of multiple sourceRepos", + projSources: []string{"https://github.com/test/repo.git", "reg.example.com/org/charts"}, + credURL: "reg.example.com", + isPermitted: true, + }, { + name: "OCI sourceRepo with credential prefix", + projSources: []string{"oci://reg.example.com/org/charts"}, + credURL: "reg.example.com", + isPermitted: true, + }, { + name: "OCI credential URL matches OCI sourceRepo", + projSources: []string{"oci://reg.example.com/org/charts"}, + credURL: "oci://reg.example.com", + isPermitted: true, + }, { + name: "credential URL does not match any sourceRepo", + projSources: []string{"https://github.com/test/repo.git"}, + credURL: "reg.example.com", + isPermitted: false, + }, { + name: "credential URL is more specific than sourceRepo", + projSources: []string{"reg.example.com"}, + credURL: "reg.example.com/org/charts", + isPermitted: false, + }, { + name: "partial hostname match should not permit", + projSources: []string{"reg.example.com-evil/org/charts"}, + credURL: "reg.example.com", + isPermitted: false, + }, { + name: "deny pattern does not permit credential", + projSources: []string{"!reg.example.com/org/charts"}, + credURL: "reg.example.com", + isPermitted: false, + }, { + name: "empty sourceRepos does not permit credential", + projSources: []string{}, + credURL: "reg.example.com", + isPermitted: false, + }} + + for _, data := range testData { + t.Run(data.name, func(t *testing.T) { + proj := AppProject{ + Spec: AppProjectSpec{ + SourceRepos: data.projSources, + }, + } + assert.Equal(t, data.isPermitted, proj.IsCredentialPermittedForAnySource(data.credURL)) + }) + } +} + func TestAppProject_IsDestinationPermitted(t *testing.T) { t.Parallel() diff --git a/util/argo/argo.go b/util/argo/argo.go index ddc0190db52ba..5d21d0eeb033f 100644 --- a/util/argo/argo.go +++ b/util/argo/argo.go @@ -1060,7 +1060,7 @@ func NormalizeSource(source *argoappv1.ApplicationSource) *argoappv1.Application func GetPermittedReposCredentials(proj *argoappv1.AppProject, repoCreds []*argoappv1.RepoCreds) ([]*argoappv1.RepoCreds, error) { var permittedRepoCreds []*argoappv1.RepoCreds for _, v := range repoCreds { - if proj.IsSourcePermitted(argoappv1.ApplicationSource{RepoURL: v.URL}) { + if proj.IsSourcePermitted(argoappv1.ApplicationSource{RepoURL: v.URL}) || proj.IsCredentialPermittedForAnySource(v.URL) { permittedRepoCreds = append(permittedRepoCreds, v) } }