diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 0489f222fab..ac987f2cabd 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -7533,6 +7533,14 @@ definitions: type: string description: 'Whether the proxy cache project should proxy OCI 1.1 referrer API requests to the upstream registry. The valid values are "true", "false".' x-nullable: true + proxy_cache_filter_pattern: + type: string + description: 'Repository filter pattern for proxy cache project. Only repositories matching the pattern will be proxied. Empty means allow all. Only has value when the current project is a proxy cache project.' + x-nullable: true + proxy_cache_filter_kind: + type: string + description: 'Matching mode for proxy_cache_filter_pattern: "doublestar" (default, glob-style with ** support) or "regex". Only has value when the current project is a proxy cache project.' + x-nullable: true ProjectSummary: type: object properties: diff --git a/src/lib/pattern/matcher.go b/src/lib/pattern/matcher.go new file mode 100644 index 00000000000..0135509480e --- /dev/null +++ b/src/lib/pattern/matcher.go @@ -0,0 +1,117 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pattern + +import ( + "regexp" + "strings" + + "github.com/bmatcuk/doublestar" + + "github.com/goharbor/harbor/src/lib/errors" +) + +const ( + // KindRegex indicates regular expression pattern matching + KindRegex = "regex" + // KindDoublestar indicates doublestar (glob) pattern matching + KindDoublestar = "doublestar" +) + +// RepositoryFilter represents a repository filter configuration with pattern and kind +type RepositoryFilter struct { + // Filter is the pattern expression to match against + Filter string + // Kind is the type of pattern matching: "regex" or "doublestar" + Kind string +} + +// NewRepositoryFilter creates a RepositoryFilter from separate pattern and kind strings. +// If kind is empty, it defaults to KindDoublestar. +func NewRepositoryFilter(filterPattern, kind string) *RepositoryFilter { + return &RepositoryFilter{ + Filter: strings.TrimSpace(filterPattern), + Kind: strings.TrimSpace(kind), + } +} + +// Match returns true if the value matches the filter pattern. +// Returns true if the filter is empty. +func (rf *RepositoryFilter) Match(value string) (bool, error) { + if rf == nil || rf.Filter == "" { + return true, nil + } + matcher := NewMatcher(rf.Kind) + return matcher.Match(value, rf.Filter) +} + +// ValidateRepositoryFilter validates the repository filter kind and pattern. +// Empty pattern is valid and means all repositories are allowed. +func ValidateRepositoryFilter(filterPattern, kind string) error { + rf := NewRepositoryFilter(filterPattern, kind) + if rf.Kind != "" && rf.Kind != KindRegex && rf.Kind != KindDoublestar { + return errors.Errorf("unsupported repository filter kind %q", kind) + } + _, err := rf.Match("") + return err +} + +// Matcher is an interface for matching strings against patterns +type Matcher interface { + // Match returns true if the value matches the pattern + Match(value, pattern string) (bool, error) +} + +// RegexMatcher implements Matcher using regular expressions +type RegexMatcher struct{} + +// Match matches value against a regular expression pattern +func (r *RegexMatcher) Match(value, pattern string) (bool, error) { + pattern = strings.TrimSpace(pattern) + if pattern == "" { + return true, nil + } + re, err := regexp.Compile(pattern) + if err != nil { + return false, err + } + match := re.FindStringIndex(value) + return match != nil && match[0] == 0 && match[1] == len(value), nil +} + +// DoublestarMatcher implements Matcher using doublestar (glob) patterns +type DoublestarMatcher struct{} + +// Match matches value against a doublestar (glob) pattern +func (d *DoublestarMatcher) Match(value, pattern string) (bool, error) { + pattern = strings.TrimSpace(pattern) + if pattern == "" { + return true, nil + } + return doublestar.Match(pattern, value) +} + +// NewMatcher creates a new Matcher based on the specified kind. +// Defaults to KindDoublestar when kind is empty or unrecognised. +func NewMatcher(kind string) Matcher { + switch kind { + case KindRegex: + return &RegexMatcher{} + case KindDoublestar: + fallthrough + default: + return &DoublestarMatcher{} + } +} diff --git a/src/lib/pattern/matcher_test.go b/src/lib/pattern/matcher_test.go new file mode 100644 index 00000000000..ae13506b958 --- /dev/null +++ b/src/lib/pattern/matcher_test.go @@ -0,0 +1,346 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pattern + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewRepositoryFilter(t *testing.T) { + tests := []struct { + name string + pattern string + kind string + want *RepositoryFilter + }{ + { + name: "empty pattern and kind", + pattern: "", + kind: "", + want: &RepositoryFilter{Filter: "", Kind: ""}, + }, + { + name: "regex filter", + pattern: "^library/.*", + kind: KindRegex, + want: &RepositoryFilter{Filter: "^library/.*", Kind: KindRegex}, + }, + { + name: "doublestar filter", + pattern: "library/**", + kind: KindDoublestar, + want: &RepositoryFilter{Filter: "library/**", Kind: KindDoublestar}, + }, + { + name: "pattern with whitespace is trimmed", + pattern: " nginx ", + kind: "", + want: &RepositoryFilter{Filter: "nginx", Kind: ""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewRepositoryFilter(tt.pattern, tt.kind) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRepositoryFilter_Match(t *testing.T) { + tests := []struct { + name string + rf *RepositoryFilter + value string + want bool + wantErr bool + }{ + { + name: "nil filter matches all", + rf: nil, + value: "library/nginx", + want: true, + }, + { + name: "empty filter matches all", + rf: &RepositoryFilter{}, + value: "library/nginx", + want: true, + }, + { + name: "regex prefix is anchored", + rf: &RepositoryFilter{Filter: "^library/", Kind: KindRegex}, + value: "library/nginx", + want: false, + }, + { + name: "regex wildcard match", + rf: &RepositoryFilter{Filter: "^library/.*", Kind: KindRegex}, + value: "library/nginx", + want: true, + }, + { + name: "regex no match", + rf: &RepositoryFilter{Filter: "^other/", Kind: KindRegex}, + value: "library/nginx", + want: false, + }, + { + name: "doublestar match", + rf: &RepositoryFilter{Filter: "library/**", Kind: KindDoublestar}, + value: "library/nginx", + want: true, + }, + { + name: "doublestar no match", + rf: &RepositoryFilter{Filter: "other/**", Kind: KindDoublestar}, + value: "library/nginx", + want: false, + }, + { + name: "empty kind defaults to doublestar", + rf: &RepositoryFilter{Filter: "nginx"}, + value: "nginx", + want: true, + }, + { + name: "invalid regex", + rf: &RepositoryFilter{Filter: "[invalid", Kind: KindRegex}, + value: "library/nginx", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.rf.Match(tt.value) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRegexMatcher(t *testing.T) { + m := &RegexMatcher{} + + tests := []struct { + name string + value string + pattern string + want bool + wantErr bool + }{ + { + name: "empty pattern matches all", + value: "library/nginx", + pattern: "", + want: true, + }, + { + name: "whitespace pattern matches all", + value: "library/nginx", + pattern: " ", + want: true, + }, + { + name: "exact match", + value: "library/nginx", + pattern: "^library/nginx$", + want: true, + }, + { + name: "partial match is anchored", + value: "library/nginx", + pattern: "nginx", + want: false, + }, + { + name: "prefix match is anchored", + value: "library/nginx", + pattern: "^library/", + want: false, + }, + { + name: "no match", + value: "library/nginx", + pattern: "^other/", + want: false, + }, + { + name: "wildcard match", + value: "library/nginx", + pattern: "library/.*", + want: true, + }, + { + name: "invalid regex returns error", + value: "library/nginx", + pattern: "[invalid", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := m.Match(tt.value, tt.pattern) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestDoublestarMatcher(t *testing.T) { + m := &DoublestarMatcher{} + + tests := []struct { + name string + value string + pattern string + want bool + wantErr bool + }{ + { + name: "empty pattern matches all", + value: "library/nginx", + pattern: "", + want: true, + }, + { + name: "whitespace pattern matches all", + value: "library/nginx", + pattern: " ", + want: true, + }, + { + name: "exact match", + value: "library/nginx", + pattern: "library/nginx", + want: true, + }, + { + name: "single star matches within path segment", + value: "library/nginx", + pattern: "library/*", + want: true, + }, + { + name: "double star matches across path segments", + value: "org/team/repo", + pattern: "org/**", + want: true, + }, + { + name: "double star matches everything", + value: "library/nginx", + pattern: "**", + want: true, + }, + { + name: "double star in middle", + value: "a/b/c/d", + pattern: "a/**/d", + want: true, + }, + { + name: "no match", + value: "library/nginx", + pattern: "other/*", + want: false, + }, + { + name: "question mark wildcard", + value: "library/nginx1", + pattern: "library/nginx?", + want: true, + }, + { + name: "character class", + value: "library/nginx1", + pattern: "library/nginx[0-9]", + want: true, + }, + { + name: "alternation", + value: "library/nginx", + pattern: "library/{nginx,alpine}", + want: true, + }, + { + name: "invalid pattern returns error", + value: "library/nginx", + pattern: "[invalid", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := m.Match(tt.value, tt.pattern) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestNewMatcher(t *testing.T) { + tests := []struct { + name string + kind string + want Matcher + }{ + { + name: "regex kind", + kind: KindRegex, + want: &RegexMatcher{}, + }, + { + name: "doublestar kind", + kind: KindDoublestar, + want: &DoublestarMatcher{}, + }, + { + name: "empty kind defaults to doublestar", + kind: "", + want: &DoublestarMatcher{}, + }, + { + name: "unknown kind defaults to doublestar", + kind: "unknown", + want: &DoublestarMatcher{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewMatcher(tt.kind) + assert.IsType(t, tt.want, got) + }) + } +} diff --git a/src/pkg/project/models/pro_meta.go b/src/pkg/project/models/pro_meta.go index 4f6f0f8845c..631f6767130 100644 --- a/src/pkg/project/models/pro_meta.go +++ b/src/pkg/project/models/pro_meta.go @@ -26,6 +26,8 @@ const ( ProMetaAutoSBOMGen = "auto_sbom_generation" ProMetaProxySpeed = "proxy_speed_kb" ProMetaMaxUpstreamConn = "max_upstream_conn" + ProMetaProxyCacheFilterPattern = "proxy_cache_filter_pattern" // plain string pattern for proxy cache repository filter + ProMetaProxyCacheFilterKind = "proxy_cache_filter_kind" // "doublestar" (default) or "regex" ProMetaProxyReferrerAPI = "proxy_referrer_api" ProMetaProxyCacheLocalOnNotFound = "proxy_cache_local_on_not_found" ) diff --git a/src/server/middleware/repoproxy/proxy.go b/src/server/middleware/repoproxy/proxy.go index 85d4dc5fef2..93ffd6985fc 100644 --- a/src/server/middleware/repoproxy/proxy.go +++ b/src/server/middleware/repoproxy/proxy.go @@ -34,6 +34,7 @@ import ( httpLib "github.com/goharbor/harbor/src/lib/http" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/pattern" "github.com/goharbor/harbor/src/lib/redis" proModels "github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/pkg/proxy/connection" @@ -193,6 +194,22 @@ func defaultBlobURL(projectName string, name string, digest string) string { return fmt.Sprintf("/v2/%s/library/%s/blobs/%s", projectName, name, digest) } +// matchRepositoryFilter returns true if repository matches the filter. +// filterPattern is the plain pattern string; filterKind is "doublestar" or "regex" (defaults to doublestar when empty). +// If filterPattern is empty, all repositories are allowed. +// An invalid pattern is treated as no match. +func matchRepositoryFilter(repository, filterPattern, filterKind string) bool { + log.Debugf("matching repository %q against filter pattern: %q (kind: %s)", repository, filterPattern, filterKind) + rf := pattern.NewRepositoryFilter(filterPattern, filterKind) + matched, err := rf.Match(repository) + if err != nil { + log.Warningf("invalid proxy_cache_filter_pattern %q (kind: %s): %v", rf.Filter, rf.Kind, err) + return false + } + log.Debugf("repository %q match result: %v (filter: %q, kind: %s)", repository, matched, rf.Filter, rf.Kind) + return matched +} + // upstreamRegistryConnectionKey get upstream registry connection key func upstreamRegistryConnectionKey(art lib.ArtifactInfo) string { limitOnProject := os.Getenv(upstreamRegistryLimitOnProject) @@ -219,6 +236,16 @@ func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) e return nil } + // Apply repository filter: if proxy_cache_filter_pattern is set, the repository must match + if filterPattern, ok := p.GetMetadata(proModels.ProMetaProxyCacheFilterPattern); ok && filterPattern != "" { + filterKind, _ := p.GetMetadata(proModels.ProMetaProxyCacheFilterKind) + remoteRepo := strings.TrimPrefix(art.Repository, art.ProjectName+"/") + if !matchRepositoryFilter(remoteRepo, filterPattern, filterKind) { + log.Infof("blocked proxy cache pull for project %q repository %q: repository does not match filter %q (kind: %s)", p.Name, remoteRepo, filterPattern, filterKind) + return errors.NotFoundError(fmt.Errorf("repository %q does not match project repository filter %q (kind: %s)", remoteRepo, filterPattern, filterKind)) + } + } + if !canProxy(r.Context(), p) { next.ServeHTTP(w, r) return nil diff --git a/src/server/middleware/repoproxy/proxy_test.go b/src/server/middleware/repoproxy/proxy_test.go index c47f20db040..e325cefa9e7 100644 --- a/src/server/middleware/repoproxy/proxy_test.go +++ b/src/server/middleware/repoproxy/proxy_test.go @@ -80,3 +80,129 @@ func TestIsProxySession(t *testing.T) { }) } } + +func TestMatchRepositoryFilter(t *testing.T) { + cases := []struct { + name string + repository string + filterPattern string + filterKind string + want bool + }{ + { + name: "empty pattern matches all", + repository: "library/nginx", + filterPattern: "", + filterKind: "", + want: true, + }, + { + name: "regex exact match", + repository: "library/nginx", + filterPattern: "^library/nginx$", + filterKind: "regex", + want: true, + }, + { + name: "regex partial match is anchored", + repository: "library/nginx", + filterPattern: "nginx", + filterKind: "regex", + want: false, + }, + { + name: "regex prefix match is anchored", + repository: "library/nginx", + filterPattern: "^library/", + filterKind: "regex", + want: false, + }, + { + name: "regex wildcard match", + repository: "library/nginx", + filterPattern: "^library/.*", + filterKind: "regex", + want: true, + }, + { + name: "regex no match", + repository: "library/nginx", + filterPattern: "^other/", + filterKind: "regex", + want: false, + }, + { + name: "regex invalid pattern treated as no match", + repository: "library/nginx", + filterPattern: "[invalid", + filterKind: "regex", + want: false, + }, + { + name: "doublestar exact match", + repository: "library/nginx", + filterPattern: "library/nginx", + filterKind: "doublestar", + want: true, + }, + { + name: "doublestar single star", + repository: "library/nginx", + filterPattern: "library/*", + filterKind: "doublestar", + want: true, + }, + { + name: "doublestar double star", + repository: "org/team/repo", + filterPattern: "org/**", + filterKind: "doublestar", + want: true, + }, + { + name: "doublestar match all", + repository: "library/nginx", + filterPattern: "**", + filterKind: "doublestar", + want: true, + }, + { + name: "doublestar no match", + repository: "library/nginx", + filterPattern: "other/*", + filterKind: "doublestar", + want: false, + }, + { + name: "doublestar alternation", + repository: "library/nginx", + filterPattern: "library/{nginx,alpine}", + filterKind: "doublestar", + want: true, + }, + { + name: "empty kind defaults to doublestar", + repository: "library/nginx", + filterPattern: "library/**", + filterKind: "", + want: true, + }, + { + name: "empty pattern with kind set matches all", + repository: "library/nginx", + filterPattern: "", + filterKind: "regex", + want: true, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + got := matchRepositoryFilter(tt.repository, tt.filterPattern, tt.filterKind) + if got != tt.want { + t.Errorf("matchRepositoryFilter(%q, %q, %q) = %v; want %v", + tt.repository, tt.filterPattern, tt.filterKind, got, tt.want) + } + }) + } +} diff --git a/src/server/v2.0/handler/model/project.go b/src/server/v2.0/handler/model/project.go index d821e8c9001..8599fe13bd6 100644 --- a/src/server/v2.0/handler/model/project.go +++ b/src/server/v2.0/handler/model/project.go @@ -52,6 +52,12 @@ func (p *Project) ToSwagger() *models.Project { m.Severity = &severity } + // proxy_cache_filter_pattern and proxy_cache_filter_kind are only for proxy cache projects + if !p.IsProxy() { + m.ProxyCacheFilterPattern = nil + m.ProxyCacheFilterKind = nil + } + md = &m } diff --git a/src/server/v2.0/handler/project.go b/src/server/v2.0/handler/project.go index de75072b8c7..797b900a621 100644 --- a/src/server/v2.0/handler/project.go +++ b/src/server/v2.0/handler/project.go @@ -44,6 +44,7 @@ import ( "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/pattern" "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg" "github.com/goharbor/harbor/src/pkg/audit" @@ -163,10 +164,12 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP } } - // ignore metadata.proxy_speed_kb and metadata.max_upstream_conn for non-proxy-cache project + // ignore metadata.proxy_speed_kb, metadata.max_upstream_conn, proxy_cache_filter_pattern and proxy_cache_filter_kind for non-proxy-cache project if req.RegistryID == nil { req.Metadata.ProxySpeedKb = nil req.Metadata.MaxUpstreamConn = nil + req.Metadata.ProxyCacheFilterPattern = nil + req.Metadata.ProxyCacheFilterKind = nil } // ignore enable_content_trust metadata for proxy cache project @@ -567,16 +570,21 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP } } - // ignore metadata.proxy_speed_kb and metadata.max_upstream_conn for non-proxy-cache project + // ignore metadata.proxy_speed_kb, metadata.max_upstream_conn, proxy_cache_filter_pattern and proxy_cache_filter_kind for non-proxy-cache project if params.Project.Metadata != nil && !p.IsProxy() { params.Project.Metadata.ProxySpeedKb = nil params.Project.Metadata.MaxUpstreamConn = nil + params.Project.Metadata.ProxyCacheFilterPattern = nil + params.Project.Metadata.ProxyCacheFilterKind = nil } // ignore enable_content_trust metadata for proxy cache project // see https://github.com/goharbor/harbor/issues/12940 to get more info if params.Project.Metadata != nil && p.IsProxy() { params.Project.Metadata.EnableContentTrust = nil + if err := validateProxyCacheRepositoryFilter(params.Project.Metadata); err != nil { + return a.SendError(ctx, err) + } } if err := lib.JSONCopy(&p.Metadata, params.Project.Metadata); err != nil { log.Warningf("failed to call JSONCopy on project metadata when UpdateProject, error: %v", err) @@ -826,6 +834,10 @@ func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.Project return errors.BadRequestError(nil).WithMessagef("metadata.max_upstream_conn should be an int, but got '%s', err: %s", *cnt, err) } } + + if err := validateProxyCacheRepositoryFilter(req.Metadata); err != nil { + return err + } } if req.StorageLimit != nil { @@ -838,6 +850,27 @@ func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.Project return nil } +func validateProxyCacheRepositoryFilter(metadata *models.ProjectMetadata) error { + if metadata == nil { + return nil + } + + filterKind := lib.StringValue(metadata.ProxyCacheFilterKind) + if filterKind == "" { + filterKind = pattern.KindDoublestar + } + if filterKind != pattern.KindRegex && filterKind != pattern.KindDoublestar { + return errors.BadRequestError(nil). + WithMessagef("metadata.proxy_cache_filter_kind should be %q or %q, but got: %q", pattern.KindDoublestar, pattern.KindRegex, filterKind) + } + + if err := pattern.ValidateRepositoryFilter(lib.StringValue(metadata.ProxyCacheFilterPattern), filterKind); err != nil { + return errors.BadRequestError(nil). + WithMessagef("metadata.proxy_cache_filter_pattern is invalid for kind %q: %v", filterKind, err) + } + return nil +} + func (a *projectAPI) populateProperties(ctx context.Context, p *project.Project) error { if secCtx, ok := security.FromContext(ctx); ok { if sc, ok := secCtx.(*local.SecurityContext); ok { diff --git a/src/server/v2.0/handler/project_metadata.go b/src/server/v2.0/handler/project_metadata.go index 831907a0e13..80685e0682e 100644 --- a/src/server/v2.0/handler/project_metadata.go +++ b/src/server/v2.0/handler/project_metadata.go @@ -25,6 +25,7 @@ import ( "github.com/goharbor/harbor/src/controller/project" "github.com/goharbor/harbor/src/controller/project/metadata" "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/pattern" proModels "github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/pkg/scan/vuln" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/project_metadata" @@ -168,6 +169,20 @@ func (p *projectMetadataAPI) validate(metas map[string]string) (map[string]strin return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid value: %s", value) } metas[proModels.ProMetaMaxUpstreamConn] = strconv.FormatInt(v, 10) + case proModels.ProMetaProxyCacheFilterPattern: + // plain pattern string; empty means match all — no further validation needed at metadata level + if err := pattern.ValidateRepositoryFilter(value, pattern.KindDoublestar); err != nil { + return nil, errors.New(nil).WithCode(errors.BadRequestCode). + WithMessagef("invalid proxy_cache_filter_pattern: %q, err: %v", value, err) + } + metas[proModels.ProMetaProxyCacheFilterPattern] = value + case proModels.ProMetaProxyCacheFilterKind: + // must be "doublestar" or "regex" when specified + if value != "" && value != pattern.KindRegex && value != pattern.KindDoublestar { + return nil, errors.New(nil).WithCode(errors.BadRequestCode). + WithMessagef("invalid proxy_cache_filter_kind: %q, must be %q or %q", value, pattern.KindDoublestar, pattern.KindRegex) + } + metas[proModels.ProMetaProxyCacheFilterKind] = value default: return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid key: %s", key) }