[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:
parent
3177f87403
commit
4f825a5e2f
@ -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:
|
||||
|
@ -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"]
|
||||
},
|
||||
{
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"])
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user