Add identity-based access control, closes #51

Add a cli subcommand to verify config files validity
This commit is contained in:
Petu Eusebiu
2021-05-13 21:59:12 +03:00
committed by Ramkumar Chinchani
parent 26926ad4c2
commit 609d85d875
14 changed files with 915 additions and 51 deletions
+1 -5
View File
@@ -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)
}
+236
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+80 -15
View File
@@ -4,6 +4,7 @@ import (
"github.com/anuvu/zot/errors"
"github.com/anuvu/zot/pkg/api"
"github.com/anuvu/zot/pkg/storage"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/mapstructure"
distspec "github.com/opencontainers/distribution-spec/specs-go"
"github.com/rs/zerolog/log"
@@ -31,29 +32,66 @@ func NewRootCmd() *cobra.Command {
Long: "`serve` stores and distributes OCI images",
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
viper.SetConfigFile(args[0])
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
md := &mapstructure.Metadata{}
if err := viper.Unmarshal(&config, metadataConfig(md)); err != nil {
panic(err)
}
// if haven't found a single key or there were unused keys, report it as
// a error
if len(md.Keys) == 0 || len(md.Unused) > 0 {
panic(errors.ErrBadConfig)
}
LoadConfiguration(config, args[0])
}
c := api.NewController(config)
// creates a new file watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
}
defer watcher.Close()
done := make(chan bool)
// run watcher
go func() {
go func() {
for {
select {
// watch for events
case event := <-watcher.Events:
if event.Op == fsnotify.Write {
log.Info().Msg("Config file changed, trying to reload accessControl config")
newConfig := api.NewConfig()
LoadConfiguration(newConfig, args[0])
c.Config.AccessControl = newConfig.AccessControl
}
// watch for errors
case err := <-watcher.Errors:
log.Error().Err(err).Msgf("FsNotify error while watching config %s", args[0])
panic(err)
}
}
}()
if err := watcher.Add(args[0]); err != nil {
log.Error().Err(err).Msgf("Error adding config file %s to FsNotify watcher", args[0])
panic(err)
}
<-done
}()
if err := c.Run(); err != nil {
panic(err)
}
},
}
verifyCmd := &cobra.Command{
Use: "verify <config>",
Aliases: []string{"verify"},
Short: "`verify` validates a zot config file",
Long: "`verify` validates a zot config file",
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
config := api.NewConfig()
LoadConfiguration(config, args[0])
log.Info().Msgf("Config file %s is valid", args[0])
}
},
}
// "garbage-collect"
gcDelUntagged := false
gcDryRun := false
@@ -98,6 +136,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(gcCmd)
rootCmd.AddCommand(verifyCmd)
enableCli(rootCmd)
@@ -105,3 +144,29 @@ func NewRootCmd() *cobra.Command {
return rootCmd
}
func LoadConfiguration(config *api.Config, configPath string) {
viper.SetConfigFile(configPath)
if err := viper.ReadInConfig(); err != nil {
log.Error().Err(err).Msg("Error while reading configuration")
panic(err)
}
md := &mapstructure.Metadata{}
if err := viper.Unmarshal(&config, metadataConfig(md)); err != nil {
log.Error().Err(err).Msg("Error while unmarshalling new config")
panic(err)
}
if len(md.Keys) == 0 || len(md.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Msg("Bad configuration, retry writing it")
panic(errors.ErrBadConfig)
}
err := config.LoadAccessControlConfig()
if err != nil {
log.Error().Err(errors.ErrBadConfig).Msg("Unable to unmarshal http.accessControl.key.policies")
panic(err)
}
}
+47
View File
@@ -6,8 +6,10 @@ import (
"path"
"testing"
"github.com/anuvu/zot/pkg/api"
"github.com/anuvu/zot/pkg/cli"
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/viper"
)
func TestUsage(t *testing.T) {
@@ -65,6 +67,51 @@ func TestServe(t *testing.T) {
})
}
func TestVerify(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("Test verify bad config", t, func(c C) {
tmpfile, err := ioutil.TempFile("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"log":{}}`)
_, 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 good config", t, func(c C) {
tmpfile, err := ioutil.TempFile("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"version": "0.1.0-dev", "storage": {"rootDirectory": "/tmp/zot"},
"http": {"address": "127.0.0.1", "port": "8080", "ReadOnly": false},
"log": {"level": "debug"}}`)
_, 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)
})
}
func TestLoadConfig(t *testing.T) {
Convey("Test viper load config", t, func(c C) {
config := api.NewConfig()
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"])
})
}
func TestGC(t *testing.T) {
oldArgs := os.Args