mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
Add identity-based access control, closes #51
Add a cli subcommand to verify config files validity
This commit is contained in:
committed by
Ramkumar Chinchani
parent
26926ad4c2
commit
609d85d875
@@ -23,11 +23,7 @@ const (
|
||||
)
|
||||
|
||||
func AuthHandler(c *Controller) mux.MiddlewareFunc {
|
||||
if c.Config.HTTP.Auth != nil &&
|
||||
c.Config.HTTP.Auth.Bearer != nil &&
|
||||
c.Config.HTTP.Auth.Bearer.Cert != "" &&
|
||||
c.Config.HTTP.Auth.Bearer.Realm != "" &&
|
||||
c.Config.HTTP.Auth.Bearer.Service != "" {
|
||||
if isBearerAuthEnabled(c.Config) {
|
||||
return bearerAuthHandler(c)
|
||||
}
|
||||
|
||||
Executable
+236
@@ -0,0 +1,236 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anuvu/zot/pkg/log"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
// actions
|
||||
CREATE = "create"
|
||||
READ = "read"
|
||||
UPDATE = "update"
|
||||
DELETE = "delete"
|
||||
|
||||
// request-local context key
|
||||
authzCtxKey contextKey = 0
|
||||
)
|
||||
|
||||
type AccessControlConfig struct {
|
||||
Repositories Repositories
|
||||
AdminPolicy Policy
|
||||
}
|
||||
|
||||
type Repositories map[string]PolicyGroup
|
||||
|
||||
type PolicyGroup struct {
|
||||
Policies []Policy
|
||||
DefaultPolicy []string
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
Users []string
|
||||
Actions []string
|
||||
}
|
||||
|
||||
// AccessController authorizes users to act on resources.
|
||||
type AccessController struct {
|
||||
Config *AccessControlConfig
|
||||
Log log.Logger
|
||||
}
|
||||
|
||||
// AccessControlContext context passed down to http.Handlers.
|
||||
type AccessControlContext struct {
|
||||
userAllowedRepos []string
|
||||
isAdmin bool
|
||||
}
|
||||
|
||||
func NewAccessController(config *Config) *AccessController {
|
||||
return &AccessController{
|
||||
Config: config.AccessControl,
|
||||
Log: log.NewLogger(config.Log.Level, config.Log.Output),
|
||||
}
|
||||
}
|
||||
|
||||
// getReadRepos get repositories from config file that the user has READ perms.
|
||||
func (ac *AccessController) getReadRepos(username string) []string {
|
||||
var repos []string
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return repos
|
||||
}
|
||||
|
||||
// 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]
|
||||
if ok {
|
||||
can = isPermitted(username, action, pg)
|
||||
}
|
||||
|
||||
//check admins based policy
|
||||
if !can {
|
||||
if ac.isAdmin(username) && contains(ac.Config.AdminPolicy.Actions, action) {
|
||||
can = true
|
||||
}
|
||||
}
|
||||
|
||||
return can
|
||||
}
|
||||
|
||||
// isAdmin .
|
||||
func (ac *AccessController) isAdmin(username string) bool {
|
||||
return contains(ac.Config.AdminPolicy.Users, username)
|
||||
}
|
||||
|
||||
// getContext builds ac context(allowed to read repos and if user is admin) and returns it.
|
||||
func (ac *AccessController) getContext(username string, r *http.Request) context.Context {
|
||||
userAllowedRepos := ac.getReadRepos(username)
|
||||
acCtx := AccessControlContext{userAllowedRepos: userAllowedRepos}
|
||||
|
||||
if ac.isAdmin(username) {
|
||||
acCtx.isAdmin = true
|
||||
} else {
|
||||
acCtx.isAdmin = false
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), authzCtxKey, acCtx)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// isPermitted returns true if username can do action on a repository policy.
|
||||
func isPermitted(username, action string, pg PolicyGroup) bool {
|
||||
var result bool
|
||||
// check repo/system based policies
|
||||
for _, p := range pg.Policies {
|
||||
if contains(p.Users, username) && contains(p.Actions, action) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// check defaultPolicy
|
||||
if !result {
|
||||
if contains(pg.DefaultPolicy, action) {
|
||||
result = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, v := range slice {
|
||||
if item == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func containsRepo(slice []string, item string) bool {
|
||||
for _, v := range slice {
|
||||
if strings.HasPrefix(item, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func AuthzHandler(c *Controller) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
resource := vars["name"]
|
||||
reference, ok := vars["reference"]
|
||||
|
||||
ac := NewAccessController(c.Config)
|
||||
username := getUsername(r)
|
||||
ctx := ac.getContext(username, r)
|
||||
|
||||
if r.RequestURI == "/v2/_catalog" || r.RequestURI == "/v2/" {
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
var action string
|
||||
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
||||
action = READ
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPut || r.Method == http.MethodPatch || r.Method == http.MethodPost {
|
||||
// assume user wants to create
|
||||
action = CREATE
|
||||
// if we get a reference (tag)
|
||||
if ok {
|
||||
is := c.StoreController.GetImageStore(resource)
|
||||
tags, err := is.GetImageTags(resource)
|
||||
// if repo exists and request's tag doesn't exist yet then action is UPDATE
|
||||
if err == nil && contains(tags, reference) && reference != "latest" {
|
||||
action = UPDATE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Method == http.MethodDelete {
|
||||
action = DELETE
|
||||
}
|
||||
|
||||
can := ac.can(username, action, resource)
|
||||
if !can {
|
||||
authzFail(w, c.Config.HTTP.Realm, c.Config.HTTP.Auth.FailDelay)
|
||||
} else {
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getUsername(r *http.Request) string {
|
||||
// this should work because it worked in auth middleware
|
||||
basicAuth := r.Header.Get("Authorization")
|
||||
s := strings.SplitN(basicAuth, " ", 2)
|
||||
b, _ := base64.StdEncoding.DecodeString(s[1])
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
|
||||
return pair[0]
|
||||
}
|
||||
|
||||
func isBearerAuthEnabled(config *Config) bool {
|
||||
if config.HTTP.Auth != nil &&
|
||||
config.HTTP.Auth.Bearer != nil &&
|
||||
config.HTTP.Auth.Bearer.Cert != "" &&
|
||||
config.HTTP.Auth.Bearer.Realm != "" &&
|
||||
config.HTTP.Auth.Bearer.Service != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func authzFail(w http.ResponseWriter, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
w.Header().Set("WWW-Authenticate", realm)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteJSON(w, http.StatusForbidden, NewErrorList(NewError(DENIED)))
|
||||
}
|
||||
+55
-14
@@ -1,11 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anuvu/zot/errors"
|
||||
ext "github.com/anuvu/zot/pkg/extensions"
|
||||
"github.com/anuvu/zot/pkg/log"
|
||||
"github.com/getlantern/deepcopy"
|
||||
distspec "github.com/opencontainers/distribution-spec/specs-go"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,13 +46,14 @@ type BearerConfig struct {
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Address string
|
||||
Port string
|
||||
TLS *TLSConfig
|
||||
Auth *AuthConfig
|
||||
Realm string
|
||||
AllowReadAccess bool `mapstructure:",omitempty"`
|
||||
ReadOnly bool `mapstructure:",omitempty"`
|
||||
Address string
|
||||
Port string
|
||||
TLS *TLSConfig
|
||||
Auth *AuthConfig
|
||||
RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"`
|
||||
Realm string
|
||||
AllowReadAccess bool `mapstructure:",omitempty"`
|
||||
ReadOnly bool `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
type LDAPConfig struct {
|
||||
@@ -80,13 +84,14 @@ type GlobalStorageConfig struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Version string
|
||||
Commit string
|
||||
BinaryType string
|
||||
Storage GlobalStorageConfig
|
||||
HTTP HTTPConfig
|
||||
Log *LogConfig
|
||||
Extensions *ext.ExtensionConfig
|
||||
Version string
|
||||
Commit string
|
||||
BinaryType string
|
||||
AccessControl *AccessControlConfig
|
||||
Storage GlobalStorageConfig
|
||||
HTTP HTTPConfig
|
||||
Log *LogConfig
|
||||
Extensions *ext.ExtensionConfig
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
@@ -134,3 +139,39 @@ func (c *Config) Validate(log log.Logger) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAccessControlConfig populates config.AccessControl struct with values from config.
|
||||
func (c *Config) LoadAccessControlConfig() error {
|
||||
if c.HTTP.RawAccessControl == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.AccessControl = &AccessControlConfig{}
|
||||
c.AccessControl.Repositories = make(map[string]PolicyGroup)
|
||||
|
||||
for k := range c.HTTP.RawAccessControl {
|
||||
var policies []Policy
|
||||
|
||||
var policyGroup PolicyGroup
|
||||
|
||||
if k == "adminpolicy" {
|
||||
adminPolicy := viper.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", k), &policies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultPolicy := viper.GetStringSlice(fmt.Sprintf("http.accessControl.%s.defaultPolicy", k))
|
||||
policyGroup.Policies = policies
|
||||
policyGroup.DefaultPolicy = defaultPolicy
|
||||
c.AccessControl.Repositories[k] = policyGroup
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+400
-10
@@ -39,16 +39,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "http://127.0.0.1:%s"
|
||||
BaseSecureURL = "https://127.0.0.1:%s"
|
||||
username = "test"
|
||||
passphrase = "test"
|
||||
ServerCert = "../../test/data/server.cert"
|
||||
ServerKey = "../../test/data/server.key"
|
||||
CACert = "../../test/data/ca.crt"
|
||||
AuthorizedNamespace = "everyone/isallowed"
|
||||
UnauthorizedNamespace = "fortknox/notallowed"
|
||||
ALICE = "alice"
|
||||
BaseURL = "http://127.0.0.1:%s"
|
||||
BaseSecureURL = "https://127.0.0.1:%s"
|
||||
username = "test"
|
||||
passphrase = "test"
|
||||
ServerCert = "../../test/data/server.cert"
|
||||
ServerKey = "../../test/data/server.key"
|
||||
CACert = "../../test/data/ca.crt"
|
||||
AuthorizedNamespace = "everyone/isallowed"
|
||||
UnauthorizedNamespace = "fortknox/notallowed"
|
||||
ALICE = "alice"
|
||||
AuthorizationNamespace = "authz/image"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -1639,6 +1640,395 @@ func parseBearerAuthHeader(authHeaderRaw string) *authHeader {
|
||||
return &h
|
||||
}
|
||||
|
||||
func TestAuthorizationWithBasicAuth(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
port := getFreePort()
|
||||
baseURL := getBaseURL(port, false)
|
||||
|
||||
config := api.NewConfig()
|
||||
config.HTTP.Port = port
|
||||
htpasswdPath := makeHtpasswdFile()
|
||||
defer os.Remove(htpasswdPath)
|
||||
|
||||
config.HTTP.Auth = &api.AuthConfig{
|
||||
HTPasswd: api.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
},
|
||||
}
|
||||
config.AccessControl = &api.AccessControlConfig{
|
||||
Repositories: api.Repositories{
|
||||
AuthorizationNamespace: api.PolicyGroup{
|
||||
Policies: []api.Policy{
|
||||
{
|
||||
Users: []string{},
|
||||
Actions: []string{},
|
||||
},
|
||||
},
|
||||
DefaultPolicy: []string{},
|
||||
},
|
||||
},
|
||||
AdminPolicy: api.Policy{
|
||||
Users: []string{},
|
||||
Actions: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
c := api.NewController(config)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
err = copyFiles("../../test/data", dir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := c.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(baseURL)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
_ = c.Server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
blob := []byte("hello, blob!")
|
||||
digest := godigest.FromBytes(blob).String()
|
||||
|
||||
// everybody should have access to /v2/
|
||||
resp, err := resty.R().SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
// everybody should have access to /v2/_catalog
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
var e api.Error
|
||||
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/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add test user to repo's policy with create perm
|
||||
config.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users =
|
||||
append(config.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test")
|
||||
config.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions =
|
||||
append(config.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, 202)
|
||||
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, 201)
|
||||
|
||||
// head blob should get 403 with 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, 403)
|
||||
|
||||
// get blob should get 403 without read perm
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// 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, 403)
|
||||
|
||||
// get tags with read access should get 200
|
||||
config.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions =
|
||||
append(config.AccessControl.Repositories[AuthorizationNamespace].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, 200)
|
||||
|
||||
// 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, 200)
|
||||
|
||||
// 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, 200)
|
||||
|
||||
// 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, 403)
|
||||
|
||||
// add delete perm on repo
|
||||
config.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions =
|
||||
append(config.AccessControl.Repositories[AuthorizationNamespace].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, 202)
|
||||
|
||||
// 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")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add read perm on repo
|
||||
config.AccessControl.Repositories["zot-test"] = api.PolicyGroup{Policies: []api.Policy{
|
||||
{
|
||||
[]string{"test"},
|
||||
[]string{"read"},
|
||||
},
|
||||
}, DefaultPolicy: []string{}}
|
||||
|
||||
// get manifest should get 200 now
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + "/v2/zot-test/manifests/0.0.1")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
manifestBlob := resp.Body()
|
||||
|
||||
// put manifest should get 403 without create perm
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add create perm on repo
|
||||
config.AccessControl.Repositories["zot-test"].Policies[0].Actions =
|
||||
append(config.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create")
|
||||
|
||||
// should get 201 with create perm
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 201)
|
||||
|
||||
// update manifest should get 403 without update perm
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add update perm on repo
|
||||
config.AccessControl.Repositories["zot-test"].Policies[0].Actions =
|
||||
append(config.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update")
|
||||
|
||||
// update manifest should get 201 with update perm
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 201)
|
||||
|
||||
// now use default repo policy
|
||||
config.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{}
|
||||
repoPolicy := config.AccessControl.Repositories["zot-test"]
|
||||
repoPolicy.DefaultPolicy = []string{"update"}
|
||||
config.AccessControl.Repositories["zot-test"] = repoPolicy
|
||||
|
||||
// update manifest should get 201 with update perm on repo's default policy
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 201)
|
||||
|
||||
// with default read on repo should still get 200
|
||||
config.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{}
|
||||
repoPolicy = config.AccessControl.Repositories[AuthorizationNamespace]
|
||||
repoPolicy.DefaultPolicy = []string{"read"}
|
||||
config.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
|
||||
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
// upload blob without user create but with default create should get 200
|
||||
repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create")
|
||||
config.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
|
||||
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 202)
|
||||
|
||||
//remove per repo policy
|
||||
repoPolicy = config.AccessControl.Repositories[AuthorizationNamespace]
|
||||
repoPolicy.Policies = []api.Policy{}
|
||||
repoPolicy.DefaultPolicy = []string{}
|
||||
config.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
|
||||
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// let's use admin policy
|
||||
// remove all repo based policy
|
||||
delete(config.AccessControl.Repositories, AuthorizationNamespace)
|
||||
delete(config.AccessControl.Repositories, "zot-test")
|
||||
|
||||
// whithout any perm 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, 403)
|
||||
|
||||
// add read perm
|
||||
config.AccessControl.AdminPolicy.Users = append(config.AccessControl.AdminPolicy.Users, "test")
|
||||
config.AccessControl.AdminPolicy.Actions = append(config.AccessControl.AdminPolicy.Actions, "read")
|
||||
// with read perm should get 200
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
// without create perm should 403
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add create perm
|
||||
config.AccessControl.AdminPolicy.Actions = append(config.AccessControl.AdminPolicy.Actions, "create")
|
||||
// with create perm 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, 202)
|
||||
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, 201)
|
||||
|
||||
// without delete perm should 403
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add delete perm
|
||||
config.AccessControl.AdminPolicy.Actions = append(config.AccessControl.AdminPolicy.Actions, "delete")
|
||||
// with delete perm 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, 202)
|
||||
|
||||
// without update perm should 403
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
|
||||
// add update perm
|
||||
config.AccessControl.AdminPolicy.Actions = append(config.AccessControl.AdminPolicy.Actions, "update")
|
||||
// update manifest should get 201 with update perm
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 201)
|
||||
|
||||
config.AccessControl = &api.AccessControlConfig{}
|
||||
|
||||
resp, err = resty.R().SetBasicAuth(username, passphrase).
|
||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 403)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvalidCases(t *testing.T) {
|
||||
Convey("Invalid repo dir", t, func() {
|
||||
port := getFreePort()
|
||||
|
||||
+19
-1
@@ -54,6 +54,11 @@ func NewRouteHandler(c *Controller) *RouteHandler {
|
||||
|
||||
func (rh *RouteHandler) SetupRoutes() {
|
||||
rh.c.Router.Use(AuthHandler(rh.c))
|
||||
|
||||
if !isBearerAuthEnabled(rh.c.Config) && rh.c.Config.AccessControl != nil {
|
||||
rh.c.Router.Use(AuthzHandler(rh.c))
|
||||
}
|
||||
|
||||
g := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
|
||||
{
|
||||
g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),
|
||||
@@ -1137,7 +1142,20 @@ func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request)
|
||||
combineRepoList = append(combineRepoList, repos...)
|
||||
}
|
||||
|
||||
is := RepositoryList{Repositories: combineRepoList}
|
||||
var repos []string
|
||||
// get passed context from authzHandler and filter out repos based on permissions
|
||||
if authCtx := r.Context().Value(authzCtxKey); authCtx != nil {
|
||||
acCtx := authCtx.(AccessControlContext)
|
||||
for _, r := range combineRepoList {
|
||||
if containsRepo(acCtx.userAllowedRepos, r) || acCtx.isAdmin {
|
||||
repos = append(repos, r)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repos = combineRepoList
|
||||
}
|
||||
|
||||
is := RepositoryList{Repositories: repos}
|
||||
|
||||
WriteJSON(w, http.StatusOK, is)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user