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:
parent
79783b4b06
commit
336526065f
@ -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": {
|
||||
|
73
examples/config-ldap.json
Normal file
73
examples/config-ldap.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"],
|
||||
|
@ -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")
|
||||
|
@ -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"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}}`)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user