mirror of
https://github.com/project-zot/zot.git
synced 2025-03-25 02:32:57 -05:00
Add identity-based access control, closes #51
Add a cli subcommand to verify config files validity
This commit is contained in:
parent
26926ad4c2
commit
609d85d875
14 changed files with 915 additions and 51 deletions
11
README.md
11
README.md
|
@ -27,6 +27,8 @@ https://anuvu.github.io/zot/
|
|||
* TLS mutual authentication
|
||||
* HTTP *Basic* (local _htpasswd_ and LDAP)
|
||||
* HTTP *Bearer* token
|
||||
* [Authorization](#Authorization) Supports Identity-Based Access Control: [Configuration](./examples/config-policy.json)
|
||||
* Supports live modifications on the config file while zot is running (Authorization config only)
|
||||
* Doesn't require _root_ privileges
|
||||
* Storage optimizations:
|
||||
* Automatic garbage collection of orphaned blobs
|
||||
|
@ -222,6 +224,15 @@ c3/openjdk-dev commit-2674e8a-squashfs b545b8ba 321MB
|
|||
c3/openjdk-dev commit-d5024ec-squashfs cd45f8cf 321MB
|
||||
```
|
||||
|
||||
## Authorization
|
||||
|
||||
zot follows the next logic for deciding who has rights on what resource:
|
||||
1) First it checks the requested resource in "repositories" as specified in configuration
|
||||
2) Searches in all policies for the user who requested the resource
|
||||
3) If found, applies the policy, otherwise it falls back to the default policy for that repository
|
||||
4) If not found, it falls back to the adminPolicy
|
||||
|
||||
|
||||
# Ecosystem
|
||||
|
||||
|
||||
|
|
|
@ -729,13 +729,13 @@ type swaggerInfo struct {
|
|||
}
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = swaggerInfo{ Schemes: []string{}}
|
||||
var SwaggerInfo = swaggerInfo{Schemes: []string{}}
|
||||
|
||||
type s struct{}
|
||||
|
||||
func (s *s) ReadDoc() string {
|
||||
t, err := template.New("swagger_info").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface {}) string {
|
||||
"marshal": func(v interface{}) string {
|
||||
a, _ := json.Marshal(v)
|
||||
return string(a)
|
||||
},
|
||||
|
|
5
examples/README.md
Normal file
5
examples/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
You can check a configuration file validity by running the verify command:
|
||||
|
||||
```console
|
||||
$ zot verify /path/to/config
|
||||
```
|
|
@ -9,6 +9,7 @@
|
|||
"ReadOnly": false
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
"level": "debug",
|
||||
"output": "/tmp/zot.log"
|
||||
}
|
||||
}
|
||||
|
|
54
examples/config-policy.json
Executable file
54
examples/config-policy.json
Executable file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"version": "0.1.0-dev",
|
||||
"storage": {
|
||||
"rootDirectory": "/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080",
|
||||
"realm": "zot",
|
||||
"auth": {
|
||||
"htpasswd": {
|
||||
"path": "test/data/htpasswd"
|
||||
},
|
||||
"failDelay": 1
|
||||
},
|
||||
"accessControl": {
|
||||
"repos1/repo": {
|
||||
"policies": [
|
||||
{
|
||||
"users": ["alice", "bob"],
|
||||
"actions": ["create", "read", "update", "delete"]
|
||||
},
|
||||
{
|
||||
"users": ["mallory"],
|
||||
"actions": ["create", "read"]
|
||||
}
|
||||
],
|
||||
"defaultPolicy": ["read"]
|
||||
},
|
||||
"repos2/repo": {
|
||||
"policies": [
|
||||
{
|
||||
"users": ["bob"],
|
||||
"actions": ["read", "create"]
|
||||
},
|
||||
{
|
||||
"users": ["mallory"],
|
||||
"actions": ["create", "read"]
|
||||
}
|
||||
],
|
||||
"defaultPolicy": ["read"]
|
||||
},
|
||||
"adminPolicy": {
|
||||
"users": ["admin"],
|
||||
"actions": ["read", "create", "update", "delete"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "/tmp/zot.log"
|
||||
}
|
||||
}
|
||||
|
3
go.mod
3
go.mod
|
@ -11,6 +11,7 @@ require (
|
|||
github.com/briandowns/spinner v1.12.0
|
||||
github.com/chartmuseum/auth v0.4.5
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
|
@ -40,7 +41,7 @@ require (
|
|||
github.com/swaggo/swag v1.7.0
|
||||
github.com/vektah/gqlparser/v2 v2.2.0
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
gopkg.in/resty.v1 v1.12.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
|
3
go.sum
3
go.sum
|
@ -1148,9 +1148,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
|
@ -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
pkg/api/authz.go
Executable file
236
pkg/api/authz.go
Executable 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)))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue