mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +01:00
fix(api): escape path paramters before APIs and unescape them in the Prepare of operations (#11013)
1. Escape the path paramters before the APIs. 2. Unescape the path paramters in the Prepare stage of the swagger operations. Closes #10860 Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
parent
c7fd3bdfc5
commit
41edfaf3a6
100
src/server/middleware/path/path.go
Normal file
100
src/server/middleware/path/path.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// 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 path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/api"
|
||||||
|
"github.com/goharbor/harbor/src/server/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultRegexps = []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`^/api/` + api.APIVersion + `/projects/.*/repositories/(.*)/artifacts/?$`),
|
||||||
|
regexp.MustCompile(`^/api/` + api.APIVersion + `/projects/.*/repositories/(.*)/artifacts/.*$`),
|
||||||
|
regexp.MustCompile(`^/api/` + api.APIVersion + `/projects/.*/repositories/(.*)/?$`),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// EscapeMiddleware middleware which escape path parameters for swagger APIs
|
||||||
|
func EscapeMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
|
||||||
|
for _, re := range defaultRegexps {
|
||||||
|
if re.MatchString(r.URL.Path) {
|
||||||
|
r.URL.Path = escape(re, r.URL.Path)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func escape(re *regexp.Regexp, path string) string {
|
||||||
|
return replaceAllSubmatchFunc(re, path, func(groups []string) []string {
|
||||||
|
var results []string
|
||||||
|
for _, group := range groups {
|
||||||
|
results = append(results, url.PathEscape(group))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceAllSubmatchFunc(re *regexp.Regexp, src string, repl func([]string) []string, n int) string {
|
||||||
|
var result string
|
||||||
|
|
||||||
|
last := 0
|
||||||
|
for _, match := range re.FindAllSubmatchIndex([]byte(src), n) {
|
||||||
|
// Append string between our last match and this one (i.e. non-matched string).
|
||||||
|
matchStart := match[0]
|
||||||
|
matchEnd := match[1]
|
||||||
|
result = result + src[last:matchStart]
|
||||||
|
last = matchEnd
|
||||||
|
|
||||||
|
// Determine the groups / submatch string and indices.
|
||||||
|
groups := []string{}
|
||||||
|
indices := [][2]int{}
|
||||||
|
for i := 2; i < len(match); i += 2 {
|
||||||
|
start := match[i]
|
||||||
|
end := match[i+1]
|
||||||
|
groups = append(groups, src[start:end])
|
||||||
|
indices = append(indices, [2]int{start, end})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the groups
|
||||||
|
groups = repl(groups)
|
||||||
|
|
||||||
|
// Append match data.
|
||||||
|
lastGroup := matchStart
|
||||||
|
for i, newValue := range groups {
|
||||||
|
// Append string between our last group match and this one (i.e. non-group-matched string)
|
||||||
|
groupStart := indices[i][0]
|
||||||
|
groupEnd := indices[i][1]
|
||||||
|
result = result + src[lastGroup:groupStart]
|
||||||
|
lastGroup = groupEnd
|
||||||
|
|
||||||
|
// Append the new group value.
|
||||||
|
result = result + newValue
|
||||||
|
}
|
||||||
|
result = result + src[lastGroup:matchEnd] // remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result + src[last:] // remaining
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
84
src/server/middleware/path/path_test.go
Normal file
84
src/server/middleware/path/path_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// 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 path
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_escape(t *testing.T) {
|
||||||
|
re := regexp.MustCompile(`/api/v2.0/projects/.*/repositories/(.*)/artifacts`)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
re *regexp.Regexp
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"/api/v2.0/projects/library/repositories/photon/artifacts",
|
||||||
|
args{re, "/api/v2.0/projects/library/repositories/photon/artifacts"},
|
||||||
|
"/api/v2.0/projects/library/repositories/photon/artifacts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/api/v2.0/projects/library/repositories/photon/hello-world/artifacts",
|
||||||
|
args{re, "/api/v2.0/projects/library/repositories/photon/hello-world/artifacts"},
|
||||||
|
"/api/v2.0/projects/library/repositories/photon%2Fhello-world/artifacts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/api/v2.0/projects/library/repositories/photon/hello-world/artifacts/digest/scan",
|
||||||
|
args{re, "/api/v2.0/projects/library/repositories/photon/hello-world/artifacts/digest/scan"},
|
||||||
|
"/api/v2.0/projects/library/repositories/photon%2Fhello-world/artifacts/digest/scan",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"/api/v2.0/projects/library/repositories",
|
||||||
|
args{re, "/api/v2.0/projects/library/repositories"},
|
||||||
|
"/api/v2.0/projects/library/repositories",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/api/v2.0/projects/library/repositories/hello/mariadb",
|
||||||
|
args{regexp.MustCompile(`^/api/v2.0/projects/.*/repositories/(.*)`), "/api/v2.0/projects/library/repositories/hello/mariadb"},
|
||||||
|
"/api/v2.0/projects/library/repositories/hello%2Fmariadb",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := escape(tt.args.re, tt.args.path); got != tt.want {
|
||||||
|
t.Errorf("escape() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEscapeMiddleware(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/api/v2.0/projects/library/repositories/hello/mariadb", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/v2.0/projects/library/repositories/hello%2Fmariadb" {
|
||||||
|
t.Errorf("escape middleware failed")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
EscapeMiddleware()(next).ServeHTTP(w, r)
|
||||||
|
}
|
@ -60,6 +60,14 @@ type artifactAPI struct {
|
|||||||
tagCtl tag.Controller
|
tagCtl tag.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *artifactAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
|
||||||
|
if err := unescapePathParams(params, "RepositoryName"); err != nil {
|
||||||
|
a.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListArtifactsParams) middleware.Responder {
|
func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListArtifactsParams) middleware.Responder {
|
||||||
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceArtifact); err != nil {
|
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceArtifact); err != nil {
|
||||||
return a.SendError(ctx, err)
|
return a.SendError(ctx, err)
|
||||||
|
@ -15,10 +15,12 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
serror "github.com/goharbor/harbor/src/server/error"
|
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
serror "github.com/goharbor/harbor/src/server/error"
|
||||||
|
"github.com/goharbor/harbor/src/server/middleware/path"
|
||||||
|
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New returns http handler for API V2.0
|
// New returns http handler for API V2.0
|
||||||
@ -34,7 +36,9 @@ func New() http.Handler {
|
|||||||
|
|
||||||
api.ServeError = serveError
|
api.ServeError = serveError
|
||||||
|
|
||||||
return h
|
// HACK: Use path.EscapeMiddleware to escape same patterns of the URL before the swagger handler
|
||||||
|
// eg /api/v2.0/projects/library/repositories/hello/world/artifacts to /api/v2.0/projects/library/repositories/hello%2Fworld/artifacts
|
||||||
|
return path.EscapeMiddleware()(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before executing operation handler, go-swagger will bind a parameters object to a request and validate the request,
|
// Before executing operation handler, go-swagger will bind a parameters object to a request and validate the request,
|
||||||
|
@ -17,6 +17,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-openapi/runtime/middleware"
|
"github.com/go-openapi/runtime/middleware"
|
||||||
"github.com/goharbor/harbor/src/api/artifact"
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
"github.com/goharbor/harbor/src/api/project"
|
"github.com/goharbor/harbor/src/api/project"
|
||||||
@ -44,6 +45,14 @@ type repositoryAPI struct {
|
|||||||
artCtl artifact.Controller
|
artCtl artifact.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *repositoryAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
|
||||||
|
if err := unescapePathParams(params, "RepositoryName"); err != nil {
|
||||||
|
r.SendError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *repositoryAPI) ListRepositories(ctx context.Context, params operation.ListRepositoriesParams) middleware.Responder {
|
func (r *repositoryAPI) ListRepositories(ctx context.Context, params operation.ListRepositoriesParams) middleware.Responder {
|
||||||
if err := r.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceRepository); err != nil {
|
if err := r.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionList, rbac.ResourceRepository); err != nil {
|
||||||
return r.SendError(ctx, err)
|
return r.SendError(ctx, err)
|
||||||
|
@ -17,10 +17,14 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/api/artifact"
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
|
||||||
"github.com/goharbor/harbor/src/api/scan"
|
"github.com/goharbor/harbor/src/api/scan"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
)
|
)
|
||||||
@ -68,3 +72,41 @@ func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Arti
|
|||||||
ContentType: "application/json",
|
ContentType: "application/json",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unescapePathParams(params interface{}, fieldNames ...string) error {
|
||||||
|
val := reflect.ValueOf(params)
|
||||||
|
if val.Kind() != reflect.Ptr {
|
||||||
|
return fmt.Errorf("params must be ptr")
|
||||||
|
}
|
||||||
|
|
||||||
|
val = val.Elem()
|
||||||
|
if val.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("params must be struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range fieldNames {
|
||||||
|
field := val.FieldByName(name)
|
||||||
|
if !field.IsValid() {
|
||||||
|
log.Warningf("field %s not found in params %v", name, params)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !field.CanSet() {
|
||||||
|
log.Warningf("field %s can not be changed in params %v", name, params)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Type().Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
v, err := url.PathUnescape(field.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetString(v)
|
||||||
|
default:
|
||||||
|
log.Warningf("field %s can not be unescaped in params %v", name, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
57
src/server/v2.0/handler/util_test.go
Normal file
57
src/server/v2.0/handler/util_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// 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 handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_unescapePathParams(t *testing.T) {
|
||||||
|
type Params struct {
|
||||||
|
ProjectName string
|
||||||
|
RepositoryName string
|
||||||
|
}
|
||||||
|
|
||||||
|
str := "params"
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
params interface{}
|
||||||
|
fieldNames []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"non ptr", args{str, []string{"RepositoryName"}}, true},
|
||||||
|
{"non struct", args{&str, []string{"RepositoryName"}}, true},
|
||||||
|
{"ptr of struct", args{&Params{}, []string{"RepositoryName"}}, false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := unescapePathParams(tt.args.params, tt.args.fieldNames...); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("unescapePathParams() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
params := Params{ProjectName: "library", RepositoryName: "hello%2Fworld"}
|
||||||
|
unescapePathParams(¶ms, "RepositoryName")
|
||||||
|
if params.RepositoryName != "hello/world" {
|
||||||
|
t.Errorf("unescapePathParams() not unescape RepositoryName field")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user