mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 04:48:26 +08: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:
committed by
Ramkumar Chinchani
parent
3177f87403
commit
4f825a5e2f
+62
-22
@@ -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
|
||||
|
||||
+117
-16
@@ -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")
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user