Files
zot/pkg/requestcontext/user_access_control.go
T
Luca Muscariello 2402296e9a fix: migrate to Go module v2 for proper semantic versioning (#3462)
* fix: migrate to Go module v2 for proper semantic versioning

This change updates the module path from 'zotregistry.dev/zot' to
'zotregistry.dev/zot/v2' to comply with Go's semantic versioning rules.

According to Go's module versioning requirements, major version v2+
must include the major version in the module path. The current
module path 'zotregistry.dev/zot' only supports v0.x.x and v1.x.x
versions, making existing v2.x.x tags (like v2.1.8) unusable.

Changes:
- Updated go.mod module path to zotregistry.dev/zot/v2
- Updated all internal import paths across 280+ Go source files
- Updated configuration files (golangcilint.yaml, gqlgen.yml)
- Updated README.md Go reference badge

This fix enables proper use of existing v2.x.x Git tags and allows
external packages to import zot v2+ versions without compatibility
errors.

Resolves: Go module import compatibility for v2+ versions
Fixes: #3071
Signed-off-by: Luca Muscariello <muscariello@ieee.org>

* fix: regenerate GraphQL files with updated v2 import paths

The gqlgen tool needs to regenerate the GraphQL schema files after
the module path change to use the new v2 imports.

Signed-off-by: Luca Muscariello <muscariello@ieee.org>

---------

Signed-off-by: Luca Muscariello <muscariello@ieee.org>
2025-10-16 22:43:47 -07:00

260 lines
6.2 KiB
Go

package uac
import (
"context"
"net/http"
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
// 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
}
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
}