From 336526065f7954b00983c217005012f08ca9a26c Mon Sep 17 00:00:00 2001 From: Lisca Ana-Roberta <55219463+aokirisaki@users.noreply.github.com> Date: Wed, 8 Mar 2023 21:47:15 +0200 Subject: [PATCH] feat(groups)!: added "groups" mechanism for authZ (#1123) BREAKING CHANGE: repository paths are now specified under a new config key called "repositories" under "accessControl" section in order to handle "groups" feature. Previously the repository paths were specified directly under "accessControl". This PR adds the ability to create groups of users which can be used for authZ policies, instead of just users. { "http": { "accessControl": { "groups": { Just like the users, groups can be part of repository policies/default policies/admin policies. The 'groups' field in accessControl can be missing if there are no groups. The permissions priority is user>group>default>admin policy, verified in this order (in authz.go), and permissions are cumulative. It works with LDAP too, and the group attribute name is configurable. The DN of the group is used as the group name and the functionality is the same. All groups for the given user are added to the context in authn.go. Repository paths are now specified under a new keyword called "repositories" under "accessControl" section in order to handle "groups" feature. Signed-off-by: Ana-Roberta Lisca --- examples/config-anonymous-authz.json | 48 +- examples/config-ldap.json | 73 ++ examples/config-policy.json | 143 ++-- pkg/api/authn.go | 53 +- pkg/api/authz.go | 94 ++- pkg/api/config/config.go | 94 +-- pkg/api/controller.go | 5 +- pkg/api/controller_test.go | 677 +++++++++++++++++-- pkg/api/ldap.go | 21 +- pkg/api/routes.go | 5 +- pkg/cli/config_reloader_test.go | 32 +- pkg/cli/root.go | 17 +- pkg/cli/root_test.go | 34 +- pkg/requestcontext/context.go | 13 +- pkg/test/common_test.go | 2 +- test/blackbox/anonymous_policy.bats | 30 +- test/blackbox/detect_manifest_collision.bats | 40 +- 17 files changed, 1023 insertions(+), 358 deletions(-) create mode 100644 examples/config-ldap.json diff --git a/examples/config-anonymous-authz.json b/examples/config-anonymous-authz.json index 22d2075d..adf1d740 100644 --- a/examples/config-anonymous-authz.json +++ b/examples/config-anonymous-authz.json @@ -8,29 +8,31 @@ "port": "8080", "realm": "zot", "accessControl": { - "**": { - "anonymousPolicy": [ - "read", - "create" - ] - }, - "tmp/**": { - "anonymousPolicy": [ - "read", - "create", - "update" - ] - }, - "infra/**": { - "anonymousPolicy": [ - "read" - ] - }, - "repos2/repo": { - "anonymousPolicy": [ - "read" - ] - } + "repositories": { + "**": { + "anonymousPolicy": [ + "read", + "create" + ] + }, + "tmp/**": { + "anonymousPolicy": [ + "read", + "create", + "update" + ] + }, + "infra/**": { + "anonymousPolicy": [ + "read" + ] + }, + "repos2/repo": { + "anonymousPolicy": [ + "read" + ] + } + } } }, "log": { diff --git a/examples/config-ldap.json b/examples/config-ldap.json new file mode 100644 index 00000000..4656e6fd --- /dev/null +++ b/examples/config-ldap.json @@ -0,0 +1,73 @@ +{ + "distSpecVersion": "1.1.0-dev", + "storage": { + "rootDirectory": "/tmp/zot" + }, + "http": { + "address": "127.0.0.1", + "port": "8080", + "realm": "zot", + "tls": { + "cert": "test/data/server.cert", + "key": "test/data/server.key" + }, + "auth": { + "ldap": { + "address": "ldap.example.org", + "port": 389, + "startTLS": false, + "baseDN":"ou=Users,dc=example,dc=org", + "userAttribute": "uid", + "userGroupAttribute": "memberOf", + "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", + "bindPassword":"ldap-searcher-password", + "skipVerify": true, + "subtreeSearch": true + }, + "failDelay": 5 + }, + "accessControl": { + "repositories": { + "**": { + "policies": [{ + "users": ["charlie"], + "groups": ["admins", "developers", "cn=ldap-group,ou=Groups,dc=example,dc=org"], + "actions": ["read", "create", "update"] + }, + { + "users": ["mary"], + "groups": ["group2"], + "actions": ["read", "create", "update", "delete"] + }], + "defaultPolicy": [] + }, + "tmp/**": { + "defaultPolicy": ["read", "create", "update"] + }, + "repos2/repo": { + "policies": [{ + "users": ["bob"], + "groups": ["sparkle_team","repo2_team"], + "actions": ["read", "create"] + }, + { + "users": ["mallory"], + "actions": ["create", "read"] + } + ], + "defaultPolicy": ["read"] + } + }, + "adminPolicy": { + "users": ["admin"], + "groups": ["admins","developers"], + "actions": ["read", "create", "update", "delete"] + } + } + }, + "log": { + "level": "debug", + "output": "/tmp/zot.log", + "audit": "/tmp/zot-audit.log" + } +} \ No newline at end of file diff --git a/examples/config-policy.json b/examples/config-policy.json index b658b544..eaf45e28 100644 --- a/examples/config-policy.json +++ b/examples/config-policy.json @@ -4,107 +4,68 @@ "rootDirectory": "/tmp/zot" }, "http": { - "address": "127.0.0.1", - "port": "8080", - "realm": "zot", "auth": { "htpasswd": { "path": "test/data/htpasswd" - }, - "failDelay": 1 + } }, "accessControl": { - "**": { - "anonymousPolicy": ["read"], - "policies": [ - { - "users": [ - "charlie" - ], - "actions": [ - "read", - "create", - "update" - ] - } - ], - "defaultPolicy": [ - "read", - "create", - "delete", - "detectManifestCollision" - ] + "groups": { + "group1": { + "users": ["jack", "john", "jane", "ana"] + }, + "group2": { + "users": ["alice", "mike", "jim"] + } }, - "tmp/**": { - "defaultPolicy": [ - "read", - "create", - "update" - ] - }, - "infra/**": { - "policies": [ - { - "users": [ - "alice", - "bob" - ], - "actions": [ - "create", - "read", - "update", - "delete" - ] + "repositories": { + "**": { + "policies": [{ + "users": ["charlie"], + "groups": ["admins", "developers", "group1"], + "actions": ["read", "create", "update"] }, { - "users": [ - "mallory" - ], - "actions": [ - "create", - "read" - ] - } - ], - "defaultPolicy": [ - "read" - ] - }, - "repos2/repo": { - "policies": [ - { - "users": [ - "charlie" - ], - "actions": [ - "read", - "create" - ] - }, - { - "users": [ - "mallory" - ], - "actions": [ - "create", - "read" - ] - } - ], - "defaultPolicy": [ - "read" - ] + "users": ["mary"], + "groups": ["group2"], + "actions": ["read", "create", "update", "delete"] + }], + "defaultPolicy": ["read", "create"] + }, + "tmp/**": { + "defaultPolicy": ["read", "create", "update"] + }, + "infra/*": { + "policies": [{ + "users": ["alice", "bob"], + "groups": ["maintainers","platformteam"], + "actions": ["create", "read", "update", "delete"] + }, + { + "users": ["mallory"], + "actions": ["create", "read"] + } + ], + "defaultPolicy": ["read"] + }, + "repos2/repo": { + "policies": [{ + "users": ["bob"], + "groups": ["sparkle_team","repo2_team"], + "actions": ["read", "create"] + }, + { + "users": ["mallory"], + "actions": ["create", "read"] + } + ], + "defaultPolicy": ["read"] + } }, "adminPolicy": { - "users": [ - "admin" - ], - "actions": [ - "read", - "create", - "update", - "delete" - ] + "users": ["admin"], + "groups": ["admins","developers"], + "actions": ["read", "create", "update", "delete"] } } }, diff --git a/pkg/api/authn.go b/pkg/api/authn.go index 7ad0d26e..55df9730 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -2,6 +2,7 @@ package api import ( "bufio" + "context" "crypto/x509" "encoding/base64" "fmt" @@ -17,6 +18,7 @@ import ( "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/config" + localCtx "zotregistry.io/zot/pkg/requestcontext" ) const ( @@ -87,7 +89,8 @@ func noPasswdAuth(realm string, config *config.Config) mux.MiddlewareFunc { } // Process request - next.ServeHTTP(response, request) + ctx := getReqContextWithAuthorization("", []string{}, request) + next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck }) } } @@ -123,6 +126,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { SkipTLS: !ldapConfig.StartTLS, Base: ldapConfig.BaseDN, BindDN: ldapConfig.BindDN, + UserGroupAttribute: ldapConfig.UserGroupAttribute, // from config BindPassword: ldapConfig.BindPassword, UserFilter: fmt.Sprintf("(%s=%%s)", ldapConfig.UserAttribute), InsecureSkipVerify: ldapConfig.SkipVerify, @@ -181,9 +185,11 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { return } - if request.Header.Get("Authorization") == "" && anonymousPolicyExists(ctlr.Config.AccessControl) { + + if request.Header.Get("Authorization") == "" && anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) { // Process request - next.ServeHTTP(response, request) + ctx := getReqContextWithAuthorization("", []string{}, request) + next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck return } @@ -198,9 +204,10 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { // some client tools might send Authorization: Basic Og== (decoded into ":") // empty username and password - if username == "" && passphrase == "" && anonymousPolicyExists(ctlr.Config.AccessControl) { + if username == "" && passphrase == "" && anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) { // Process request - next.ServeHTTP(response, request) + ctx := getReqContextWithAuthorization("", []string{}, request) + next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck return } @@ -210,7 +217,15 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { if ok { if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err == nil { // Process request - next.ServeHTTP(response, request) + var userGroups []string + + if ctlr.Config.HTTP.AccessControl != nil { + ac := NewAccessController(ctlr.Config) + userGroups = ac.getUserGroups(username) + } + + ctx := getReqContextWithAuthorization(username, userGroups, request) + next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck return } @@ -218,10 +233,20 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { // next, LDAP if configured (network-based which can lose connectivity) if ctlr.Config.HTTP.Auth != nil && ctlr.Config.HTTP.Auth.LDAP != nil { - ok, _, err := ldapClient.Authenticate(username, passphrase) + ok, _, ldapgroups, err := ldapClient.Authenticate(username, passphrase) if ok && err == nil { // Process request - next.ServeHTTP(response, request) + var userGroups []string + + if ctlr.Config.HTTP.AccessControl != nil { + ac := NewAccessController(ctlr.Config) + userGroups = ac.getUserGroups(username) + } + + userGroups = append(userGroups, ldapgroups...) + + ctx := getReqContextWithAuthorization(username, userGroups, request) + next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck return } @@ -232,6 +257,18 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { } } +func getReqContextWithAuthorization(username string, groups []string, request *http.Request) context.Context { + acCtx := localCtx.AccessControlContext{ + Username: username, + Groups: groups, + } + + authzCtxKey := localCtx.GetContextKey() + ctx := context.WithValue(request.Context(), authzCtxKey, acCtx) + + return ctx +} + func isAuthnEnabled(config *config.Config) bool { if config.HTTP.Auth != nil && (config.HTTP.Auth.HTPasswd.Path != "" || config.HTTP.Auth.LDAP != nil) { diff --git a/pkg/api/authz.go b/pkg/api/authz.go index d2b22ac3..c7e0d780 100644 --- a/pkg/api/authz.go +++ b/pkg/api/authz.go @@ -35,14 +35,14 @@ type AccessController struct { func NewAccessController(config *config.Config) *AccessController { return &AccessController{ - Config: config.AccessControl, + Config: config.HTTP.AccessControl, Log: log.NewLogger(config.Log.Level, config.Log.Output), } } // getGlobPatterns gets glob patterns from authz config on which has perms. // used to filter /v2/_catalog repositories based on user rights. -func (ac *AccessController) getGlobPatterns(username string, action string) map[string]bool { +func (ac *AccessController) getGlobPatterns(username string, groups []string, action string) map[string]bool { globPatterns := make(map[string]bool) for pattern, policyGroup := range ac.Config.Repositories { @@ -65,6 +65,15 @@ func (ac *AccessController) getGlobPatterns(username string, action string) map[ } } + // check group based policy + for _, group := range groups { + for _, p := range policyGroup.Policies { + if common.Contains(p.Groups, group) && common.Contains(p.Actions, action) { + globPatterns[pattern] = true + } + } + } + // if not allowed then mark it if _, ok := globPatterns[pattern]; !ok { globPatterns[pattern] = false @@ -75,7 +84,7 @@ func (ac *AccessController) getGlobPatterns(username string, action string) map[ } // can verifies if a user can do action on repository. -func (ac *AccessController) can(username, action, repository string) bool { +func (ac *AccessController) can(ctx context.Context, username, action, repository string) bool { can := false var longestMatchedPattern string @@ -89,10 +98,17 @@ func (ac *AccessController) can(username, action, repository string) bool { } } + acCtx, err := localCtx.GetAccessControlContext(ctx) + if err != nil { + return false + } + + userGroups := acCtx.Groups + // check matched repo based policy pg, ok := ac.Config.Repositories[longestMatchedPattern] if ok { - can = isPermitted(username, action, pg) + can = ac.isPermitted(userGroups, username, action, pg) } // check admins based policy @@ -100,6 +116,10 @@ func (ac *AccessController) can(username, action, repository string) bool { if ac.isAdmin(username) && common.Contains(ac.Config.AdminPolicy.Actions, action) { can = true } + + if ac.isAnyGroupInAdminPolicy(userGroups) && common.Contains(ac.Config.AdminPolicy.Actions, action) { + can = true + } } return can @@ -110,17 +130,44 @@ func (ac *AccessController) isAdmin(username string) bool { return common.Contains(ac.Config.AdminPolicy.Users, username) } +func (ac *AccessController) isAnyGroupInAdminPolicy(userGroups []string) bool { + for _, group := range userGroups { + if common.Contains(ac.Config.AdminPolicy.Groups, group) { + return true + } + } + + return false +} + +func (ac *AccessController) getUserGroups(username string) []string { + var groupNames []string + + for groupName, group := range ac.Config.Groups { + for _, user := range group.Users { + // find if the user is part of any groups + if user == username { + groupNames = append(groupNames, groupName) + } + } + } + + return groupNames +} + // getContext builds ac context(allowed to read repos and if user is admin) and returns it. func (ac *AccessController) getContext(username string, request *http.Request) context.Context { - readGlobPatterns := ac.getGlobPatterns(username, Read) - dmcGlobPatterns := ac.getGlobPatterns(username, DetectManifestCollision) - - acCtx := localCtx.AccessControlContext{ - ReadGlobPatterns: readGlobPatterns, - DmcGlobPatterns: dmcGlobPatterns, - Username: username, + acCtx, err := localCtx.GetAccessControlContext(request.Context()) + if err != nil { + return nil } + readGlobPatterns := ac.getGlobPatterns(username, acCtx.Groups, Read) + dmcGlobPatterns := ac.getGlobPatterns(username, acCtx.Groups, DetectManifestCollision) + + acCtx.ReadGlobPatterns = readGlobPatterns + acCtx.DmcGlobPatterns = dmcGlobPatterns + if ac.isAdmin(username) { acCtx.IsAdmin = true } else { @@ -128,20 +175,37 @@ func (ac *AccessController) getContext(username string, request *http.Request) c } authzCtxKey := localCtx.GetContextKey() - ctx := context.WithValue(request.Context(), authzCtxKey, acCtx) + ctx := context.WithValue(request.Context(), authzCtxKey, *acCtx) return ctx } // isPermitted returns true if username can do action on a repository policy. -func isPermitted(username, action string, policyGroup config.PolicyGroup) bool { +func (ac *AccessController) isPermitted(userGroups []string, username, action string, + policyGroup config.PolicyGroup, +) bool { var result bool + // check repo/system based policies for _, p := range policyGroup.Policies { if common.Contains(p.Users, username) && common.Contains(p.Actions, action) { result = true - break + return result + } + } + + if userGroups != nil { + for _, p := range policyGroup.Policies { + if common.Contains(p.Actions, action) { + for _, group := range p.Groups { + if common.Contains(userGroups, group) { + result = true + + return result + } + } + } } } @@ -246,7 +310,7 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc { action = Delete } - can := acCtrlr.can(identity, action, resource) + can := acCtrlr.can(ctx, identity, action, resource) //nolint:contextcheck if !can { authzFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) } else { diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 393115f8..2ac7e3e9 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -1,13 +1,11 @@ package config import ( - "fmt" "os" "time" "github.com/getlantern/deepcopy" distspec "github.com/opencontainers/distribution-spec/specs-go" - "github.com/spf13/viper" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/storage" @@ -66,28 +64,29 @@ type RatelimitConfig struct { } type HTTPConfig struct { - Address string - Port string - AllowOrigin string // comma separated - TLS *TLSConfig - Auth *AuthConfig - RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"` - Realm string - Ratelimit *RatelimitConfig `mapstructure:",omitempty"` + Address string + Port string + AllowOrigin string // comma separated + TLS *TLSConfig + Auth *AuthConfig + AccessControl *AccessControlConfig + Realm string + Ratelimit *RatelimitConfig `mapstructure:",omitempty"` } type LDAPConfig struct { - Port int - Insecure bool - StartTLS bool // if !Insecure, then StartTLS or LDAPs - SkipVerify bool - SubtreeSearch bool - Address string - BindDN string - BindPassword string - BaseDN string - UserAttribute string - CACert string + Port int + Insecure bool + StartTLS bool // if !Insecure, then StartTLS or LDAPs + SkipVerify bool + SubtreeSearch bool + Address string + BindDN string + UserGroupAttribute string + BindPassword string + BaseDN string + UserAttribute string + CACert string } type LogConfig struct { @@ -102,11 +101,19 @@ type GlobalStorageConfig struct { } type AccessControlConfig struct { - Repositories Repositories + Repositories Repositories `json:"repositories" mapstructure:"repositories"` AdminPolicy Policy + Groups Groups } -type Repositories map[string]PolicyGroup +type ( + Repositories map[string]PolicyGroup + Groups map[string]Group +) + +type Group struct { + Users []string +} type PolicyGroup struct { Policies []Policy @@ -117,6 +124,7 @@ type PolicyGroup struct { type Policy struct { Users []string Actions []string + Groups []string } type Config struct { @@ -125,7 +133,6 @@ type Config struct { Commit string ReleaseTag string BinaryType string - AccessControl *AccessControlConfig Storage GlobalStorageConfig HTTP HTTPConfig Log *LogConfig @@ -187,42 +194,3 @@ func (c *Config) Sanitize() *Config { return sanitizedConfig } - -// LoadAccessControlConfig populates config.AccessControl struct with values from config. -func (c *Config) LoadAccessControlConfig(viperInstance *viper.Viper) error { - if c.HTTP.RawAccessControl == nil { - return nil - } - - c.AccessControl = &AccessControlConfig{} - c.AccessControl.Repositories = make(map[string]PolicyGroup) - - for policy := range c.HTTP.RawAccessControl { - var policies []Policy - - var policyGroup PolicyGroup - - if policy == "adminpolicy" { - adminPolicy := viperInstance.GetStringMapStringSlice("http::accessControl::adminPolicy") - c.AccessControl.AdminPolicy.Actions = adminPolicy["actions"] - c.AccessControl.AdminPolicy.Users = adminPolicy["users"] - - continue - } - - err := viperInstance.UnmarshalKey(fmt.Sprintf("http::accessControl::%s::policies", policy), &policies) - if err != nil { - return err - } - - defaultPolicy := viperInstance.GetStringSlice(fmt.Sprintf("http::accessControl::%s::defaultPolicy", policy)) - policyGroup.DefaultPolicy = defaultPolicy - - anonymousPolicy := viperInstance.GetStringSlice(fmt.Sprintf("http::accessControl::%s::anonymousPolicy", policy)) - policyGroup.Policies = policies - policyGroup.AnonymousPolicy = anonymousPolicy - c.AccessControl.Repositories[policy] = policyGroup - } - - return nil -} diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 8ca17023..ea752be0 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -211,7 +211,7 @@ func (c *Controller) Run(reloadCtx context.Context) error { if c.Config.HTTP.TLS.CACert != "" { clientAuth := tls.VerifyClientCertIfGiven if (c.Config.HTTP.Auth == nil || c.Config.HTTP.Auth.HTPasswd.Path == "") && - !anonymousPolicyExists(c.Config.AccessControl) { + !anonymousPolicyExists(c.Config.HTTP.AccessControl) { clientAuth = tls.RequireAndVerifyClientCert } @@ -599,8 +599,7 @@ func toStringIfOk(cacheDriverConfig map[string]interface{}, param string, log lo func (c *Controller) LoadNewConfig(reloadCtx context.Context, config *config.Config) { // reload access control config - c.Config.AccessControl = config.AccessControl - c.Config.HTTP.RawAccessControl = config.HTTP.RawAccessControl + c.Config.HTTP.AccessControl = config.HTTP.AccessControl // Enable extensions if extension config is provided if config.Extensions != nil && config.Extensions.Sync != nil { diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index b34c056f..156c41ee 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -60,6 +60,8 @@ import ( const ( username = "test" passphrase = "test" + group = "test" + repo = "test" ServerCert = "../../test/data/server.cert" ServerKey = "../../test/data/server.key" CACert = "../../test/data/ca.crt" @@ -1145,7 +1147,7 @@ func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) { Key: ServerKey, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -1210,7 +1212,7 @@ func TestMutualTLSAuthWithUserPermissions(t *testing.T) { CACert: CACert, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -1234,7 +1236,7 @@ func TestMutualTLSAuthWithUserPermissions(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] // setup TLS mutual auth cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key") @@ -1261,7 +1263,7 @@ func TestMutualTLSAuthWithUserPermissions(t *testing.T) { // empty default authorization and give user the permission to create repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create") - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) @@ -1291,7 +1293,7 @@ func TestMutualTLSAuthWithoutCN(t *testing.T) { CACert: "../../test/data/noidentity/ca.crt", } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -1410,7 +1412,7 @@ func TestTLSMutualAuthAllowReadAccess(t *testing.T) { CACert: CACert, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -1574,7 +1576,7 @@ func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) { CACert: CACert, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -1702,7 +1704,15 @@ func (l *testLDAPServer) Search(boundDN string, req vldap.SearchRequest, if check == req.Filter { return vldap.ServerSearchResult{ Entries: []*vldap.Entry{ - {DN: fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN)}, + { + DN: fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN), + Attributes: []*vldap.EntryAttribute{ + { + Name: "memberOf", + Values: []string{group}, + }, + }, + }, }, ResultCode: vldap.LDAPResultSuccess, }, nil @@ -1759,6 +1769,83 @@ func TestBasicAuthWithLDAP(t *testing.T) { resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // missing password + resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + }) +} + +func TestGroupsPermissionsForLDAP(t *testing.T) { + Convey("Make a new controller", t, func() { + l := newTestLDAPServer() + port := test.GetFreePort() + ldapPort, err := strconv.Atoi(port) + So(err, ShouldBeNil) + l.Start(ldapPort) + defer l.Stop() + + port = test.GetFreePort() + baseURL := test.GetBaseURL(port) + tempDir := t.TempDir() + + conf := config.New() + conf.HTTP.Port = port + conf.HTTP.Auth = &config.AuthConfig{ + LDAP: &config.LDAPConfig{ + Insecure: true, + Address: LDAPAddress, + Port: ldapPort, + BindDN: LDAPBindDN, + BindPassword: LDAPBindPassword, + BaseDN: LDAPBaseDN, + UserAttribute: "uid", + UserGroupAttribute: "memberOf", + }, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group: { + Users: []string{username}, + }, + }, + Repositories: config.Repositories{ + repo: config.PolicyGroup{ + Policies: []config.Policy{ + { + Groups: []string{group}, + Actions: []string{"read", "create"}, + }, + }, + DefaultPolicy: []string{}, + }, + }, + AdminPolicy: config.Policy{ + Users: []string{}, + Actions: []string{}, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repo, + username, passphrase) + + So(err, ShouldBeNil) }) } @@ -1989,7 +2076,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) { } ctlr := makeController(conf, t.TempDir(), "") - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{"read"}, @@ -2205,7 +2292,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { Path: htpasswdPath, }, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -2264,9 +2351,9 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // first let's use global based policies // add test user to global policy with create perm - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll // now it should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2302,7 +2389,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // get tags with read access should get 200 - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2332,7 +2419,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add delete perm on repo - conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll // delete blob should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2344,7 +2431,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // now let's use only repository based policies // add test user to repo's policy with create perm // longest path matching should match the repo and not **/* - conf.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{ + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{ Policies: []config.Policy{ { Users: []string{}, @@ -2354,8 +2441,8 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { DefaultPolicy: []string{}, } - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll // now it should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2391,7 +2478,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // get tags with read access should get 200 - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2427,7 +2514,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add delete perm on repo - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll // delete blob should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2437,10 +2524,10 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // remove permissions on **/* so it will not interfere with zot-test namespace - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] repoPolicy.Policies = []config.Policy{} repoPolicy.DefaultPolicy = []string{} - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy // get manifest should get 403, we don't have perm at all on this repo resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2450,7 +2537,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add read perm on repo - conf.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{ + conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{ { Users: []string{"test"}, Actions: []string{"read"}, @@ -2498,7 +2585,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add create perm on repo - conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll // should get 201 with create perm resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2584,7 +2671,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.Body(), ShouldResemble, manifestBlob) // add update perm on repo - conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll // update manifest should get 201 with update perm resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2604,10 +2691,10 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.Body(), ShouldResemble, updatedManifestBlob) // now use default repo policy - conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} - repoPolicy = conf.AccessControl.Repositories["zot-test"] + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} + repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] repoPolicy.DefaultPolicy = []string{"update"} - conf.AccessControl.Repositories["zot-test"] = repoPolicy + conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy // update manifest should get 201 with update perm on repo's default policy resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -2619,10 +2706,10 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusCreated) // with default read on repo should still get 200 - conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{} - repoPolicy = conf.AccessControl.Repositories[AuthorizationNamespace] + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{} + repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] repoPolicy.DefaultPolicy = []string{"read"} - conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2632,7 +2719,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // upload blob without user create but with default create should get 200 repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create") - conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -2641,15 +2728,15 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // remove per repo policy - repoPolicy = conf.AccessControl.Repositories[AuthorizationNamespace] + repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] repoPolicy.Policies = []config.Policy{} repoPolicy.DefaultPolicy = []string{} - conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy - repoPolicy = conf.AccessControl.Repositories["zot-test"] + repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] repoPolicy.Policies = []config.Policy{} repoPolicy.DefaultPolicy = []string{} - conf.AccessControl.Repositories["zot-test"] = repoPolicy + conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -2665,8 +2752,8 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add read perm - conf.AccessControl.AdminPolicy.Users = append(conf.AccessControl.AdminPolicy.Users, "test") - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "read") + conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "test") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "read") // with read perm should get 200 resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") @@ -2682,7 +2769,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add create perm - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "create") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") // with create perm should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -2710,7 +2797,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add delete perm - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "delete") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "delete") // with delete perm should get http.StatusAccepted resp, err = resty.R().SetBasicAuth(username, passphrase). Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) @@ -2726,7 +2813,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add update perm - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "update") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "update") // update manifest should get 201 with update perm resp, err = resty.R().SetBasicAuth(username, passphrase). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). @@ -2736,7 +2823,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - conf.AccessControl = &config.AccessControlConfig{} + conf.HTTP.AccessControl = &config.AccessControlConfig{} resp, err = resty.R().SetBasicAuth(username, passphrase). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). @@ -2809,7 +2896,7 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { conf := config.New() conf.HTTP.Port = port conf.HTTP.Auth = &config.AuthConfig{} - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ TestRepo: config.PolicyGroup{ AnonymousPolicy: []string{}, @@ -2845,9 +2932,9 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - if entry, ok := conf.AccessControl.Repositories[TestRepo]; ok { + if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok { entry.AnonymousPolicy = []string{"create", "read"} - conf.AccessControl.Repositories[TestRepo] = entry + conf.HTTP.AccessControl.Repositories[TestRepo] = entry } // now it should get 202 @@ -2964,9 +3051,9 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { So(resp.Body(), ShouldResemble, manifestBlob) // add update perm on repo - if entry, ok := conf.AccessControl.Repositories[TestRepo]; ok { + if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok { entry.AnonymousPolicy = []string{"create", "read", "update"} - conf.AccessControl.Repositories[TestRepo] = entry + conf.HTTP.AccessControl.Repositories[TestRepo] = entry } // update manifest should get 201 with update perm @@ -3006,7 +3093,7 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { }, } // config with all policy types, to test that the correct one is applied in each case - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ @@ -3041,9 +3128,9 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] repoPolicy.AnonymousPolicy = append(repoPolicy.AnonymousPolicy, "read") - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy // should have access to /v2/, anonymous policy is applied, "read" allowed resp, err = resty.R().Get(baseURL + "/v2/") @@ -3089,7 +3176,7 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "read") - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy // with read permission should get 200, because default policy allows reading now resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -3125,8 +3212,8 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) // add read permission to user "bob" - conf.AccessControl.AdminPolicy.Users = append(conf.AccessControl.AdminPolicy.Users, "bob") - conf.AccessControl.AdminPolicy.Actions = append(conf.AccessControl.AdminPolicy.Actions, "create") + conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "bob") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") // added create permission to user "bob", should be allowed now resp, err = resty.R().SetBasicAuth("bob", passphrase). @@ -3208,7 +3295,7 @@ func TestHTTPReadOnly(t *testing.T) { conf := config.New() conf.HTTP.Port = port // enable read-only mode - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ DefaultPolicy: []string{"read"}, @@ -5535,7 +5622,7 @@ func TestManifestCollision(t *testing.T) { dir := t.TempDir() ctlr := makeController(conf, dir, "") - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ AuthorizationAllRepos: config.PolicyGroup{ AnonymousPolicy: []string{api.Read, api.Create, api.Delete, api.DetectManifestCollision}, @@ -5592,9 +5679,9 @@ func TestManifestCollision(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusConflict) // remove detectManifestCollision action from ** (all repos) - repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] repoPolicy.AnonymousPolicy = []string{"read", "delete"} - conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String()) So(err, ShouldBeNil) @@ -6577,7 +6664,7 @@ func TestSearchRoutes(t *testing.T) { Search: searchConfig, } - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ repoName: config.PolicyGroup{ Policies: []config.Policy{ @@ -6640,18 +6727,18 @@ func TestSearchRoutes(t *testing.T) { So(err, ShouldBeNil) query := ` - { - GlobalSearch(query:"testrepo"){ - Repos { - Name - Score - NewestImage { - RepoName - Tag + { + GlobalSearch(query:"testrepo"){ + Repos { + Name + Score + NewestImage { + RepoName + Tag + } } } - } - }` + }` resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) @@ -6665,7 +6752,7 @@ func TestSearchRoutes(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ repoName: config.PolicyGroup{ Policies: []config.Policy{ @@ -6701,6 +6788,460 @@ func TestSearchRoutes(t *testing.T) { So(string(resp.Body()), ShouldNotContainSubstring, repoName) So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo) }) + + Convey("Testing group permissions", func(c C) { + conf := config.New() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + user1 := "test1" + password1 := "test1" + group1 := "testgroup3" + testString1 := getCredString(user1, password1) + htpasswdPath := test.MakeHtpasswdFileFromString(testString1) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + conf.HTTP.Port = port + + defaultVal := true + + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + } + + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group1: { + Users: []string{user1}, + }, + }, + Repositories: config.Repositories{ + repoName: config.PolicyGroup{ + Policies: []config.Policy{ + { + Groups: []string{group1}, + Actions: []string{"read", "create"}, + }, + }, + DefaultPolicy: []string{}, + }, + }, + AdminPolicy: config.Policy{ + Users: []string{}, + Actions: []string{}, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repoName, + user1, password1) + + So(err, ShouldBeNil) + + query := ` + { + GlobalSearch(query:"testrepo"){ + Repos { + Name + Score + NewestImage { + RepoName + Tag + } + } + } + }` + resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix + + "?query=" + url.QueryEscape(query)) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) + + Convey("Testing group permissions when the user is part of more groups with different permissions", func(c C) { + conf := config.New() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + user1 := "test2" + password1 := "test2" + group1 := "testgroup1" + group2 := "secondtestgroup" + testString1 := getCredString(user1, password1) + htpasswdPath := test.MakeHtpasswdFileFromString(testString1) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + conf.HTTP.Port = port + + defaultVal := true + + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + } + + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group1: { + Users: []string{user1}, + }, + }, + Repositories: config.Repositories{ + repoName: config.PolicyGroup{ + Policies: []config.Policy{ + { + Groups: []string{group1}, + Actions: []string{"delete"}, + }, + { + Groups: []string{group2}, + Actions: []string{"read", "create"}, + }, + }, + DefaultPolicy: []string{}, + }, + }, + AdminPolicy: config.Policy{ + Users: []string{}, + Actions: []string{}, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repoName, + user1, password1) + + So(err, ShouldNotBeNil) + }) + + Convey("Testing group permissions when group has less permissions than user", func(c C) { + conf := config.New() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + user1 := "test3" + password1 := "test3" + group1 := "testgroup" + testString1 := getCredString(user1, password1) + htpasswdPath := test.MakeHtpasswdFileFromString(testString1) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + conf.HTTP.Port = port + + defaultVal := true + + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + } + + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group1: { + Users: []string{user1}, + }, + }, + Repositories: config.Repositories{ + repoName: config.PolicyGroup{ + Policies: []config.Policy{ + { + Groups: []string{group1}, + Actions: []string{"delete"}, + }, + { + Users: []string{user1}, + Actions: []string{"read", "create", "delete"}, + }, + }, + DefaultPolicy: []string{}, + }, + }, + AdminPolicy: config.Policy{ + Users: []string{}, + Actions: []string{}, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repoName, + user1, password1) + + So(err, ShouldBeNil) + }) + + Convey("Testing group permissions when user has less permissions than group", func(c C) { + conf := config.New() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + user1 := "test4" + password1 := "test4" + group1 := "testgroup1" + testString1 := getCredString(user1, password1) + htpasswdPath := test.MakeHtpasswdFileFromString(testString1) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + conf.HTTP.Port = port + + defaultVal := true + + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + } + + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group1: { + Users: []string{user1}, + }, + }, + Repositories: config.Repositories{ + repoName: config.PolicyGroup{ + Policies: []config.Policy{ + { + Groups: []string{group1}, + Actions: []string{"read", "create", "delete"}, + }, + { + Users: []string{user1}, + Actions: []string{"delete"}, + }, + }, + DefaultPolicy: []string{}, + }, + }, + AdminPolicy: config.Policy{ + Users: []string{}, + Actions: []string{}, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repoName, + user1, password1) + + So(err, ShouldBeNil) + }) + + Convey("Testing group permissions on admin policy", func(c C) { + conf := config.New() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + user1 := "test5" + password1 := "test5" + group1 := "testgroup2" + testString1 := getCredString(user1, password1) + htpasswdPath := test.MakeHtpasswdFileFromString(testString1) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + conf.HTTP.Port = port + + defaultVal := true + + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + } + + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group1: { + Users: []string{user1}, + }, + }, + Repositories: config.Repositories{}, + AdminPolicy: config.Policy{ + Groups: []string{group1}, + Actions: []string{"read", "create"}, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repoName, + user1, password1) + + So(err, ShouldBeNil) + }) + + Convey("Testing group permissions on anonymous policy", func(c C) { + conf := config.New() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf.HTTP.Port = port + + defaultVal := true + group1 := group + user1 := username + password1 := passphrase + + testString1 := getCredString(user1, password1) + htpasswdPath := test.MakeHtpasswdFileFromString(testString1) + defer os.Remove(htpasswdPath) + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + } + + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Groups: config.Groups{ + group1: { + Users: []string{user1}, + }, + }, + Repositories: config.Repositories{ + repoName: config.PolicyGroup{ + Policies: []config.Policy{ + { + Groups: []string{group1}, + Actions: []string{"read", "create", "delete"}, + }, + { + Users: []string{user1}, + Actions: []string{"delete"}, + }, + }, + DefaultPolicy: []string{}, + AnonymousPolicy: []string{"read", "create"}, + }, + }, + } + + ctlr := makeController(conf, tempDir, "") + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + cfg, layers, manifest, err := test.GetImageComponents(10000) + So(err, ShouldBeNil) + + err = test.UploadImageWithBasicAuth( + test.Image{ + Config: cfg, + Layers: layers, + Manifest: manifest, + }, baseURL, repoName, + "", "") + + So(err, ShouldBeNil) + }) }) } diff --git a/pkg/api/ldap.go b/pkg/api/ldap.go index 84bb78c3..a5695dda 100644 --- a/pkg/api/ldap.go +++ b/pkg/api/ldap.go @@ -26,6 +26,7 @@ type LDAPClient struct { BindDN string BindPassword string GroupFilter string // e.g. "(memberUid=%s)" + UserGroupAttribute string // e.g. "memberOf" Host string ServerName string UserFilter string // e.g. "(uid=%s)" @@ -121,14 +122,14 @@ func sleepAndRetry(retries, maxRetries int) bool { } // Authenticate authenticates the user against the ldap backend. -func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string]string, error) { +func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string]string, []string, error) { // serialize LDAP calls since some LDAP servers don't allow searches when binds are in flight lc.lock.Lock() defer lc.lock.Unlock() if password == "" { // RFC 4513 section 5.1.2 - return false, nil, errors.ErrLDAPEmptyPassphrase + return false, nil, nil, errors.ErrLDAPEmptyPassphrase } connected := false @@ -158,11 +159,13 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] if !connected { lc.Log.Error().Err(errors.ErrLDAPBadConn).Msg("exhausted all retries") - return false, nil, errors.ErrLDAPBadConn + return false, nil, nil, errors.ErrLDAPBadConn } attributes := lc.Attributes attributes = append(attributes, "dn") + attributes = append(attributes, lc.UserGroupAttribute) + searchScope := ldap.ScopeSingleLevel if lc.SubtreeSearch { @@ -183,7 +186,7 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username). Str("baseDN", lc.Base).Msg("search failed") - return false, nil, err + return false, nil, nil, err } if len(search.Entries) < 1 { @@ -191,7 +194,7 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username). Str("baseDN", lc.Base).Msg("entries not found") - return false, nil, err + return false, nil, nil, err } if len(search.Entries) > 1 { @@ -199,10 +202,12 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username). Str("baseDN", lc.Base).Msg("too many entries") - return false, nil, err + return false, nil, nil, err } userDN := search.Entries[0].DN + userAttributes := search.Entries[0].Attributes[0] + userGroups := userAttributes.Values user := map[string]string{} for _, attr := range lc.Attributes { @@ -214,8 +219,8 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] if err != nil { lc.Log.Error().Err(err).Str("bindDN", userDN).Msg("user bind failed") - return false, user, err + return false, user, userGroups, err } - return true, user, nil + return true, user, userGroups, nil } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 30b8ceb4..0e340dab 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -62,7 +62,7 @@ func (rh *RouteHandler) SetupRoutes() { prefixedRouter.Use(AuthHandler(rh.c)) // authz is being enabled if AccessControl is specified // if Authn is not present AccessControl will have only default policies - if rh.c.Config.AccessControl != nil && !isBearerAuthEnabled(rh.c.Config) { + if rh.c.Config.HTTP.AccessControl != nil && !isBearerAuthEnabled(rh.c.Config) { if isAuthnEnabled(rh.c.Config) { rh.c.Log.Info().Msg("access control is being enabled") } else { @@ -1521,8 +1521,7 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request * combineRepoList = append(combineRepoList, repos...) } - var repos []string - + repos := make([]string, 0) // authz context acCtx, err := localCtx.GetAccessControlContext(request.Context()) if err != nil { diff --git a/pkg/cli/config_reloader_test.go b/pkg/cli/config_reloader_test.go index 3dd10bf2..9d13b8a7 100644 --- a/pkg/cli/config_reloader_test.go +++ b/pkg/cli/config_reloader_test.go @@ -57,14 +57,16 @@ func TestConfigReloader(t *testing.T) { "failDelay": 1 }, "accessControl": { - "**": { - "policies": [ - { - "users": ["charlie"], - "actions": ["read"] + "repositories": { + "**": { + "policies": [ + { + "users": ["charlie"], + "actions": ["read"] + } + ], + "defaultPolicy": ["read", "create"] } - ], - "defaultPolicy": ["read", "create"] }, "adminPolicy": { "users": ["admin"], @@ -113,14 +115,16 @@ func TestConfigReloader(t *testing.T) { "failDelay": 1 }, "accessControl": { - "**": { - "policies": [ - { - "users": ["alice"], - "actions": ["read", "create", "update", "delete"] + "repositories": { + "**": { + "policies": [ + { + "users": ["alice"], + "actions": ["read", "create", "update", "delete"] + } + ], + "defaultPolicy": ["read"] } - ], - "defaultPolicy": ["read"] }, "adminPolicy": { "users": ["admin"], diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 1267d0b3..20f36544 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -363,7 +363,7 @@ func validateConfiguration(config *config.Config) error { } // check authorization config, it should have basic auth enabled or ldap - if config.HTTP.RawAccessControl != nil { + if config.HTTP.AccessControl != nil { // checking for anonymous policy only authorization config: no users, no policies but anonymous policy if err := validateAuthzPolicies(config); err != nil { return err @@ -405,8 +405,8 @@ func validateConfiguration(config *config.Config) error { } // check glob patterns in authz config are compilable - if config.AccessControl != nil { - for pattern := range config.AccessControl.Repositories { + if config.HTTP.AccessControl != nil { + for pattern := range config.HTTP.AccessControl.Repositories { ok := glob.ValidatePattern(pattern) if !ok { log.Error().Err(glob.ErrBadPattern).Str("pattern", pattern).Msg("authorization pattern could not be compiled") @@ -603,13 +603,6 @@ func LoadConfiguration(config *config.Config, configPath string) error { return errors.ErrBadConfig } - err := config.LoadAccessControlConfig(viperInstance) - if err != nil { - log.Error().Err(err).Msg("unable to unmarshal config's accessControl") - - return err - } - // defaults applyDefaultValues(config, viperInstance) @@ -625,7 +618,7 @@ func LoadConfiguration(config *config.Config, configPath string) error { } func authzContainsOnlyAnonymousPolicy(cfg *config.Config) bool { - adminPolicy := cfg.AccessControl.AdminPolicy + adminPolicy := cfg.HTTP.AccessControl.AdminPolicy anonymousPolicyPresent := false log.Info().Msg("checking if anonymous authorization is the only type of authorization policy configured") @@ -636,7 +629,7 @@ func authzContainsOnlyAnonymousPolicy(cfg *config.Config) bool { return false } - for _, repository := range cfg.AccessControl.Repositories { + for _, repository := range cfg.HTTP.AccessControl.Repositories { if len(repository.DefaultPolicy) > 0 { log.Info().Interface("repository", repository). Msg("default policy detected, anonymous authorization is not the only authorization policy configured") diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 61e11fa2..3321a82e 100644 --- a/pkg/cli/root_test.go +++ b/pkg/cli/root_test.go @@ -681,7 +681,7 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "accessControl":{"adminPolicy":{"users":["admin"], + "accessControl":{"repositories":{},"adminPolicy":{"users":["admin"], "actions":["read","create","update","delete"]}}}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) @@ -698,7 +698,7 @@ func TestVerify(t *testing.T) { content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", "auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}, - "accessControl":{"adminPolicy":{"users":["admin"], + "accessControl":{"repositories":{},"adminPolicy":{"users":["admin"], "actions":["read","create","update","delete"]}}}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) @@ -714,8 +714,8 @@ func TestVerify(t *testing.T) { defer os.Remove(tmpfile.Name()) // clean up content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", - "accessControl":{"**":{"anonymousPolicy": ["read", "create"]}, - "/repo":{"anonymousPolicy": ["read", "create"]} + "accessControl":{"repositories":{"**":{"anonymousPolicy": ["read", "create"]}, + "/repo":{"anonymousPolicy": ["read", "create"]}} }}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) @@ -732,8 +732,10 @@ func TestVerify(t *testing.T) { content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", "accessControl":{ - "**":{"defaultPolicy": ["read", "create"]}, - "/repo":{"anonymousPolicy": ["read", "create"]}, + "repositories":{ + "**":{"defaultPolicy": ["read", "create"]}, + "/repo":{"anonymousPolicy": ["read", "create"]}, + }, "adminPolicy":{ "users":["admin"], "actions":["read","create","update","delete"] @@ -755,8 +757,10 @@ func TestVerify(t *testing.T) { content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", "accessControl":{ - "**":{"defaultPolicy": ["read", "create"]}, - "/repo":{"anonymousPolicy": ["read", "create"]} + "repositories": { + "**":{"defaultPolicy": ["read", "create"]}, + "/repo":{"anonymousPolicy": ["read", "create"]} + } } }}`) _, err = tmpfile.Write(content) @@ -774,12 +778,14 @@ func TestVerify(t *testing.T) { content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", "accessControl":{ - "/repo":{"anonymousPolicy": ["read", "create"]}, - "/repo2":{ - "policies": [{ - "users": ["charlie"], - "actions": ["read", "create", "update"] - }] + "repositories": { + "/repo":{"anonymousPolicy": ["read", "create"]}, + "/repo2":{ + "policies": [{ + "users": ["charlie"], + "actions": ["read", "create", "update"] + }] + } } } }}`) diff --git a/pkg/requestcontext/context.go b/pkg/requestcontext/context.go index f3f50d70..a5981802 100644 --- a/pkg/requestcontext/context.go +++ b/pkg/requestcontext/context.go @@ -26,6 +26,7 @@ type AccessControlContext struct { DmcGlobPatterns map[string]bool IsAdmin bool Username string + Groups []string } func GetAccessControlContext(ctx context.Context) (*AccessControlContext, error) { @@ -63,10 +64,18 @@ func (acCtx *AccessControlContext) matchesRepo(globPatterns map[string]bool, rep // returns either a user has or not read rights on 'repository'. func (acCtx *AccessControlContext) CanReadRepo(repository string) bool { - return acCtx.matchesRepo(acCtx.ReadGlobPatterns, repository) + if acCtx.ReadGlobPatterns != nil { + return acCtx.matchesRepo(acCtx.ReadGlobPatterns, repository) + } + + return true } // returns either a user has or not detectManifestCollision rights on 'repository'. func (acCtx *AccessControlContext) CanDetectManifestCollision(repository string) bool { - return acCtx.matchesRepo(acCtx.DmcGlobPatterns, repository) + if acCtx.DmcGlobPatterns != nil { + return acCtx.matchesRepo(acCtx.DmcGlobPatterns, repository) + } + + return false } diff --git a/pkg/test/common_test.go b/pkg/test/common_test.go index 9223bfc5..6b38a23f 100644 --- a/pkg/test/common_test.go +++ b/pkg/test/common_test.go @@ -476,7 +476,7 @@ func TestUploadImage(t *testing.T) { conf.HTTP.Port = port - conf.AccessControl = &config.AccessControlConfig{ + conf.HTTP.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ "repo": config.PolicyGroup{ Policies: []config.Policy{ diff --git a/test/blackbox/anonymous_policy.bats b/test/blackbox/anonymous_policy.bats index 690ad2aa..75e1e63a 100644 --- a/test/blackbox/anonymous_policy.bats +++ b/test/blackbox/anonymous_policy.bats @@ -31,20 +31,22 @@ function setup_file() { } }, "accessControl": { - "**": { - "anonymousPolicy": ["read"], - "policies": [ - { - "users": [ - "test" - ], - "actions": [ - "read", - "create", - "update" - ] - } - ] + "repositories": { + "**": { + "anonymousPolicy": ["read"], + "policies": [ + { + "users": [ + "test" + ], + "actions": [ + "read", + "create", + "update" + ] + } + ] + } } } }, diff --git a/test/blackbox/detect_manifest_collision.bats b/test/blackbox/detect_manifest_collision.bats index bab8a02a..d494e62b 100644 --- a/test/blackbox/detect_manifest_collision.bats +++ b/test/blackbox/detect_manifest_collision.bats @@ -31,25 +31,27 @@ function setup_file() { } }, "accessControl": { - "**": { - "anonymousPolicy": [ - "read", - "create", - "delete", - "detectManifestCollision" - ], - "policies": [ - { - "users": [ - "test" - ], - "actions": [ - "read", - "create", - "delete" - ] - } - ] + "repositories": { + "**": { + "anonymousPolicy": [ + "read", + "create", + "delete", + "detectManifestCollision" + ], + "policies": [ + { + "users": [ + "test" + ], + "actions": [ + "read", + "create", + "delete" + ] + } + ] + } } } },