mirror of
https://github.com/project-zot/zot.git
synced 2025-04-08 02:54:41 -05:00
refactor: split AuthZ mdw in 2 different parts, each for a specific purpose (#1542)
- AuthzHandler has now been split in BaseAuthzHandler and DistSpecAuthzHandler The former populates context with user specific data needed in most handlers, while the latter executes access logic specific to distribution-spec handlers. Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
parent
7fee57e7cc
commit
62889c3cb1
3 changed files with 71 additions and 37 deletions
|
@ -2,15 +2,12 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
glob "github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
|
@ -220,12 +217,13 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st
|
|||
return result
|
||||
}
|
||||
|
||||
func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
vars := mux.Vars(request)
|
||||
resource := vars["name"]
|
||||
reference, ok := vars["reference"]
|
||||
/* NOTE:
|
||||
since we only do READ actions in extensions, this middleware is enough for them because
|
||||
it populates the context with user relevant data to be processed by each individual extension
|
||||
*/
|
||||
|
||||
if request.Method == http.MethodOptions {
|
||||
next.ServeHTTP(response, request)
|
||||
|
@ -286,17 +284,41 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
ctx := acCtrlr.getContext(acCtx, request)
|
||||
|
||||
/* Notes:
|
||||
- since we only do READ actions in extensions, we can bypass authz for them
|
||||
only need to know the username, whether the user is an admin, or what repos he can read.
|
||||
let each extension to apply authorization on them using localCtx.AccessControlContext{}
|
||||
*/
|
||||
if isExtensionURI(request.RequestURI) {
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(request)
|
||||
resource := vars["name"]
|
||||
reference, ok := vars["reference"]
|
||||
|
||||
acCtrlr := NewAccessController(ctlr.Config)
|
||||
|
||||
var identity string
|
||||
|
||||
var err error
|
||||
|
||||
// get acCtx built in authn and previous authz middlewares
|
||||
acCtx, err := localCtx.GetAccessControlContext(request.Context())
|
||||
if err != nil { // should never happen
|
||||
authFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// get username from context made in authn.go
|
||||
identity = acCtx.Username
|
||||
|
||||
var action string
|
||||
if request.Method == http.MethodGet || request.Method == http.MethodHead {
|
||||
action = Read
|
||||
|
@ -320,17 +342,12 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
action = Delete
|
||||
}
|
||||
|
||||
can := acCtrlr.can(ctx, identity, action, resource) //nolint:contextcheck
|
||||
can := acCtrlr.can(request.Context(), identity, action, resource) //nolint:contextcheck
|
||||
if !can {
|
||||
common.AuthzFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
} else {
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
next.ServeHTTP(response, request) //nolint:contextcheck
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isExtensionURI(requestURI string) bool {
|
||||
return strings.Contains(requestURI, constants.ExtPrefix) ||
|
||||
requestURI == fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,8 @@ func NewRouteHandler(c *Controller) *RouteHandler {
|
|||
func (rh *RouteHandler) SetupRoutes() {
|
||||
prefixedRouter := rh.c.Router.PathPrefix(constants.RoutePrefix).Subrouter()
|
||||
prefixedRouter.Use(AuthHandler(rh.c))
|
||||
|
||||
prefixedDistSpecRouter := prefixedRouter.NewRoute().Subrouter()
|
||||
// authz is being enabled if AccessControl is specified
|
||||
// if Authn is not present AccessControl will have only default policies
|
||||
if rh.c.Config.HTTP.AccessControl != nil && !isBearerAuthEnabled(rh.c.Config) {
|
||||
|
@ -66,41 +68,42 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
rh.c.Log.Info().Msg("default policy only access control is being enabled")
|
||||
}
|
||||
|
||||
prefixedRouter.Use(AuthzHandler(rh.c))
|
||||
prefixedRouter.Use(BaseAuthzHandler(rh.c))
|
||||
prefixedDistSpecRouter.Use(DistSpecAuthzHandler(rh.c))
|
||||
}
|
||||
|
||||
applyCORSHeaders := getCORSHeadersHandler(rh.c.Config.HTTP.AllowOrigin)
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#endpoints
|
||||
{
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", zreg.NameRegexp.String()),
|
||||
applyCORSHeaders(rh.ListTags)).Methods(zcommon.AllowedMethods("GET")...)
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
applyCORSHeaders(rh.CheckManifest)).Methods(zcommon.AllowedMethods("HEAD")...)
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
applyCORSHeaders(rh.GetManifest)).Methods(zcommon.AllowedMethods("GET")...)
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
rh.UpdateManifest).Methods("PUT")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
|
||||
rh.DeleteManifest).Methods("DELETE")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
|
||||
rh.CheckBlob).Methods("HEAD")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
|
||||
rh.GetBlob).Methods("GET")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
|
||||
rh.DeleteBlob).Methods("DELETE")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", zreg.NameRegexp.String()),
|
||||
rh.CreateBlobUpload).Methods("POST")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
rh.GetBlobUpload).Methods("GET")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
rh.PatchBlobUpload).Methods("PATCH")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
rh.UpdateBlobUpload).Methods("PUT")
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
|
||||
rh.DeleteBlobUpload).Methods("DELETE")
|
||||
// support for OCI artifact references
|
||||
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/referrers/{digest}", zreg.NameRegexp.String()),
|
||||
prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/referrers/{digest}", zreg.NameRegexp.String()),
|
||||
applyCORSHeaders(rh.GetReferrers)).Methods(zcommon.AllowedMethods("GET")...)
|
||||
prefixedRouter.HandleFunc(constants.ExtCatalogPrefix,
|
||||
applyCORSHeaders(rh.ListRepositories)).Methods(zcommon.AllowedMethods("GET")...)
|
||||
|
|
|
@ -20,6 +20,19 @@ package extensions
|
|||
- when adding a new tag, specify the new order in which multiple tags should be used (bottom of this page)
|
||||
|
||||
- for each and every new file that contains functions (functionalities) specific to an extension, one should create a corresponding file that <b>must contain the exact same functions, but no functionalities included</b>. This file must begin with an "anti-tag" (e.g. // +build !sync) which will include this file in binaries that don't include this extension ( in this example, the file won't be used in binaries that include sync extension ). See [extension-sync-disabled.go](extension-sync-disabled.go) for an example.
|
||||
- each extension is responsible with implementing authorization for newly added HTTP endpoints. zot will provide the necessary data, including user permissions, to the extension, but actual enforcement of these permissions is the responsibility of each extension. Each extension http.Handler has access to a context previously populated by <b>BaseAuthzHandler</b> with relevant user info. That info has the following structure:
|
||||
```
|
||||
type AccessControlContext struct {
|
||||
// read method action
|
||||
ReadGlobPatterns map[string]bool
|
||||
// detectManifestCollision behaviour action
|
||||
DmcGlobPatterns map[string]bool
|
||||
IsAdmin bool
|
||||
Username string
|
||||
Groups []string
|
||||
}
|
||||
```
|
||||
This data can then be accessed from the request context so that <b>every extension can apply its own authorization logic, if needed </b>.
|
||||
|
||||
- when a new extension comes out, the developer should also write some blackbox tests, where a binary that contains the new extension should be tested in a real usage scenario. See [test/blackbox](test/blackbox/sync.bats) folder for multiple extensions examples.
|
||||
|
||||
|
@ -28,4 +41,5 @@ package extensions
|
|||
- with every new extension, you should modify the EXTENSIONS variable in Makefile by adding the new extension. The EXTENSIONS variable represents all extensions and is used in Make targets that require them all (e.g make test).
|
||||
|
||||
- the available extensions that can be used at the moment are: <b>sync, scrub, metrics, search </b>.
|
||||
NOTE: When multiple extensions are used, they should be enlisted in the above presented order.
|
||||
NOTE: When multiple extensions are used, they should be listed in the above presented order.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue