Files
zot/pkg/requestcontext/user_access_control.go
Ramkumar Chinchani a2d738c575 fix: miscellaneous fixes for ai-reported suggestions (#4101)
* test(cli): replace panic with t.Fatalf in deprecated config tests

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(trivy): keep sbom generation failures non-fatal in runTrivy

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* docs(meta): fix typos in hooks comments

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* refactor(requestcontext): align package name and godoc comments

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* test(gc): factor metrics setup helper and fix typo

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* test(trivy): cover non-fatal SBOM generation failures

- add runTrivy test ensuring scan succeeds when SBOM generation fails

- inject artifact runner constructor for deterministic internal testing

- fix matchesRepo doc comment to be action-agnostic

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

---------

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
2026-05-26 14:58:30 -07:00

258 lines
7.1 KiB
Go

package requestcontext
import (
"context"
"net/http"
"slices"
glob "github.com/bmatcuk/doublestar/v4" //nolint:gci
"zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/api/constants"
)
type Key int
// request-local context key.
var uacCtxKey = Key(0) //nolint: gochecknoglobals
// GetContextKey returns a pointer needed for use in context.WithValue.
func GetContextKey() *Key {
return &uacCtxKey
}
type UserAccessControl struct {
authzInfo *UserAuthzInfo
authnInfo *UserAuthnInfo
// claims is a free-form bag of authentication-time attributes surfaced to
// authorization-time CEL conditions as `req.claims`. It is populated by
// whichever authn flow has structured attributes to expose: OIDC bearer
// today (the ID token's claim set), and optionally other flows (browser
// OpenID, mTLS cert attributes, ...) as they grow that capability.
claims map[string]any
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
}
// SetClaims stores authentication-time attributes (OIDC token claims, mTLS
// cert attributes, etc.) that should be exposed to authz-time CEL conditions
// as `req.claims`. Authn flows are free to populate whichever subset they
// have available; everything else is left nil.
func (uac *UserAccessControl) SetClaims(claims map[string]any) {
uac.claims = claims
}
// GetClaims returns the authentication-time attribute bag, or nil if the
// active authn flow did not populate one.
func (uac *UserAccessControl) GetClaims() map[string]any {
return uac.claims
}
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{}
}
if uac.authzInfo.globPatterns == nil {
uac.authzInfo.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 {
return slices.Contains(uac.behaviourActions, action)
}
func (uac *UserAccessControl) isMethodAction(action string) bool {
return slices.Contains(uac.methodActions, action)
}
// 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
}
// matchesRepo returns whether repository matches the provided action's glob patterns
// and is allowed by the longest matching pattern.
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
}
return uac.Can(constants.ReadPermission, repoName), nil
}
func CanDelete(ctx context.Context, repoName string) (bool, error) {
uac, err := UserAcFromContext(ctx)
if err != nil {
return false, err
}
return uac.Can(constants.DeletePermission, repoName), nil
}