0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-02-03 23:09:41 -05:00

[Identity-based Authorization] Add an option to specify a global policy for all repositories

using regex.

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
Petu Eusebiu 2021-09-10 18:23:26 +03:00 committed by Ramkumar Chinchani
parent 3177f87403
commit 4f825a5e2f
8 changed files with 332 additions and 101 deletions

View file

@ -186,9 +186,35 @@ identities. An additional per-repository default policy can be specified for
identities not in the whitelist. Furthermore, a global admin policy can also be identities not in the whitelist. Furthermore, a global admin policy can also be
specified which can override per-repository policies. specified which can override per-repository policies.
Glob patterns can also be used as repository paths.
Authorization is granted based on the longest path matched.
For example repos2/repo repository will match both "**" and "repos2/repo" keys,
in such case repos2/repo policy will be used because it's longer.
Because we use longest path matching we need a way to specify a global policy to override all the other policies.
For example, we can specify a global policy with "**" (will match all repos), but any other policy will overwrite it,
because it will be longer. So that's why we have the option to specify an adminPolicy.
Basically '**' means repositories not matched by any other per-repository policy.
create/update/delete can not be used without 'read' action, make sure read is always included in policies!
``` ```
"accessControl": { "accessControl": {
"repos1/repo": { "**": { # matches all repos (which are not matched by any other per-repository policy)
"policies": [ # user based policies
{
"users": ["charlie"],
"actions": ["read", "create", "update"]
}
],
"defaultPolicy": ["read", "create"] # default policy which is applied for all users => so all users can read/create repositories
},
"tmp/**": { # matches all repos under tmp/ recursively
"defaultPolicy": ["read", "create", "update"] # so all users have read/create/update on all repos under tmp/ eg: tmp/infra/repo
},
"infra/*": { # matches all repos directly under infra/ (not recursively)
"policies": [ "policies": [
{ {
"users": ["alice", "bob"], "users": ["alice", "bob"],
@ -201,7 +227,7 @@ specified which can override per-repository policies.
], ],
"defaultPolicy": ["read"] "defaultPolicy": ["read"]
}, },
"repos2/repo": { "repos2/repo": { # matches only repos2/repo repository
"policies": [ "policies": [
{ {
"users": ["bob"], "users": ["bob"],
@ -214,13 +240,15 @@ specified which can override per-repository policies.
], ],
"defaultPolicy": ["read"] "defaultPolicy": ["read"]
}, },
"adminPolicy": { "adminPolicy": { # global admin policy (overrides per-repo policy)
"users": ["admin"], "users": ["admin"],
"actions": ["read", "create", "update", "delete"] "actions": ["read", "create", "update", "delete"]
} }
} }
``` ```
## Logging ## Logging
Enable and configure logging with: Enable and configure logging with:

View file

@ -14,7 +14,19 @@
"failDelay": 1 "failDelay": 1
}, },
"accessControl": { "accessControl": {
"repos1/repo": { "**": {
"policies": [
{
"users": ["charlie"],
"actions": ["read", "create", "update"]
}
],
"defaultPolicy": ["read", "create"]
},
"tmp/**": {
"defaultPolicy": ["read", "create", "update"]
},
"infra/**": {
"policies": [ "policies": [
{ {
"users": ["alice", "bob"], "users": ["alice", "bob"],
@ -30,7 +42,7 @@
"repos2/repo": { "repos2/repo": {
"policies": [ "policies": [
{ {
"users": ["bob"], "users": ["charlie"],
"actions": ["read", "create"] "actions": ["read", "create"]
}, },
{ {

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
glob "github.com/bmatcuk/doublestar/v4"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
@ -33,7 +34,7 @@ type AccessController struct {
// AccessControlContext context passed down to http.Handlers. // AccessControlContext context passed down to http.Handlers.
type AccessControlContext struct { type AccessControlContext struct {
userAllowedRepos []string globPatterns map[string]bool
isAdmin bool isAdmin bool
} }
@ -44,27 +45,49 @@ func NewAccessController(config *config.Config) *AccessController {
} }
} }
// getReadRepos get repositories from config file that the user has READ perms. // getReadRepos get glob patterns from config file that the user has or doesn't have READ perms.
func (ac *AccessController) getReadRepos(username string) []string { // used to filter /v2/_catalog repositories based on user rights.
var repos []string func (ac *AccessController) getReadGlobPatterns(username string) map[string]bool {
globPatterns := make(map[string]bool)
for r, pg := range ac.Config.Repositories { for pattern, policyGroup := range ac.Config.Repositories {
for _, p := range pg.Policies { // check default policy
if (contains(p.Users, username) && contains(p.Actions, READ)) || if contains(policyGroup.DefaultPolicy, READ) {
contains(pg.DefaultPolicy, READ) { globPatterns[pattern] = true
repos = append(repos, r)
} }
// check user based policy
for _, p := range policyGroup.Policies {
if contains(p.Users, username) && contains(p.Actions, READ) {
globPatterns[pattern] = true
} }
} }
return repos // if not allowed then mark it
if _, ok := globPatterns[pattern]; !ok {
globPatterns[pattern] = false
}
}
return globPatterns
} }
// can verifies if a user can do action on repository. // can verifies if a user can do action on repository.
func (ac *AccessController) can(username, action, repository string) bool { func (ac *AccessController) can(username, action, repository string) bool {
can := false can := false
// check repo based policy
pg, ok := ac.Config.Repositories[repository] var longestMatchedPattern string
for pattern := range ac.Config.Repositories {
matched, err := glob.Match(pattern, repository)
if err == nil {
if matched && len(pattern) > len(longestMatchedPattern) {
longestMatchedPattern = pattern
}
}
}
// check matched repo based policy
pg, ok := ac.Config.Repositories[longestMatchedPattern]
if ok { if ok {
can = isPermitted(username, action, pg) can = isPermitted(username, action, pg)
} }
@ -86,8 +109,8 @@ func (ac *AccessController) isAdmin(username string) bool {
// getContext builds ac context(allowed to read repos and if user is admin) and returns it. // 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 { func (ac *AccessController) getContext(username string, request *http.Request) context.Context {
userAllowedRepos := ac.getReadRepos(username) readGlobPatterns := ac.getReadGlobPatterns(username)
acCtx := AccessControlContext{userAllowedRepos: userAllowedRepos} acCtx := AccessControlContext{globPatterns: readGlobPatterns}
if ac.isAdmin(username) { if ac.isAdmin(username) {
acCtx.isAdmin = true acCtx.isAdmin = true
@ -132,14 +155,23 @@ func contains(slice []string, item string) bool {
return false return false
} }
func containsRepo(slice []string, item string) bool { // returns either a user has or not rights on 'repository'.
for _, v := range slice { func matchesRepo(globPatterns map[string]bool, repository string) bool {
if strings.HasPrefix(item, v) { var longestMatchedPattern string
return true
// because of the longest path matching rule, we need to check all patterns from config
for pattern := range globPatterns {
matched, err := glob.Match(pattern, repository)
if err == nil {
if matched && len(pattern) > len(longestMatchedPattern) {
longestMatchedPattern = pattern
}
} }
} }
return false allowed := globPatterns[longestMatchedPattern]
return allowed
} }
func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc { func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
@ -149,11 +181,19 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
resource := vars["name"] resource := vars["name"]
reference, ok := vars["reference"] reference, ok := vars["reference"]
// bypass authz for /v2/ route
if request.RequestURI == "/v2/" {
next.ServeHTTP(response, request)
return
}
acCtrlr := NewAccessController(ctlr.Config) acCtrlr := NewAccessController(ctlr.Config)
username := getUsername(request) username := getUsername(request)
ctx := acCtrlr.getContext(username, request) ctx := acCtrlr.getContext(username, request)
if request.RequestURI == "/v2/_catalog" || request.RequestURI == "/v2/" { // will return only repos on which client is authorized to read
if request.RequestURI == "/v2/_catalog" {
next.ServeHTTP(response, request.WithContext(ctx)) next.ServeHTTP(response, request.WithContext(ctx))
return return
@ -193,7 +233,7 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
} }
func getUsername(r *http.Request) string { func getUsername(r *http.Request) string {
// this should work because it worked in auth middleware // this should work because it was already parsed in authn middleware
basicAuth := r.Header.Get("Authorization") basicAuth := r.Header.Get("Authorization")
s := strings.SplitN(basicAuth, " ", 2) //nolint:gomnd s := strings.SplitN(basicAuth, " ", 2) //nolint:gomnd
b, _ := base64.StdEncoding.DecodeString(s[1]) b, _ := base64.StdEncoding.DecodeString(s[1])

View file

@ -162,7 +162,7 @@ func (c *Config) Validate(log log.Logger) error {
} }
// LoadAccessControlConfig populates config.AccessControl struct with values from config. // LoadAccessControlConfig populates config.AccessControl struct with values from config.
func (c *Config) LoadAccessControlConfig() error { func (c *Config) LoadAccessControlConfig(viperInstance *viper.Viper) error {
if c.HTTP.RawAccessControl == nil { if c.HTTP.RawAccessControl == nil {
return nil return nil
} }
@ -176,19 +176,19 @@ func (c *Config) LoadAccessControlConfig() error {
var policyGroup PolicyGroup var policyGroup PolicyGroup
if policy == "adminpolicy" { if policy == "adminpolicy" {
adminPolicy := viper.GetStringMapStringSlice("http.accessControl.adminPolicy") adminPolicy := viperInstance.GetStringMapStringSlice("http::accessControl::adminPolicy")
c.AccessControl.AdminPolicy.Actions = adminPolicy["actions"] c.AccessControl.AdminPolicy.Actions = adminPolicy["actions"]
c.AccessControl.AdminPolicy.Users = adminPolicy["users"] c.AccessControl.AdminPolicy.Users = adminPolicy["users"]
continue continue
} }
err := viper.UnmarshalKey(fmt.Sprintf("http.accessControl.%s.policies", policy), &policies) err := viperInstance.UnmarshalKey(fmt.Sprintf("http::accessControl::%s::policies", policy), &policies)
if err != nil { if err != nil {
return err return err
} }
defaultPolicy := viper.GetStringSlice(fmt.Sprintf("http.accessControl.%s.defaultPolicy", policy)) defaultPolicy := viperInstance.GetStringSlice(fmt.Sprintf("http::accessControl::%s::defaultPolicy", policy))
policyGroup.Policies = policies policyGroup.Policies = policies
policyGroup.DefaultPolicy = defaultPolicy policyGroup.DefaultPolicy = defaultPolicy
c.AccessControl.Repositories[policy] = policyGroup c.AccessControl.Repositories[policy] = policyGroup

View file

@ -56,6 +56,7 @@ const (
UnauthorizedNamespace = "fortknox/notallowed" UnauthorizedNamespace = "fortknox/notallowed"
ALICE = "alice" ALICE = "alice"
AuthorizationNamespace = "authz/image" AuthorizationNamespace = "authz/image"
AuthorizationAllRepos = "**"
) )
type ( type (
@ -1752,7 +1753,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
} }
conf.AccessControl = &config.AccessControlConfig{ conf.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{ Repositories: config.Repositories{
AuthorizationNamespace: config.PolicyGroup{ AuthorizationAllRepos: config.PolicyGroup{
Policies: []config.Policy{ Policies: []config.Policy{
{ {
Users: []string{}, Users: []string{},
@ -1787,8 +1788,14 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
blob := []byte("hello, blob!") blob := []byte("hello, blob!")
digest := godigest.FromBytes(blob).String() digest := godigest.FromBytes(blob).String()
// unauthenticated clients should not have access to /v2/
resp, err := resty.R().Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
// everybody should have access to /v2/ // everybody should have access to /v2/
resp, err := resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/") Get(baseURL + "/v2/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
@ -1804,7 +1811,6 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
err = json.Unmarshal(resp.Body(), &e) err = json.Unmarshal(resp.Body(), &e)
So(err, ShouldBeNil) So(err, ShouldBeNil)
// first let's use only repositories based policies
// should get 403 without create // should get 403 without create
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
@ -1812,11 +1818,13 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
// add test user to repo's policy with create perm // first let's use global based policies
conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = // add test user to global policy with create perm
append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users =
conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test")
append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create")
conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions =
append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create")
// now it should get 202 // now it should get 202
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
@ -1837,18 +1845,104 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated) So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
// head blob should get 403 with read perm // head blob should get 403 without read perm
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
// get blob should get 403 without read perm // get tags without read access should get 403
resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
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")
resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// head blob should get 200 now
resp, err = resty.R().SetBasicAuth(username, passphrase).
Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// get blob should get 200 now
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// delete blob should get 403 without delete perm
resp, err = resty.R().SetBasicAuth(username, passphrase).
Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
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")
// delete blob should get 202
resp, err = resty.R().SetBasicAuth(username, passphrase).
Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// 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{
Policies: []config.Policy{
{
Users: []string{},
Actions: []string{},
},
},
DefaultPolicy: []string{},
}
conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users =
append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test")
conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions =
append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create")
// now it should get 202
resp, err = resty.R().SetBasicAuth(username, passphrase).
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
loc = resp.Header().Get("Location")
// uploading blob should get 201
resp, err = resty.R().SetBasicAuth(username, passphrase).
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
SetHeader("Content-Type", "application/octet-stream").
SetQueryParam("digest", digest).
SetBody(blob).
Put(baseURL + loc)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
// head blob should get 403 without read perm
resp, err = resty.R().SetBasicAuth(username, passphrase).
Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
// get tags without read access should get 403 // get tags without read access should get 403
@ -1861,6 +1955,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
// get tags with read access should get 200 // get tags with read access should get 200
conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions =
append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read")
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -1899,6 +1994,12 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// remove permissions on **/* so it will not interfere with zot-test namespace
repoPolicy := conf.AccessControl.Repositories[AuthorizationAllRepos]
repoPolicy.Policies = []config.Policy{}
repoPolicy.DefaultPolicy = []string{}
conf.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
// get manifest should get 403, we don't have perm at all on this repo // get manifest should get 403, we don't have perm at all on this repo
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/zot-test/manifests/0.0.1") Get(baseURL + "/v2/zot-test/manifests/0.0.1")
@ -1965,7 +2066,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
// now use default repo policy // now use default repo policy
conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} conf.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{}
repoPolicy := conf.AccessControl.Repositories["zot-test"] repoPolicy = conf.AccessControl.Repositories["zot-test"]
repoPolicy.DefaultPolicy = []string{"update"} repoPolicy.DefaultPolicy = []string{"update"}
conf.AccessControl.Repositories["zot-test"] = repoPolicy conf.AccessControl.Repositories["zot-test"] = repoPolicy
@ -2006,17 +2107,17 @@ func TestAuthorizationWithBasicAuth(t *testing.T) {
repoPolicy.DefaultPolicy = []string{} repoPolicy.DefaultPolicy = []string{}
conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy conf.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
repoPolicy = conf.AccessControl.Repositories["zot-test"]
repoPolicy.Policies = []config.Policy{}
repoPolicy.DefaultPolicy = []string{}
conf.AccessControl.Repositories["zot-test"] = repoPolicy
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
// let's use admin policy
// remove all repo based policy
delete(conf.AccessControl.Repositories, AuthorizationNamespace)
delete(conf.AccessControl.Repositories, "zot-test")
// whithout any perm should get 403 // whithout any perm should get 403
resp, err = resty.R().SetBasicAuth(username, passphrase). resp, err = resty.R().SetBasicAuth(username, passphrase).
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")

View file

@ -1234,7 +1234,7 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
} }
for _, r := range combineRepoList { for _, r := range combineRepoList {
if containsRepo(acCtx.userAllowedRepos, r) || acCtx.isAdmin { if acCtx.isAdmin || matchesRepo(acCtx.globPatterns, r) {
repos = append(repos, r) repos = append(repos, r)
} }
} }

View file

@ -186,25 +186,7 @@ func NewRootCmd() *cobra.Command {
return rootCmd return rootCmd
} }
func LoadConfiguration(config *config.Config, configPath string) { func validateConfiguration(config *config.Config) {
viper.SetConfigFile(configPath)
if err := viper.ReadInConfig(); err != nil {
log.Error().Err(err).Msg("error while reading configuration")
panic(err)
}
metaData := &mapstructure.Metadata{}
if err := viper.Unmarshal(&config, metadataConfig(metaData)); err != nil {
log.Error().Err(err).Msg("error while unmarshalling new config")
panic(err)
}
if len(metaData.Keys) == 0 || len(metaData.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Msg("bad configuration, retry writing it")
panic(errors.ErrBadConfig)
}
// check authorization config, it should have basic auth enabled or ldap // check authorization config, it should have basic auth enabled or ldap
if config.HTTP.RawAccessControl != nil { if config.HTTP.RawAccessControl != nil {
if config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil) { if config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil) {
@ -228,14 +210,14 @@ func LoadConfiguration(config *config.Config, configPath string) {
} }
} }
// check glob patterns in sync are compilable // check glob patterns in sync config are compilable
if config.Extensions != nil && config.Extensions.Sync != nil { if config.Extensions != nil && config.Extensions.Sync != nil {
for _, regCfg := range config.Extensions.Sync.Registries { for _, regCfg := range config.Extensions.Sync.Registries {
if regCfg.Content != nil { if regCfg.Content != nil {
for _, content := range regCfg.Content { for _, content := range regCfg.Content {
ok := glob.ValidatePattern(content.Prefix) ok := glob.ValidatePattern(content.Prefix)
if !ok { if !ok {
log.Error().Err(glob.ErrBadPattern).Str("pattern", content.Prefix).Msg("pattern could not be compiled") log.Error().Err(glob.ErrBadPattern).Str("pattern", content.Prefix).Msg("sync pattern could not be compiled")
panic(errors.ErrBadConfig) panic(errors.ErrBadConfig)
} }
} }
@ -260,19 +242,57 @@ func LoadConfiguration(config *config.Config, configPath string) {
} }
} }
err := config.LoadAccessControlConfig() // check glob patterns in authz config are compilable
if err != nil { if config.AccessControl != nil {
log.Error().Err(errors.ErrBadConfig).Msg("unable to unmarshal http.accessControl.key.policies") for pattern := range config.AccessControl.Repositories {
ok := glob.ValidatePattern(pattern)
if !ok {
log.Error().Err(glob.ErrBadPattern).Str("pattern", pattern).Msg("authorization pattern could not be compiled")
panic(errors.ErrBadConfig)
}
}
}
}
func LoadConfiguration(config *config.Config, configPath string) {
// Default is dot (.) but because we allow glob patterns in authz
// we need another key delimiter.
viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::"))
viperInstance.SetConfigFile(configPath)
if err := viperInstance.ReadInConfig(); err != nil {
log.Error().Err(err).Msg("error while reading configuration")
panic(err) panic(err)
} }
metaData := &mapstructure.Metadata{}
if err := viperInstance.Unmarshal(&config, metadataConfig(metaData)); err != nil {
log.Error().Err(err).Msg("error while unmarshalling new config")
panic(err)
}
if len(metaData.Keys) == 0 || len(metaData.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Msg("bad configuration, retry writing it")
panic(errors.ErrBadConfig)
}
err := config.LoadAccessControlConfig(viperInstance)
if err != nil {
log.Error().Err(err).Msg("unable to unmarshal config's accessControl")
panic(err)
}
// various config checks
validateConfiguration(config)
// defaults // defaults
defualtTLSVerify := true defaultTLSVerify := true
if config.Extensions != nil && config.Extensions.Sync != nil { if config.Extensions != nil && config.Extensions.Sync != nil {
for id, regCfg := range config.Extensions.Sync.Registries { for id, regCfg := range config.Extensions.Sync.Registries {
if regCfg.TLSVerify == nil { if regCfg.TLSVerify == nil {
config.Extensions.Sync.Registries[id].TLSVerify = &defualtTLSVerify config.Extensions.Sync.Registries[id].TLSVerify = &defaultTLSVerify
} }
} }
} }

View file

@ -10,7 +10,6 @@ import (
"time" "time"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/spf13/viper"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
@ -188,6 +187,40 @@ func TestVerify(t *testing.T) {
So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic)
}) })
Convey("Test verify with bad authorization repo patterns", t, func(c C) {
tmpfile, err := ioutil.TempFile("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
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":{"\|":{"policies":[],"defaultPolicy":[]}}}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
err = tmpfile.Close()
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic)
})
Convey("Test verify sync config default tls value", t, func(c C) {
tmpfile, err := ioutil.TempFile("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
"extensions":{"sync": {"registries": [{"url":"localhost:9999",
"content": [{"prefix":"repo**"}]}]}}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
err = tmpfile.Close()
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
err = cli.NewRootCmd().Execute()
So(err, ShouldBeNil)
})
Convey("Test verify good config", t, func(c C) { Convey("Test verify good config", t, func(c C) {
tmpfile, err := ioutil.TempFile("", "zot-test*.json") tmpfile, err := ioutil.TempFile("", "zot-test*.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -209,9 +242,6 @@ func TestLoadConfig(t *testing.T) {
Convey("Test viper load config", t, func(c C) { Convey("Test viper load config", t, func(c C) {
config := config.New() config := config.New()
So(func() { cli.LoadConfiguration(config, "../../examples/config-policy.json") }, ShouldNotPanic) So(func() { cli.LoadConfiguration(config, "../../examples/config-policy.json") }, ShouldNotPanic)
adminPolicy := viper.GetStringMapStringSlice("http.accessControl.adminPolicy")
So(config.AccessControl.AdminPolicy.Actions, ShouldResemble, adminPolicy["actions"])
So(config.AccessControl.AdminPolicy.Users, ShouldResemble, adminPolicy["users"])
}) })
} }