From 4f825a5e2fe801ad39d71939454c0108dd1e0a21 Mon Sep 17 00:00:00 2001 From: Petu Eusebiu Date: Fri, 10 Sep 2021 18:23:26 +0300 Subject: [PATCH] [Identity-based Authorization] Add an option to specify a global policy for all repositories using regex. Signed-off-by: Petu Eusebiu --- examples/README.md | 64 ++++++++++++----- examples/config-policy.json | 16 ++++- pkg/api/authz.go | 84 +++++++++++++++++------ pkg/api/config/config.go | 8 +-- pkg/api/controller_test.go | 133 +++++++++++++++++++++++++++++++----- pkg/api/routes.go | 2 +- pkg/cli/root.go | 88 +++++++++++++++--------- pkg/cli/root_test.go | 38 +++++++++-- 8 files changed, 332 insertions(+), 101 deletions(-) diff --git a/examples/README.md b/examples/README.md index a974ab07..47f38bca 100644 --- a/examples/README.md +++ b/examples/README.md @@ -186,41 +186,69 @@ identities. An additional per-repository default policy can be specified for identities not in the whitelist. Furthermore, a global admin policy can also be 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": { - "repos1/repo": { - "policies": [ + "**": { # matches all repos (which are not matched by any other per-repository policy) + "policies": [ # user based policies { - "users": ["alice", "bob"], - "actions": ["create", "read", "update", "delete"] - }, - { - "users": ["mallory"], - "actions": ["create", "read"] + "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": [ + { + "users": ["alice", "bob"], + "actions": ["create", "read", "update", "delete"] + }, + { + "users": ["mallory"], + "actions": ["create", "read"] + } ], "defaultPolicy": ["read"] }, - "repos2/repo": { + "repos2/repo": { # matches only repos2/repo repository "policies": [ - { - "users": ["bob"], - "actions": ["read", "create"] - }, - { - "users": ["mallory"], - "actions": ["create", "read"] - } + { + "users": ["bob"], + "actions": ["read", "create"] + }, + { + "users": ["mallory"], + "actions": ["create", "read"] + } ], "defaultPolicy": ["read"] }, - "adminPolicy": { + "adminPolicy": { # global admin policy (overrides per-repo policy) "users": ["admin"], "actions": ["read", "create", "update", "delete"] } } ``` + + ## Logging Enable and configure logging with: diff --git a/examples/config-policy.json b/examples/config-policy.json index fda5b019..6d68196b 100644 --- a/examples/config-policy.json +++ b/examples/config-policy.json @@ -14,7 +14,19 @@ "failDelay": 1 }, "accessControl": { - "repos1/repo": { + "**": { + "policies": [ + { + "users": ["charlie"], + "actions": ["read", "create", "update"] + } + ], + "defaultPolicy": ["read", "create"] + }, + "tmp/**": { + "defaultPolicy": ["read", "create", "update"] + }, + "infra/**": { "policies": [ { "users": ["alice", "bob"], @@ -30,7 +42,7 @@ "repos2/repo": { "policies": [ { - "users": ["bob"], + "users": ["charlie"], "actions": ["read", "create"] }, { diff --git a/pkg/api/authz.go b/pkg/api/authz.go index 20221704..edf397c0 100644 --- a/pkg/api/authz.go +++ b/pkg/api/authz.go @@ -7,6 +7,7 @@ import ( "strings" "time" + glob "github.com/bmatcuk/doublestar/v4" "github.com/gorilla/mux" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/log" @@ -33,8 +34,8 @@ type AccessController struct { // AccessControlContext context passed down to http.Handlers. type AccessControlContext struct { - userAllowedRepos []string - isAdmin bool + globPatterns map[string]bool + isAdmin bool } func NewAccessController(config *config.Config) *AccessController { @@ -44,27 +45,49 @@ func NewAccessController(config *config.Config) *AccessController { } } -// getReadRepos get repositories from config file that the user has READ perms. -func (ac *AccessController) getReadRepos(username string) []string { - var repos []string +// getReadRepos get glob patterns from config file that the user has or doesn't have READ perms. +// used to filter /v2/_catalog repositories based on user rights. +func (ac *AccessController) getReadGlobPatterns(username string) map[string]bool { + globPatterns := make(map[string]bool) - for r, pg := range ac.Config.Repositories { - for _, p := range pg.Policies { - if (contains(p.Users, username) && contains(p.Actions, READ)) || - contains(pg.DefaultPolicy, READ) { - repos = append(repos, r) + for pattern, policyGroup := range ac.Config.Repositories { + // check default policy + if contains(policyGroup.DefaultPolicy, READ) { + globPatterns[pattern] = true + } + // check user based policy + for _, p := range policyGroup.Policies { + if contains(p.Users, username) && contains(p.Actions, READ) { + globPatterns[pattern] = true } } + + // if not allowed then mark it + if _, ok := globPatterns[pattern]; !ok { + globPatterns[pattern] = false + } } - return repos + return globPatterns } // can verifies if a user can do action on repository. func (ac *AccessController) can(username, action, repository string) bool { 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 { 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. func (ac *AccessController) getContext(username string, request *http.Request) context.Context { - userAllowedRepos := ac.getReadRepos(username) - acCtx := AccessControlContext{userAllowedRepos: userAllowedRepos} + readGlobPatterns := ac.getReadGlobPatterns(username) + acCtx := AccessControlContext{globPatterns: readGlobPatterns} if ac.isAdmin(username) { acCtx.isAdmin = true @@ -132,14 +155,23 @@ func contains(slice []string, item string) bool { return false } -func containsRepo(slice []string, item string) bool { - for _, v := range slice { - if strings.HasPrefix(item, v) { - return true +// returns either a user has or not rights on 'repository'. +func matchesRepo(globPatterns map[string]bool, repository string) bool { + var longestMatchedPattern string + + // 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 { @@ -149,11 +181,19 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc { resource := vars["name"] reference, ok := vars["reference"] + // bypass authz for /v2/ route + if request.RequestURI == "/v2/" { + next.ServeHTTP(response, request) + + return + } + acCtrlr := NewAccessController(ctlr.Config) username := getUsername(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)) return @@ -193,7 +233,7 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc { } 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") s := strings.SplitN(basicAuth, " ", 2) //nolint:gomnd b, _ := base64.StdEncoding.DecodeString(s[1]) diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index b5866b58..6be1d101 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -162,7 +162,7 @@ func (c *Config) Validate(log log.Logger) error { } // 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 { return nil } @@ -176,19 +176,19 @@ func (c *Config) LoadAccessControlConfig() error { var policyGroup PolicyGroup if policy == "adminpolicy" { - adminPolicy := viper.GetStringMapStringSlice("http.accessControl.adminPolicy") + adminPolicy := viperInstance.GetStringMapStringSlice("http::accessControl::adminPolicy") c.AccessControl.AdminPolicy.Actions = adminPolicy["actions"] c.AccessControl.AdminPolicy.Users = adminPolicy["users"] 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 { 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.DefaultPolicy = defaultPolicy c.AccessControl.Repositories[policy] = policyGroup diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 46683bc5..c04a6c85 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -56,6 +56,7 @@ const ( UnauthorizedNamespace = "fortknox/notallowed" ALICE = "alice" AuthorizationNamespace = "authz/image" + AuthorizationAllRepos = "**" ) type ( @@ -1752,7 +1753,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { } conf.AccessControl = &config.AccessControlConfig{ Repositories: config.Repositories{ - AuthorizationNamespace: config.PolicyGroup{ + AuthorizationAllRepos: config.PolicyGroup{ Policies: []config.Policy{ { Users: []string{}, @@ -1787,8 +1788,14 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { blob := []byte("hello, blob!") 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/ - resp, err := resty.R().SetBasicAuth(username, passphrase). + resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) @@ -1804,7 +1811,6 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) - // first let's use only repositories based policies // should get 403 without create resp, err = resty.R().SetBasicAuth(username, passphrase). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") @@ -1812,11 +1818,13 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - // add test user to repo's policy with create perm - 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") + // 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") + + conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = + append(conf.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") // now it should get 202 resp, err = resty.R().SetBasicAuth(username, passphrase). @@ -1837,18 +1845,104 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp, ShouldNotBeNil) 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). Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) 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). Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) So(err, ShouldBeNil) 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) // 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 conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") + resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") So(err, ShouldBeNil) @@ -1899,6 +1994,12 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(resp, ShouldNotBeNil) 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 resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/zot-test/manifests/0.0.1") @@ -1965,7 +2066,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { // now use default repo policy 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"} conf.AccessControl.Repositories["zot-test"] = repoPolicy @@ -2006,17 +2107,17 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { repoPolicy.DefaultPolicy = []string{} 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). Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) 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 resp, err = resty.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 2b06f6ac..467b3a8a 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -1234,7 +1234,7 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request * } for _, r := range combineRepoList { - if containsRepo(acCtx.userAllowedRepos, r) || acCtx.isAdmin { + if acCtx.isAdmin || matchesRepo(acCtx.globPatterns, r) { repos = append(repos, r) } } diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 739ba0f9..5ca23390 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -186,25 +186,7 @@ func NewRootCmd() *cobra.Command { return rootCmd } -func LoadConfiguration(config *config.Config, configPath string) { - 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) - } - +func validateConfiguration(config *config.Config) { // check authorization config, it should have basic auth enabled or ldap if config.HTTP.RawAccessControl != 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 { for _, regCfg := range config.Extensions.Sync.Registries { if regCfg.Content != nil { for _, content := range regCfg.Content { ok := glob.ValidatePattern(content.Prefix) 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) } } @@ -260,19 +242,57 @@ func LoadConfiguration(config *config.Config, configPath string) { } } - err := config.LoadAccessControlConfig() - if err != nil { - log.Error().Err(errors.ErrBadConfig).Msg("unable to unmarshal http.accessControl.key.policies") - panic(err) - } - - // defaults - defualtTLSVerify := true - - if config.Extensions != nil && config.Extensions.Sync != nil { - for id, regCfg := range config.Extensions.Sync.Registries { - if regCfg.TLSVerify == nil { - config.Extensions.Sync.Registries[id].TLSVerify = &defualtTLSVerify + // check glob patterns in authz config are compilable + if config.AccessControl != nil { + 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) + } + + 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 + defaultTLSVerify := true + + if config.Extensions != nil && config.Extensions.Sync != nil { + for id, regCfg := range config.Extensions.Sync.Registries { + if regCfg.TLSVerify == nil { + config.Extensions.Sync.Registries[id].TLSVerify = &defaultTLSVerify } } } diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 909ec87b..7d8e8841 100644 --- a/pkg/cli/root_test.go +++ b/pkg/cli/root_test.go @@ -10,7 +10,6 @@ import ( "time" . "github.com/smartystreets/goconvey/convey" - "github.com/spf13/viper" "gopkg.in/resty.v1" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" @@ -188,6 +187,40 @@ func TestVerify(t *testing.T) { 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) { tmpfile, err := ioutil.TempFile("", "zot-test*.json") So(err, ShouldBeNil) @@ -209,9 +242,6 @@ func TestLoadConfig(t *testing.T) { Convey("Test viper load config", t, func(c C) { config := config.New() 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"]) }) }