mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
refactor(authz): use a struct for user access control info operations (#1682)
fix(authz): fix isAdmin not using groups to determine if a user is admin. fix(authz): return 401 instead of 403 403 is correct as per HTTP spec However authz is not part of dist-spec and clients know only about 401 So this is a compromise. Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"zotregistry.io/zot/errors"
|
||||
)
|
||||
|
||||
// request-local context key.
|
||||
var amwCtxKey = Key(1) //nolint: gochecknoglobals
|
||||
|
||||
// pointer needed for use in context.WithValue.
|
||||
func GetAuthnMiddlewareCtxKey() *Key {
|
||||
return &amwCtxKey
|
||||
}
|
||||
|
||||
type AuthnMiddlewareContext struct {
|
||||
AuthnType string
|
||||
}
|
||||
|
||||
func GetAuthnMiddlewareContext(ctx context.Context) (*AuthnMiddlewareContext, error) {
|
||||
authnMiddlewareCtxKey := GetAuthnMiddlewareCtxKey()
|
||||
if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil {
|
||||
amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext)
|
||||
if !ok {
|
||||
return nil, errors.ErrBadType
|
||||
}
|
||||
|
||||
return &amCtx, nil
|
||||
}
|
||||
|
||||
return nil, nil //nolint: nilnil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package requestcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
)
|
||||
|
||||
func RepoIsUserAvailable(ctx context.Context, repoName string) (bool, error) {
|
||||
authzCtxKey := GetContextKey()
|
||||
|
||||
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
|
||||
acCtx, ok := authCtx.(AccessControlContext)
|
||||
if !ok {
|
||||
err := zerr.ErrBadCtxFormat
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
if acCtx.IsAdmin || acCtx.CanReadRepo(repoName) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func GetUsernameFromContext(ctx *AccessControlContext) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ctx.Username
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package requestcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
glob "github.com/bmatcuk/doublestar/v4" //nolint:gci
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
)
|
||||
|
||||
type Key int
|
||||
|
||||
// request-local context key.
|
||||
var authzCtxKey = Key(0) //nolint: gochecknoglobals
|
||||
|
||||
// pointer needed for use in context.WithValue.
|
||||
func GetContextKey() *Key {
|
||||
return &authzCtxKey
|
||||
}
|
||||
|
||||
// AccessControlContext - contains user authn/authz information.
|
||||
type AccessControlContext struct {
|
||||
// read method action
|
||||
ReadGlobPatterns map[string]bool
|
||||
// detectManifestCollision behaviour action
|
||||
DmcGlobPatterns map[string]bool
|
||||
IsAdmin bool
|
||||
Username string
|
||||
Groups []string
|
||||
}
|
||||
|
||||
/*
|
||||
GetAccessControlContext returns an AccessControlContext struct made available on all http requests
|
||||
(using context.Context values) by authz and authn middlewares.
|
||||
|
||||
its methods and attributes can be used in http.Handlers to get user info for that specific request
|
||||
(username, groups, if it's an admin, if it can access certain resources).
|
||||
*/
|
||||
func GetAccessControlContext(ctx context.Context) (*AccessControlContext, error) {
|
||||
authzCtxKey := GetContextKey()
|
||||
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
|
||||
acCtx, ok := authCtx.(AccessControlContext)
|
||||
if !ok {
|
||||
return nil, zerr.ErrBadType
|
||||
}
|
||||
|
||||
return &acCtx, nil
|
||||
}
|
||||
|
||||
return nil, nil //nolint: nilnil
|
||||
}
|
||||
|
||||
// returns whether or not the user/anonymous who made the request has read permission on 'repository'.
|
||||
func (acCtx *AccessControlContext) CanReadRepo(repository string) bool {
|
||||
if acCtx.ReadGlobPatterns != nil {
|
||||
return acCtx.matchesRepo(acCtx.ReadGlobPatterns, repository)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
returns whether or not the user/anonymous who made the request
|
||||
has detectManifestCollision permission on 'repository'.
|
||||
*/
|
||||
func (acCtx *AccessControlContext) CanDetectManifestCollision(repository string) bool {
|
||||
if acCtx.DmcGlobPatterns != nil {
|
||||
return acCtx.matchesRepo(acCtx.DmcGlobPatterns, repository)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
returns whether or not 'repository' can be found in the list of patterns
|
||||
on which the user who made the request has read permission on.
|
||||
*/
|
||||
func (acCtx *AccessControlContext) matchesRepo(globPatterns map[string]bool, repository string) bool {
|
||||
var longestMatchedPattern string
|
||||
|
||||
// because of the longest path matching rule, we need to check all patterns from config
|
||||
for pattern := range globPatterns {
|
||||
matched, err := glob.Match(pattern, repository)
|
||||
if err == nil {
|
||||
if matched && len(pattern) > len(longestMatchedPattern) {
|
||||
longestMatchedPattern = pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allowed := globPatterns[longestMatchedPattern]
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
// request-local context key.
|
||||
var amwCtxKey = Key(1) //nolint: gochecknoglobals
|
||||
|
||||
// pointer needed for use in context.WithValue.
|
||||
func GetAuthnMiddlewareCtxKey() *Key {
|
||||
return &amwCtxKey
|
||||
}
|
||||
|
||||
type AuthnMiddlewareContext struct {
|
||||
AuthnType string
|
||||
}
|
||||
|
||||
func GetAuthnMiddlewareContext(ctx context.Context) (*AuthnMiddlewareContext, error) {
|
||||
authnMiddlewareCtxKey := GetAuthnMiddlewareCtxKey()
|
||||
if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil {
|
||||
amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext)
|
||||
if !ok {
|
||||
return nil, zerr.ErrBadType
|
||||
}
|
||||
|
||||
return &amCtx, nil
|
||||
}
|
||||
|
||||
return nil, nil //nolint: nilnil
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package uac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
glob "github.com/bmatcuk/doublestar/v4" //nolint:gci
|
||||
|
||||
"zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
)
|
||||
|
||||
type Key int
|
||||
|
||||
// request-local context key.
|
||||
var uacCtxKey = Key(0) //nolint: gochecknoglobals
|
||||
|
||||
// pointer needed for use in context.WithValue.
|
||||
func GetContextKey() *Key {
|
||||
return &uacCtxKey
|
||||
}
|
||||
|
||||
type UserAccessControl struct {
|
||||
authzInfo *UserAuthzInfo
|
||||
authnInfo *UserAuthnInfo
|
||||
methodActions []string
|
||||
behaviourActions []string
|
||||
}
|
||||
|
||||
type UserAuthzInfo struct {
|
||||
// {action: {repo: bool}}
|
||||
globPatterns map[string]map[string]bool
|
||||
isAdmin bool
|
||||
}
|
||||
|
||||
type UserAuthnInfo struct {
|
||||
groups []string
|
||||
username string
|
||||
}
|
||||
|
||||
func NewUserAccessControl() *UserAccessControl {
|
||||
return &UserAccessControl{
|
||||
// authzInfo will be populated in authz.go middleware
|
||||
// if no authz enabled on server this will be nil
|
||||
authzInfo: nil,
|
||||
// authnInfo will be populated in authn.go middleware
|
||||
// if no authn enabled on server this will be nil
|
||||
authnInfo: nil,
|
||||
// actions type
|
||||
behaviourActions: []string{constants.DetectManifestCollisionPermission},
|
||||
methodActions: []string{
|
||||
constants.ReadPermission,
|
||||
constants.CreatePermission,
|
||||
constants.UpdatePermission,
|
||||
constants.DeletePermission,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) SetUsername(username string) {
|
||||
if uac.authnInfo == nil {
|
||||
uac.authnInfo = &UserAuthnInfo{}
|
||||
}
|
||||
|
||||
uac.authnInfo.username = username
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) GetUsername() string {
|
||||
if uac.authnInfo == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return uac.authnInfo.username
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) AddGroups(groups []string) {
|
||||
if uac.authnInfo == nil {
|
||||
uac.authnInfo = &UserAuthnInfo{
|
||||
groups: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
uac.authnInfo.groups = append(uac.authnInfo.groups, groups...)
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) GetGroups() []string {
|
||||
if uac.authnInfo == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return uac.authnInfo.groups
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) IsAnonymous() bool {
|
||||
if uac.authnInfo == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return uac.authnInfo.username == ""
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) IsAdmin() bool {
|
||||
// if isAdmin was not set in authz.go then everybody is admin
|
||||
if uac.authzInfo == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return uac.authzInfo.isAdmin
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) SetIsAdmin(isAdmin bool) {
|
||||
if uac.authzInfo == nil {
|
||||
uac.authzInfo = &UserAuthzInfo{}
|
||||
}
|
||||
|
||||
uac.authzInfo.isAdmin = isAdmin
|
||||
}
|
||||
|
||||
/*
|
||||
UserAcFromContext returns an UserAccessControl struct made available on all http requests
|
||||
(using context.Context values) by authz and authn middlewares.
|
||||
|
||||
If no UserAccessControl is found on context, it will return an empty one.
|
||||
|
||||
its methods and attributes can be used in http.Handlers to get user info for that specific request
|
||||
(username, groups, if it's an admin, if it can access certain resources).
|
||||
*/
|
||||
func UserAcFromContext(ctx context.Context) (*UserAccessControl, error) {
|
||||
if uacValue := ctx.Value(GetContextKey()); uacValue != nil {
|
||||
uac, ok := uacValue.(UserAccessControl)
|
||||
if !ok {
|
||||
return nil, errors.ErrBadType
|
||||
}
|
||||
|
||||
return &uac, nil
|
||||
}
|
||||
|
||||
return NewUserAccessControl(), nil
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) SetGlobPatterns(action string, patterns map[string]bool) {
|
||||
if uac.authzInfo == nil {
|
||||
uac.authzInfo = &UserAuthzInfo{
|
||||
globPatterns: make(map[string]map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
uac.authzInfo.globPatterns[action] = patterns
|
||||
}
|
||||
|
||||
/*
|
||||
Can returns whether or not the user/anonymous who made the request has 'action' permission on 'repository'.
|
||||
*/
|
||||
func (uac *UserAccessControl) Can(action, repository string) bool {
|
||||
var defaultRet bool
|
||||
if uac.isBehaviourAction(action) {
|
||||
defaultRet = false
|
||||
} else if uac.isMethodAction(action) {
|
||||
defaultRet = true
|
||||
}
|
||||
|
||||
if uac.IsAdmin() {
|
||||
return defaultRet
|
||||
}
|
||||
|
||||
// if glob patterns are not set then authz is not enabled, so everybody have access.
|
||||
if !uac.areGlobPatternsSet() {
|
||||
return defaultRet
|
||||
}
|
||||
|
||||
return uac.matchesRepo(uac.authzInfo.globPatterns[action], repository)
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) isBehaviourAction(action string) bool {
|
||||
for _, behaviourAction := range uac.behaviourActions {
|
||||
if action == behaviourAction {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (uac *UserAccessControl) isMethodAction(action string) bool {
|
||||
for _, methodAction := range uac.methodActions {
|
||||
if action == methodAction {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// returns whether or not glob patterns have been set in authz.go.
|
||||
func (uac *UserAccessControl) areGlobPatternsSet() bool {
|
||||
notSet := uac.authzInfo == nil || uac.authzInfo.globPatterns == nil
|
||||
|
||||
return !notSet
|
||||
}
|
||||
|
||||
/*
|
||||
returns whether or not 'repository' can be found in the list of patterns
|
||||
on which the user who made the request has read permission on.
|
||||
*/
|
||||
func (uac *UserAccessControl) matchesRepo(globPatterns map[string]bool, repository string) bool {
|
||||
var longestMatchedPattern string
|
||||
|
||||
// because of the longest path matching rule, we need to check all patterns from config
|
||||
for pattern := range globPatterns {
|
||||
matched, err := glob.Match(pattern, repository)
|
||||
if err == nil {
|
||||
if matched && len(pattern) > len(longestMatchedPattern) {
|
||||
longestMatchedPattern = pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allowed := globPatterns[longestMatchedPattern]
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
||||
/*
|
||||
SaveOnRequest saves UserAccessControl on the request's context.
|
||||
|
||||
Later UserAcFromContext(request.Context()) can be used to obtain UserAccessControl that was saved on it.
|
||||
*/
|
||||
func (uac *UserAccessControl) SaveOnRequest(request *http.Request) {
|
||||
uacContext := context.WithValue(request.Context(), GetContextKey(), *uac)
|
||||
|
||||
*request = *request.WithContext(uacContext)
|
||||
}
|
||||
|
||||
/*
|
||||
DeriveContext takes a context(parent) and returns a derived context(child) containing this UserAccessControl.
|
||||
|
||||
Later UserAcFromContext(ctx context.Context) can be used to obtain the UserAccessControl that was added on it.
|
||||
*/
|
||||
func (uac *UserAccessControl) DeriveContext(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, GetContextKey(), *uac)
|
||||
}
|
||||
|
||||
func RepoIsUserAvailable(ctx context.Context, repoName string) (bool, error) {
|
||||
uac, err := UserAcFromContext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// no authn/authz enabled on server
|
||||
if uac == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return uac.Can("read", repoName), nil
|
||||
}
|
||||
Reference in New Issue
Block a user