zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/api/authz.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  
     7  	glob "github.com/bmatcuk/doublestar/v4"
     8  	"github.com/gorilla/mux"
     9  
    10  	"zotregistry.io/zot/pkg/api/config"
    11  	"zotregistry.io/zot/pkg/api/constants"
    12  	"zotregistry.io/zot/pkg/common"
    13  	"zotregistry.io/zot/pkg/log"
    14  	reqCtx "zotregistry.io/zot/pkg/requestcontext"
    15  )
    16  
    17  const (
    18  	BASIC  = "Basic"
    19  	BEARER = "Bearer"
    20  	OPENID = "OpenID"
    21  )
    22  
    23  // AccessController authorizes users to act on resources.
    24  type AccessController struct {
    25  	Config *config.AccessControlConfig
    26  	Log    log.Logger
    27  }
    28  
    29  func NewAccessController(conf *config.Config) *AccessController {
    30  	if conf.HTTP.AccessControl == nil {
    31  		return &AccessController{
    32  			Config: &config.AccessControlConfig{},
    33  			Log:    log.NewLogger(conf.Log.Level, conf.Log.Output),
    34  		}
    35  	}
    36  
    37  	return &AccessController{
    38  		Config: conf.HTTP.AccessControl,
    39  		Log:    log.NewLogger(conf.Log.Level, conf.Log.Output),
    40  	}
    41  }
    42  
    43  // getGlobPatterns gets glob patterns from authz config on which <username> has <action> perms.
    44  // used to filter /v2/_catalog repositories based on user rights.
    45  func (ac *AccessController) getGlobPatterns(username string, groups []string, action string) map[string]bool {
    46  	globPatterns := make(map[string]bool)
    47  
    48  	for pattern, policyGroup := range ac.Config.Repositories {
    49  		if username == "" {
    50  			// check anonymous policy
    51  			if common.Contains(policyGroup.AnonymousPolicy, action) {
    52  				globPatterns[pattern] = true
    53  			}
    54  		} else {
    55  			// check default policy (authenticated user)
    56  			if common.Contains(policyGroup.DefaultPolicy, action) {
    57  				globPatterns[pattern] = true
    58  			}
    59  		}
    60  
    61  		// check user based policy
    62  		for _, p := range policyGroup.Policies {
    63  			if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
    64  				globPatterns[pattern] = true
    65  			}
    66  		}
    67  
    68  		// check group based policy
    69  		for _, group := range groups {
    70  			for _, p := range policyGroup.Policies {
    71  				if common.Contains(p.Groups, group) && common.Contains(p.Actions, action) {
    72  					globPatterns[pattern] = true
    73  				}
    74  			}
    75  		}
    76  
    77  		// if not allowed then mark it
    78  		if _, ok := globPatterns[pattern]; !ok {
    79  			globPatterns[pattern] = false
    80  		}
    81  	}
    82  
    83  	return globPatterns
    84  }
    85  
    86  // can verifies if a user can do action on repository.
    87  func (ac *AccessController) can(userAc *reqCtx.UserAccessControl, action, repository string) bool {
    88  	can := false
    89  
    90  	var longestMatchedPattern string
    91  
    92  	for pattern := range ac.Config.Repositories {
    93  		matched, err := glob.Match(pattern, repository)
    94  		if err == nil {
    95  			if matched && len(pattern) > len(longestMatchedPattern) {
    96  				longestMatchedPattern = pattern
    97  			}
    98  		}
    99  	}
   100  
   101  	userGroups := userAc.GetGroups()
   102  	username := userAc.GetUsername()
   103  
   104  	// check matched repo based policy
   105  	pg, ok := ac.Config.Repositories[longestMatchedPattern]
   106  	if ok {
   107  		can = ac.isPermitted(userGroups, username, action, pg)
   108  	}
   109  
   110  	// check admins based policy
   111  	if !can {
   112  		if ac.isAdmin(username, userGroups) && common.Contains(ac.Config.AdminPolicy.Actions, action) {
   113  			can = true
   114  		}
   115  	}
   116  
   117  	return can
   118  }
   119  
   120  // isAdmin .
   121  func (ac *AccessController) isAdmin(username string, userGroups []string) bool {
   122  	if common.Contains(ac.Config.AdminPolicy.Users, username) || ac.isAnyGroupInAdminPolicy(userGroups) {
   123  		return true
   124  	}
   125  
   126  	return false
   127  }
   128  
   129  func (ac *AccessController) isAnyGroupInAdminPolicy(userGroups []string) bool {
   130  	for _, group := range userGroups {
   131  		if common.Contains(ac.Config.AdminPolicy.Groups, group) {
   132  			return true
   133  		}
   134  	}
   135  
   136  	return false
   137  }
   138  
   139  func (ac *AccessController) getUserGroups(username string) []string {
   140  	var groupNames []string
   141  
   142  	for groupName, group := range ac.Config.Groups {
   143  		for _, user := range group.Users {
   144  			// find if the user is part of any groups
   145  			if user == username {
   146  				groupNames = append(groupNames, groupName)
   147  			}
   148  		}
   149  	}
   150  
   151  	return groupNames
   152  }
   153  
   154  // getContext updates an UserAccessControl with admin status and specific permissions on repos.
   155  func (ac *AccessController) updateUserAccessControl(userAc *reqCtx.UserAccessControl) {
   156  	identity := userAc.GetUsername()
   157  	groups := userAc.GetGroups()
   158  
   159  	readGlobPatterns := ac.getGlobPatterns(identity, groups, constants.ReadPermission)
   160  	createGlobPatterns := ac.getGlobPatterns(identity, groups, constants.CreatePermission)
   161  	updateGlobPatterns := ac.getGlobPatterns(identity, groups, constants.UpdatePermission)
   162  	deleteGlobPatterns := ac.getGlobPatterns(identity, groups, constants.DeletePermission)
   163  	dmcGlobPatterns := ac.getGlobPatterns(identity, groups, constants.DetectManifestCollisionPermission)
   164  
   165  	userAc.SetGlobPatterns(constants.ReadPermission, readGlobPatterns)
   166  	userAc.SetGlobPatterns(constants.CreatePermission, createGlobPatterns)
   167  	userAc.SetGlobPatterns(constants.UpdatePermission, updateGlobPatterns)
   168  	userAc.SetGlobPatterns(constants.DeletePermission, deleteGlobPatterns)
   169  	userAc.SetGlobPatterns(constants.DetectManifestCollisionPermission, dmcGlobPatterns)
   170  
   171  	if ac.isAdmin(userAc.GetUsername(), userAc.GetGroups()) {
   172  		userAc.SetIsAdmin(true)
   173  	} else {
   174  		userAc.SetIsAdmin(false)
   175  	}
   176  }
   177  
   178  // getAuthnMiddlewareContext builds ac context(allowed to read repos and if user is admin) and returns it.
   179  func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request *http.Request) context.Context {
   180  	amwCtx := reqCtx.AuthnMiddlewareContext{
   181  		AuthnType: authnType,
   182  	}
   183  
   184  	amwCtxKey := reqCtx.GetAuthnMiddlewareCtxKey()
   185  	ctx := context.WithValue(request.Context(), amwCtxKey, amwCtx)
   186  
   187  	return ctx
   188  }
   189  
   190  // isPermitted returns true if username can do action on a repository policy.
   191  func (ac *AccessController) isPermitted(userGroups []string, username, action string,
   192  	policyGroup config.PolicyGroup,
   193  ) bool {
   194  	// check repo/system based policies
   195  	for _, p := range policyGroup.Policies {
   196  		if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
   197  			return true
   198  		}
   199  	}
   200  
   201  	if userGroups != nil {
   202  		for _, p := range policyGroup.Policies {
   203  			if common.Contains(p.Actions, action) {
   204  				for _, group := range p.Groups {
   205  					if common.Contains(userGroups, group) {
   206  						return true
   207  					}
   208  				}
   209  			}
   210  		}
   211  	}
   212  
   213  	// check defaultPolicy
   214  	if common.Contains(policyGroup.DefaultPolicy, action) && username != "" {
   215  		return true
   216  	}
   217  
   218  	// check anonymousPolicy
   219  	if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" {
   220  		return true
   221  	}
   222  
   223  	return false
   224  }
   225  
   226  func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
   227  	return func(next http.Handler) http.Handler {
   228  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
   229  			/* NOTE:
   230  			since we only do READ actions in extensions, this middleware is enough for them because
   231  			it populates the context with user relevant data to be processed by each individual extension
   232  			*/
   233  
   234  			if request.Method == http.MethodOptions {
   235  				next.ServeHTTP(response, request)
   236  
   237  				return
   238  			}
   239  
   240  			// request comes from bearer authn, bypass it
   241  			authnMwCtx, err := reqCtx.GetAuthnMiddlewareContext(request.Context())
   242  			if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
   243  				next.ServeHTTP(response, request)
   244  
   245  				return
   246  			}
   247  
   248  			// bypass authz for /v2/ route
   249  			if request.RequestURI == "/v2/" {
   250  				next.ServeHTTP(response, request)
   251  
   252  				return
   253  			}
   254  
   255  			aCtlr := NewAccessController(ctlr.Config)
   256  
   257  			// get access control context made in authn.go
   258  			userAc, err := reqCtx.UserAcFromContext(request.Context())
   259  			if err != nil { // should never happen
   260  				authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
   261  
   262  				return
   263  			}
   264  
   265  			aCtlr.updateUserAccessControl(userAc)
   266  			userAc.SaveOnRequest(request)
   267  
   268  			next.ServeHTTP(response, request) //nolint:contextcheck
   269  		})
   270  	}
   271  }
   272  
   273  func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
   274  	return func(next http.Handler) http.Handler {
   275  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
   276  			if request.Method == http.MethodOptions {
   277  				next.ServeHTTP(response, request)
   278  
   279  				return
   280  			}
   281  
   282  			// request comes from bearer authn, bypass it
   283  			authnMwCtx, err := reqCtx.GetAuthnMiddlewareContext(request.Context())
   284  			if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
   285  				next.ServeHTTP(response, request)
   286  
   287  				return
   288  			}
   289  
   290  			vars := mux.Vars(request)
   291  			resource := vars["name"]
   292  			reference, ok := vars["reference"]
   293  
   294  			acCtrlr := NewAccessController(ctlr.Config)
   295  
   296  			// get userAc built in authn and previous authz middlewares
   297  			userAc, err := reqCtx.UserAcFromContext(request.Context())
   298  			if err != nil { // should never happen
   299  				authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
   300  
   301  				return
   302  			}
   303  
   304  			var action string
   305  			if request.Method == http.MethodGet || request.Method == http.MethodHead {
   306  				action = constants.ReadPermission
   307  			}
   308  
   309  			if request.Method == http.MethodPut || request.Method == http.MethodPatch || request.Method == http.MethodPost {
   310  				// assume user wants to create
   311  				action = constants.CreatePermission
   312  				// if we get a reference (tag)
   313  				if ok {
   314  					is := ctlr.StoreController.GetImageStore(resource)
   315  					tags, err := is.GetImageTags(resource)
   316  					// if repo exists and request's tag exists then action is UPDATE
   317  					if err == nil && common.Contains(tags, reference) && reference != "latest" {
   318  						action = constants.UpdatePermission
   319  					}
   320  				}
   321  			}
   322  
   323  			if request.Method == http.MethodDelete {
   324  				action = constants.DeletePermission
   325  			}
   326  
   327  			can := acCtrlr.can(userAc, action, resource) //nolint:contextcheck
   328  			if !can {
   329  				common.AuthzFail(response, request, userAc.GetUsername(), ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
   330  			} else {
   331  				next.ServeHTTP(response, request) //nolint:contextcheck
   332  			}
   333  		})
   334  	}
   335  }
   336  
   337  func MetricsAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
   338  	return func(next http.Handler) http.Handler {
   339  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
   340  			if ctlr.Config.HTTP.AccessControl == nil {
   341  				// allow access to authenticated user as anonymous policy does not exist
   342  				next.ServeHTTP(response, request)
   343  
   344  				return
   345  			}
   346  			if len(ctlr.Config.HTTP.AccessControl.Metrics.Users) == 0 {
   347  				log := ctlr.Log
   348  				log.Warn().Msg("auth is enabled but no metrics users in accessControl: /metrics is unaccesible")
   349  				common.AuthzFail(response, request, "", ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
   350  
   351  				return
   352  			}
   353  
   354  			// get access control context made in authn.go
   355  			userAc, err := reqCtx.UserAcFromContext(request.Context())
   356  			if err != nil { // should never happen
   357  				common.AuthzFail(response, request, "", ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
   358  
   359  				return
   360  			}
   361  
   362  			username := userAc.GetUsername()
   363  			if !common.Contains(ctlr.Config.HTTP.AccessControl.Metrics.Users, username) {
   364  				common.AuthzFail(response, request, username, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
   365  
   366  				return
   367  			}
   368  
   369  			next.ServeHTTP(response, request) //nolint:contextcheck
   370  		})
   371  	}
   372  }