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 <ana.kagome@yahoo.com>
This commit is contained in:
Lisca Ana-Roberta 2023-03-08 21:47:15 +02:00 committed by GitHub
parent 79783b4b06
commit 336526065f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1023 additions and 358 deletions

View File

@ -8,6 +8,7 @@
"port": "8080",
"realm": "zot",
"accessControl": {
"repositories": {
"**": {
"anonymousPolicy": [
"read",
@ -32,6 +33,7 @@
]
}
}
}
},
"log": {
"level": "debug"

73
examples/config-ldap.json Normal file
View File

@ -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"
}
}

View File

@ -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"
]
"groups": {
"group1": {
"users": ["jack", "john", "jane", "ana"]
},
"group2": {
"users": ["alice", "mike", "jim"]
}
],
"defaultPolicy": [
"read",
"create",
"delete",
"detectManifestCollision"
]
},
"repositories": {
"**": {
"policies": [{
"users": ["charlie"],
"groups": ["admins", "developers", "group1"],
"actions": ["read", "create", "update"]
},
{
"users": ["mary"],
"groups": ["group2"],
"actions": ["read", "create", "update", "delete"]
}],
"defaultPolicy": ["read", "create"]
},
"tmp/**": {
"defaultPolicy": [
"read",
"create",
"update"
]
"defaultPolicy": ["read", "create", "update"]
},
"infra/**": {
"policies": [
{
"users": [
"alice",
"bob"
],
"actions": [
"create",
"read",
"update",
"delete"
]
"infra/*": {
"policies": [{
"users": ["alice", "bob"],
"groups": ["maintainers","platformteam"],
"actions": ["create", "read", "update", "delete"]
},
{
"users": [
"mallory"
],
"actions": [
"create",
"read"
]
"users": ["mallory"],
"actions": ["create", "read"]
}
],
"defaultPolicy": [
"read"
]
"defaultPolicy": ["read"]
},
"repos2/repo": {
"policies": [
{
"users": [
"charlie"
],
"actions": [
"read",
"create"
]
"policies": [{
"users": ["bob"],
"groups": ["sparkle_team","repo2_team"],
"actions": ["read", "create"]
},
{
"users": [
"mallory"
],
"actions": [
"create",
"read"
]
"users": ["mallory"],
"actions": ["create", "read"]
}
],
"defaultPolicy": [
"read"
]
"defaultPolicy": ["read"]
}
},
"adminPolicy": {
"users": [
"admin"
],
"actions": [
"read",
"create",
"update",
"delete"
]
"users": ["admin"],
"groups": ["admins","developers"],
"actions": ["read", "create", "update", "delete"]
}
}
},

View File

@ -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) {

View File

@ -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 <username> has <action> 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 {

View File

@ -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"
@ -71,7 +69,7 @@ type HTTPConfig struct {
AllowOrigin string // comma separated
TLS *TLSConfig
Auth *AuthConfig
RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"`
AccessControl *AccessControlConfig
Realm string
Ratelimit *RatelimitConfig `mapstructure:",omitempty"`
}
@ -84,6 +82,7 @@ type LDAPConfig struct {
SubtreeSearch bool
Address string
BindDN string
UserGroupAttribute string
BindPassword string
BaseDN string
UserAttribute string
@ -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
}

View File

@ -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 {

View File

@ -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{
@ -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)
})
})
}

View File

@ -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
}

View File

@ -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 {

View File

@ -57,6 +57,7 @@ func TestConfigReloader(t *testing.T) {
"failDelay": 1
},
"accessControl": {
"repositories": {
"**": {
"policies": [
{
@ -65,6 +66,7 @@ func TestConfigReloader(t *testing.T) {
}
],
"defaultPolicy": ["read", "create"]
}
},
"adminPolicy": {
"users": ["admin"],
@ -113,6 +115,7 @@ func TestConfigReloader(t *testing.T) {
"failDelay": 1
},
"accessControl": {
"repositories": {
"**": {
"policies": [
{
@ -121,6 +124,7 @@ func TestConfigReloader(t *testing.T) {
}
],
"defaultPolicy": ["read"]
}
},
"adminPolicy": {
"users": ["admin"],

View File

@ -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")

View File

@ -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":{
"repositories":{
"**":{"defaultPolicy": ["read", "create"]},
"/repo":{"anonymousPolicy": ["read", "create"]},
},
"adminPolicy":{
"users":["admin"],
"actions":["read","create","update","delete"]
@ -755,9 +757,11 @@ func TestVerify(t *testing.T) {
content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"accessControl":{
"repositories": {
"**":{"defaultPolicy": ["read", "create"]},
"/repo":{"anonymousPolicy": ["read", "create"]}
}
}
}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
@ -774,6 +778,7 @@ func TestVerify(t *testing.T) {
content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"accessControl":{
"repositories": {
"/repo":{"anonymousPolicy": ["read", "create"]},
"/repo2":{
"policies": [{
@ -782,6 +787,7 @@ func TestVerify(t *testing.T) {
}]
}
}
}
}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)

View File

@ -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 {
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 {
if acCtx.DmcGlobPatterns != nil {
return acCtx.matchesRepo(acCtx.DmcGlobPatterns, repository)
}
return false
}

View File

@ -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{

View File

@ -31,6 +31,7 @@ function setup_file() {
}
},
"accessControl": {
"repositories": {
"**": {
"anonymousPolicy": ["read"],
"policies": [
@ -47,6 +48,7 @@ function setup_file() {
]
}
}
}
},
"log": {
"level": "debug"

View File

@ -31,6 +31,7 @@ function setup_file() {
}
},
"accessControl": {
"repositories": {
"**": {
"anonymousPolicy": [
"read",
@ -52,6 +53,7 @@ function setup_file() {
]
}
}
}
},
"log": {
"level": "debug"