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" + ] + } + ] + } } } },