zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/requestcontext/user_access_control.go (about)

     1  package uac
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  
     7  	glob "github.com/bmatcuk/doublestar/v4" //nolint:gci
     8  
     9  	"zotregistry.dev/zot/errors"
    10  	"zotregistry.dev/zot/pkg/api/constants"
    11  )
    12  
    13  type Key int
    14  
    15  // request-local context key.
    16  var uacCtxKey = Key(0) //nolint: gochecknoglobals
    17  
    18  // pointer needed for use in context.WithValue.
    19  func GetContextKey() *Key {
    20  	return &uacCtxKey
    21  }
    22  
    23  type UserAccessControl struct {
    24  	authzInfo        *UserAuthzInfo
    25  	authnInfo        *UserAuthnInfo
    26  	methodActions    []string
    27  	behaviourActions []string
    28  }
    29  
    30  type UserAuthzInfo struct {
    31  	// {action: {repo: bool}}
    32  	globPatterns map[string]map[string]bool
    33  	isAdmin      bool
    34  }
    35  
    36  type UserAuthnInfo struct {
    37  	groups   []string
    38  	username string
    39  }
    40  
    41  func NewUserAccessControl() *UserAccessControl {
    42  	return &UserAccessControl{
    43  		// authzInfo will be populated in authz.go middleware
    44  		// if no authz enabled on server this will be nil
    45  		authzInfo: nil,
    46  		// authnInfo will be populated in authn.go middleware
    47  		// if no authn enabled on server this will be nil
    48  		authnInfo: nil,
    49  		// actions type
    50  		behaviourActions: []string{constants.DetectManifestCollisionPermission},
    51  		methodActions: []string{
    52  			constants.ReadPermission,
    53  			constants.CreatePermission,
    54  			constants.UpdatePermission,
    55  			constants.DeletePermission,
    56  		},
    57  	}
    58  }
    59  
    60  func (uac *UserAccessControl) SetUsername(username string) {
    61  	if uac.authnInfo == nil {
    62  		uac.authnInfo = &UserAuthnInfo{}
    63  	}
    64  
    65  	uac.authnInfo.username = username
    66  }
    67  
    68  func (uac *UserAccessControl) GetUsername() string {
    69  	if uac.authnInfo == nil {
    70  		return ""
    71  	}
    72  
    73  	return uac.authnInfo.username
    74  }
    75  
    76  func (uac *UserAccessControl) AddGroups(groups []string) {
    77  	if uac.authnInfo == nil {
    78  		uac.authnInfo = &UserAuthnInfo{
    79  			groups: []string{},
    80  		}
    81  	}
    82  
    83  	uac.authnInfo.groups = append(uac.authnInfo.groups, groups...)
    84  }
    85  
    86  func (uac *UserAccessControl) GetGroups() []string {
    87  	if uac.authnInfo == nil {
    88  		return []string{}
    89  	}
    90  
    91  	return uac.authnInfo.groups
    92  }
    93  
    94  func (uac *UserAccessControl) IsAnonymous() bool {
    95  	if uac.authnInfo == nil {
    96  		return true
    97  	}
    98  
    99  	return uac.authnInfo.username == ""
   100  }
   101  
   102  func (uac *UserAccessControl) IsAdmin() bool {
   103  	// if isAdmin was not set in authz.go then everybody is admin
   104  	if uac.authzInfo == nil {
   105  		return true
   106  	}
   107  
   108  	return uac.authzInfo.isAdmin
   109  }
   110  
   111  func (uac *UserAccessControl) SetIsAdmin(isAdmin bool) {
   112  	if uac.authzInfo == nil {
   113  		uac.authzInfo = &UserAuthzInfo{}
   114  	}
   115  
   116  	uac.authzInfo.isAdmin = isAdmin
   117  }
   118  
   119  /*
   120  	UserAcFromContext returns an UserAccessControl struct made available on all http requests
   121  	(using context.Context values) by authz and authn middlewares.
   122  
   123  	If no UserAccessControl is found on context, it will return an empty one.
   124  
   125  its methods and attributes can be used in http.Handlers to get user info for that specific request
   126  (username, groups, if it's an admin, if it can access certain resources).
   127  */
   128  func UserAcFromContext(ctx context.Context) (*UserAccessControl, error) {
   129  	if uacValue := ctx.Value(GetContextKey()); uacValue != nil {
   130  		uac, ok := uacValue.(UserAccessControl)
   131  		if !ok {
   132  			return nil, errors.ErrBadType
   133  		}
   134  
   135  		return &uac, nil
   136  	}
   137  
   138  	return NewUserAccessControl(), nil
   139  }
   140  
   141  func (uac *UserAccessControl) SetGlobPatterns(action string, patterns map[string]bool) {
   142  	if uac.authzInfo == nil {
   143  		uac.authzInfo = &UserAuthzInfo{
   144  			globPatterns: make(map[string]map[string]bool),
   145  		}
   146  	}
   147  
   148  	uac.authzInfo.globPatterns[action] = patterns
   149  }
   150  
   151  /*
   152  Can returns whether or not the user/anonymous who made the request has 'action' permission on 'repository'.
   153  */
   154  func (uac *UserAccessControl) Can(action, repository string) bool {
   155  	var defaultRet bool
   156  	if uac.isBehaviourAction(action) {
   157  		defaultRet = false
   158  	} else if uac.isMethodAction(action) {
   159  		defaultRet = true
   160  	}
   161  
   162  	if uac.IsAdmin() {
   163  		return defaultRet
   164  	}
   165  
   166  	// if glob patterns are not set then authz is not enabled, so everybody have access.
   167  	if !uac.areGlobPatternsSet() {
   168  		return defaultRet
   169  	}
   170  
   171  	return uac.matchesRepo(uac.authzInfo.globPatterns[action], repository)
   172  }
   173  
   174  func (uac *UserAccessControl) isBehaviourAction(action string) bool {
   175  	for _, behaviourAction := range uac.behaviourActions {
   176  		if action == behaviourAction {
   177  			return true
   178  		}
   179  	}
   180  
   181  	return false
   182  }
   183  
   184  func (uac *UserAccessControl) isMethodAction(action string) bool {
   185  	for _, methodAction := range uac.methodActions {
   186  		if action == methodAction {
   187  			return true
   188  		}
   189  	}
   190  
   191  	return false
   192  }
   193  
   194  // returns whether or not glob patterns have been set in authz.go.
   195  func (uac *UserAccessControl) areGlobPatternsSet() bool {
   196  	notSet := uac.authzInfo == nil || uac.authzInfo.globPatterns == nil
   197  
   198  	return !notSet
   199  }
   200  
   201  /*
   202  returns whether or not 'repository' can be found in the list of patterns
   203  on which the user who made the request has read permission on.
   204  */
   205  func (uac *UserAccessControl) matchesRepo(globPatterns map[string]bool, repository string) bool {
   206  	var longestMatchedPattern string
   207  
   208  	// because of the longest path matching rule, we need to check all patterns from config
   209  	for pattern := range globPatterns {
   210  		matched, err := glob.Match(pattern, repository)
   211  		if err == nil {
   212  			if matched && len(pattern) > len(longestMatchedPattern) {
   213  				longestMatchedPattern = pattern
   214  			}
   215  		}
   216  	}
   217  
   218  	allowed := globPatterns[longestMatchedPattern]
   219  
   220  	return allowed
   221  }
   222  
   223  /*
   224  	SaveOnRequest saves UserAccessControl on the request's context.
   225  
   226  Later UserAcFromContext(request.Context()) can be used to obtain UserAccessControl that was saved on it.
   227  */
   228  func (uac *UserAccessControl) SaveOnRequest(request *http.Request) {
   229  	uacContext := context.WithValue(request.Context(), GetContextKey(), *uac)
   230  
   231  	*request = *request.WithContext(uacContext)
   232  }
   233  
   234  /*
   235  	DeriveContext takes a context(parent) and returns a derived context(child) containing this UserAccessControl.
   236  
   237  Later UserAcFromContext(ctx context.Context) can be used to obtain the UserAccessControl that was added on it.
   238  */
   239  func (uac *UserAccessControl) DeriveContext(ctx context.Context) context.Context {
   240  	return context.WithValue(ctx, GetContextKey(), *uac)
   241  }
   242  
   243  func RepoIsUserAvailable(ctx context.Context, repoName string) (bool, error) {
   244  	uac, err := UserAcFromContext(ctx)
   245  	if err != nil {
   246  		return false, err
   247  	}
   248  
   249  	return uac.Can(constants.ReadPermission, repoName), nil
   250  }
   251  
   252  func CanDelete(ctx context.Context, repoName string) (bool, error) {
   253  	uac, err := UserAcFromContext(ctx)
   254  	if err != nil {
   255  		return false, err
   256  	}
   257  
   258  	return uac.Can(constants.DeletePermission, repoName), nil
   259  }