mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
graphql: Apply authorization on /_search endpoint
- AccessControlContext now resides in a separate package from where it can be imported, along with the contextKey that will be used to set and retrieve this context value. - AccessControlContext has a new field called Username, that will be of use for future implementations in graphQL resolvers. - GlobalSearch resolver now uses this context to filter repos available to the logged user. - moved logic for uploading images in tests so that it can be used in every package - tests were added for multiple request scenarios, when zot-server requires authz on specific repos - added tests with injected errors for extended coverage - added tests for status code error injection utilities Closes https://github.com/project-zot/zot/issues/615 Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
+18
-17
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
glob "github.com/bmatcuk/doublestar/v4"
|
||||
@@ -12,19 +13,15 @@ import (
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
// actions.
|
||||
CREATE = "create"
|
||||
READ = "read"
|
||||
UPDATE = "update"
|
||||
DELETE = "delete"
|
||||
|
||||
// request-local context key.
|
||||
authzCtxKey contextKey = 0
|
||||
)
|
||||
|
||||
// AccessController authorizes users to act on resources.
|
||||
@@ -33,12 +30,6 @@ type AccessController struct {
|
||||
Log log.Logger
|
||||
}
|
||||
|
||||
// AccessControlContext context passed down to http.Handlers.
|
||||
type AccessControlContext struct {
|
||||
globPatterns map[string]bool
|
||||
isAdmin bool
|
||||
}
|
||||
|
||||
func NewAccessController(config *config.Config) *AccessController {
|
||||
return &AccessController{
|
||||
Config: config.AccessControl,
|
||||
@@ -111,14 +102,18 @@ func (ac *AccessController) isAdmin(username string) bool {
|
||||
// getContext builds ac context(allowed to read repos and if user is admin) and returns it.
|
||||
func (ac *AccessController) getContext(username string, request *http.Request) context.Context {
|
||||
readGlobPatterns := ac.getReadGlobPatterns(username)
|
||||
acCtx := AccessControlContext{globPatterns: readGlobPatterns}
|
||||
|
||||
if ac.isAdmin(username) {
|
||||
acCtx.isAdmin = true
|
||||
} else {
|
||||
acCtx.isAdmin = false
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
GlobPatterns: readGlobPatterns,
|
||||
Username: username,
|
||||
}
|
||||
|
||||
if ac.isAdmin(username) {
|
||||
acCtx.IsAdmin = true
|
||||
} else {
|
||||
acCtx.IsAdmin = false
|
||||
}
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(request.Context(), authzCtxKey, acCtx)
|
||||
|
||||
return ctx
|
||||
@@ -227,6 +222,12 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(request.RequestURI, constants.ExtSearchPrefix) {
|
||||
next.ServeHTTP(response, request.WithContext(ctx))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var action string
|
||||
if request.Method == http.MethodGet || request.Method == http.MethodHead {
|
||||
action = READ
|
||||
|
||||
+158
-2
@@ -317,7 +317,6 @@ func TestHtpasswdTwoCreds(t *testing.T) {
|
||||
}
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
test.WaitTillServerReady(baseURL)
|
||||
@@ -5701,7 +5700,7 @@ func TestInjectTooManyOpenFiles(t *testing.T) {
|
||||
|
||||
func TestPeriodicGC(t *testing.T) {
|
||||
Convey("Periodic gc enabled for default store", t, func() {
|
||||
repoName := "test"
|
||||
repoName := "testRepo"
|
||||
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
@@ -5858,6 +5857,163 @@ func TestPeriodicTasks(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchRoutes(t *testing.T) {
|
||||
Convey("Upload image for test", t, func(c C) {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
tempDir := t.TempDir()
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = tempDir
|
||||
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
repoName := "testrepo"
|
||||
inaccessibleRepo := "inaccessible"
|
||||
cfg, layers, manifest, err := test.GetImageComponents(10000)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = test.UploadImage(
|
||||
test.Image{
|
||||
Config: cfg,
|
||||
Layers: layers,
|
||||
Manifest: manifest,
|
||||
Tag: "latest",
|
||||
}, baseURL, repoName)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// data for the inaccessible repo
|
||||
cfg, layers, manifest, err = test.GetImageComponents(10000)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = test.UploadImage(
|
||||
test.Image{
|
||||
Config: cfg,
|
||||
Layers: layers,
|
||||
Manifest: manifest,
|
||||
Tag: "latest",
|
||||
}, baseURL, inaccessibleRepo)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("GlobalSearch with authz enabled", func(c C) {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
user1 := "test"
|
||||
password1 := "test"
|
||||
testString1 := getCredString(user1, password1)
|
||||
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
||||
defer os.Remove(htpasswdPath)
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
},
|
||||
}
|
||||
|
||||
conf.HTTP.Port = port
|
||||
|
||||
defaultVal := true
|
||||
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
Enable: &defaultVal,
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
conf.AccessControl = &config.AccessControlConfig{
|
||||
Repositories: config.Repositories{
|
||||
repoName: config.PolicyGroup{
|
||||
Policies: []config.Policy{
|
||||
{
|
||||
Users: []string{user1},
|
||||
Actions: []string{"read"},
|
||||
},
|
||||
},
|
||||
DefaultPolicy: []string{},
|
||||
},
|
||||
inaccessibleRepo: config.PolicyGroup{
|
||||
Policies: []config.Policy{
|
||||
{
|
||||
Users: []string{},
|
||||
Actions: []string{},
|
||||
},
|
||||
},
|
||||
DefaultPolicy: []string{},
|
||||
},
|
||||
},
|
||||
AdminPolicy: config.Policy{
|
||||
Users: []string{},
|
||||
Actions: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = tempDir
|
||||
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
query := `
|
||||
{
|
||||
GlobalSearch(query:""){
|
||||
Repos {
|
||||
Name
|
||||
Score
|
||||
NewestImage {
|
||||
RepoName
|
||||
Tag
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query=" + url.QueryEscape(query))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
So(string(resp.Body()), ShouldContainSubstring, repoName)
|
||||
So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
|
||||
|
||||
resp, err = resty.R().Get(baseURL + constants.ExtSearchPrefix + "?query=" + url.QueryEscape(query))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
// credentials for user unauthorized to access repo
|
||||
user2 := "notWorking"
|
||||
password2 := "notWorking"
|
||||
testString2 := getCredString(user2, password2)
|
||||
htpasswdPath2 := test.MakeHtpasswdFileFromString(testString2)
|
||||
defer os.Remove(htpasswdPath2)
|
||||
|
||||
ctlr.Config.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath2,
|
||||
},
|
||||
}
|
||||
// authenticated, but no access to resource
|
||||
resp, err = resty.R().SetBasicAuth(user2, password2).Get(baseURL + constants.ExtSearchPrefix +
|
||||
"?query=" + url.QueryEscape(query))
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDistSpecExtensions(t *testing.T) {
|
||||
Convey("start zot server with search extension", t, func(c C) {
|
||||
conf := config.New()
|
||||
|
||||
+5
-2
@@ -34,6 +34,7 @@ import (
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
ext "zotregistry.io/zot/pkg/extensions"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
"zotregistry.io/zot/pkg/test" // nolint:goimports
|
||||
// as required by swaggo.
|
||||
@@ -1240,9 +1241,11 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
|
||||
}
|
||||
|
||||
var repos []string
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
// get passed context from authzHandler and filter out repos based on permissions
|
||||
if authCtx := request.Context().Value(authzCtxKey); authCtx != nil {
|
||||
acCtx, ok := authCtx.(AccessControlContext)
|
||||
acCtx, ok := authCtx.(localCtx.AccessControlContext)
|
||||
if !ok {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -1250,7 +1253,7 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
|
||||
}
|
||||
|
||||
for _, r := range combineRepoList {
|
||||
if acCtx.isAdmin || matchesRepo(acCtx.globPatterns, r) {
|
||||
if acCtx.IsAdmin || matchesRepo(acCtx.GlobPatterns, r) {
|
||||
repos = append(repos, r)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user