0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

refactor(extensions)!: refactor the extensions URLs and errors (#1636)

BREAKING CHANGE: The functionality provided by the mgmt endpoint has beed redesigned - see details below
BREAKING CHANGE: The API keys endpoint has been moved -  see details below
BREAKING CHANGE: The mgmt extension config has been removed - endpoint is now enabled by having both the search and the ui extensions enabled
BREAKING CHANGE: The API keys configuration has been moved from extensions to http>auth>apikey

mgmt and imagetrust extensions:
- separate the _zot/ext/mgmt into 3 separate endpoints: _zot/ext/auth, _zot/ext/notation, _zot/ext/cosign
- signature verification logic is in a separate `imagetrust` extension
- better hanling or errors in case of signature uploads: logging and error codes (more 400 and less 500 errors)
- add authz on signature uploads (and add a new middleware in common for this purpose)
- remove the mgmt extension configuration - it is now enabled if the UI and the search extensions are enabled

userprefs estension:
- userprefs are enabled if both search and ui extensions are enabled (as opposed to just search)

apikey extension is removed and logic moved into the api folder
- Move apikeys code out of pkg/extensions and into pkg/api
- Remove apikey configuration options from the extensions configuration and move it inside the http auth section
- remove the build label apikeys

other changes:
- move most of the logic adding handlers to the extensions endpoints out of routes.go and into the extensions files.
- add warnings in case the users are still using configurations with the obsolete settings for mgmt and api keys
- add a new function in the extension package which could be a single point of starting backgroud tasks for all extensions
- more clear methods for verifying specific extensions are enabled
- fix http methods paired with the UI handlers
- rebuild swagger docs

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
Andrei Aaron 2023-08-02 21:58:34 +03:00 committed by GitHub
parent 42f9f78125
commit 77149aa85c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 3405 additions and 1471 deletions

View file

@ -39,7 +39,7 @@ jobs:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
env:
CGO_ENABLED: 0
GOFLAGS: "-tags=sync,search,scrub,metrics,userprefs,apikey,containers_image_openpgp"
GOFLAGS: "-tags=sync,search,scrub,metrics,userprefs,mgmt,imagetrust,containers_image_openpgp"
steps:
- name: Checkout repository

View file

@ -32,7 +32,7 @@ jobs:
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,userprefs,metrics,containers_image_openpgp,lint,mgmt,apikey ./cmd/... ./pkg/...
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,userprefs,metrics,containers_image_openpgp,lint,mgmt,imagetrust ./cmd/... ./pkg/...
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

3
.gitignore vendored
View file

@ -29,4 +29,5 @@ coverage.html
tags
vendor/
.vscode/
examples/config-sync-localhost.json
examples/config-sync-localhost.json
node_modules

View file

@ -32,8 +32,8 @@ TESTDATA := $(TOP_LEVEL)/test/data
OS ?= linux
ARCH ?= amd64
BENCH_OUTPUT ?= stdout
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,apikey
UI_DEPENDENCIES := search,mgmt,userprefs,apikey
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,imagetrust
UI_DEPENDENCIES := search,mgmt,userprefs
# freebsd/arm64 not supported for pie builds
BUILDMODE_FLAGS := -buildmode=pie
ifeq ($(OS),freebsd)

View file

@ -107,6 +107,7 @@ var (
ErrInvalidTruststoreType = errors.New("signatures: invalid truststore type")
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content")
ErrInvalidPublicKeyContent = errors.New("signatures: invalid public key content")
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones")
)

View file

@ -207,12 +207,12 @@ zot can be configured to use the above providers with:
}
```
the login with either provider use http://127.0.0.1:8080/auth/login?provider=\<provider\>&callback_ui=http://127.0.0.1:8080/home
The login with either provider use http://127.0.0.1:8080/auth/login?provider=\<provider\>&callback_ui=http://127.0.0.1:8080/home
for example to login with github use http://127.0.0.1:8080/auth/login?provider=github&callback_ui=http://127.0.0.1:8080/home
callback_ui query parameter is used by zot to redirect to UI after a successful openid/oauth2 authentication
the callback url which should be used when making oauth2 provider setup is http://127.0.0.1:8080/auth/callback/\<provider\>
The callback url which should be used when making oauth2 provider setup is http://127.0.0.1:8080/auth/callback/\<provider\>
for example github callback url would be http://127.0.0.1:8080/auth/callback/github
If network policy doesn't allow inbound connections, this callback wont work!
@ -220,7 +220,7 @@ If network policy doesn't allow inbound connections, this callback wont work!
dex is an identity service that uses OpenID Connect to drive authentication for other apps https://github.com/dexidp/dex
To setup dex service see https://dexidp.io/docs/getting-started/
to configure zot as a client in dex (assuming zot is hosted at 127.0.0.1:8080), we need to configure dex with:
To configure zot as a client in dex (assuming zot is hosted at 127.0.0.1:8080), we need to configure dex with:
```
staticClients:
@ -251,7 +251,12 @@ zot can be configured to use dex with:
}
```
to login using openid dex provider use http://127.0.0.1:8080/auth/login?provider=dex
To login using openid dex provider use http://127.0.0.1:8080/auth/login?provider=dex
NOTE: Social login is not supported by command line tools, or other software responsible for pushing/pulling
images to/from zot.
Given this limitation, if openif authentication is enabled in the configuration, API keys are also enabled
implicitly, as a viable alternative authentication method for pushing and pulling container images.
### Session based login
@ -261,6 +266,90 @@ Using that cookie on subsequent calls will authenticate them, asumming the cooki
In case of using filesystem storage sessions are saved in zot's root directory.
In case of using cloud storage sessions are saved in memory.
#### API keys
zot allows authentication for REST API calls using your API key as an alternative to your password.
The user can create or revoke his API keys after he has already authenticated using a different authentication mechanism.
An API key is shown to the user only when it is created. It can not be retrieved from zot with any other call.
An API key has the same permissions as the user who generated it.
Below are several use cases where API keys offer advantages:
- OpenID/OAuth2 social login is not supported by command-line tools or other such clients. In this case, the user
can login to zot using OpenID/OAuth2 and generate API keys to use later when pushing and pulling images.
- In cases where LDAP authentication is used and the user has scripts pushing or pulling images, he will probably not
want to store his LDAP username and password in a shared environment where there is a chance they are compromised.
If he generates and uses an API key instead, the security impact of that key being compromised is limited to zot,
the other services he accesses based on LDAP would not be affected.
To activate API keys use:
```
"http": {
"auth": {
"apikey: true
```
##### How to create an API Key
Create an API key for the current user using the REST API
**Usage**: POST /auth/apikey
**Produces**: application/json
**Sample input**:
```
POST /auth/apiKey
Body: {"label": "git", "scopes": ["repo1", "repo2"]}'
```
**Example cURL**
```bash
curl -u user:password -X POST http://localhost:8080/auth/apikey -d '{"label": "myLabel"}'
```
**Sample output**:
```json
{
"createdAt": "2023-05-05T15:39:28.420926+03:00",
"creatorUa": "curl/7.68.0",
"generatedBy": "manual",
"lastUsed": "2023-05-05T15:39:28.4209282+03:00",
"label": "git",
"scopes": null,
"uuid": "46a45ce7-5d92-498a-a9cb-9654b1da3da1",
"apiKey": "zak_e77bcb9e9f634f1581756abbf9ecd269"
}
```
##### How to use API Keys
**Using API keys with cURL**
```bash
curl -u user:zak_e77bcb9e9f634f1581756abbf9ecd269 http://localhost:8080/v2/_catalog
```
Other command line tools will similarly accept the API key instead of a password.
##### How to revoke an API Key
How to revoke an API key for the current user
**Usage**: DELETE /auth/apiKey?id=$uuid
**Produces**: application/json
**Example cURL**
```bash
curl -u user:password -X DELETE http://localhost:8080/v2/auth/apikey?id=46a45ce7-5d92-498a-a9cb-9654b1da3da1
```
#### Authentication Failures
Should authentication fail, to prevent automated attacks, a delayed response can be configured with:
@ -271,21 +360,6 @@ Should authentication fail, to prevent automated attacks, a delayed response can
"failDelay": 5
```
#### API keys
zot allows authentication for REST API calls using your API key as an alternative to your password.
for more info see [API keys doc](../pkg/extensions/README_apikey.md)
To activate API keys use:
```
"extensions": {
"apikey": {
"enable": true
}
}
```
## Identity-based Authorization
Allowing actions on one or more repository paths can be tied to user

View file

@ -48,9 +48,6 @@
"scrub": {
"enable": true,
"interval": "24h"
},
"mgmt": {
"enable": true
}
}
}

View file

@ -12,6 +12,7 @@
"htpasswd": {
"path": "test/data/htpasswd"
},
"apikey": true,
"openid": {
"providers": {
"github": {
@ -64,12 +65,5 @@
"log": {
"level": "debug"
},
"extensions": {
"apikey": {
"enable": true
},
"mgmt": {
"enable": true
}
}
"extensions": {}
}

View file

@ -18,9 +18,6 @@
},
"ui": {
"enable": true
},
"mgmt": {
"enable": true
}
}
}

View file

@ -18,6 +18,7 @@ import (
"time"
"github.com/chartmuseum/auth"
guuid "github.com/gofrs/uuid"
"github.com/google/go-github/v52/github"
"github.com/google/uuid"
"github.com/gorilla/mux"
@ -353,7 +354,7 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
return
}
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
isMgmtRequested := request.RequestURI == constants.FullMgmt
allowAnonymous := ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists()
// try basic auth if authorization header is given
@ -443,7 +444,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
name := vars["name"]
// we want to bypass auth for mgmt route
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
isMgmtRequested := request.RequestURI == constants.FullMgmt
header := request.Header.Get("Authorization")
@ -849,3 +850,25 @@ func GetAuthUserFromRequestSession(cookieStore sessions.Store, request *http.Req
return identity, true
}
func GenerateAPIKey(uuidGenerator guuid.Generator, log log.Logger,
) (string, string, error) {
apiKeyBase, err := uuidGenerator.NewV4()
if err != nil {
log.Error().Err(err).Msg("unable to generate uuid for api key base")
return "", "", err
}
apiKey := strings.ReplaceAll(apiKeyBase.String(), "-", "")
// will be used for identifying a specific api key
apiKeyID, err := uuidGenerator.NewV4()
if err != nil {
log.Error().Err(err).Msg("unable to generate uuid for api key id")
return "", "", err
}
return apiKey, apiKeyID.String(), err
}

View file

@ -1,16 +1,19 @@
//go:build apikey
// +build apikey
//go:build mgmt
// +build mgmt
package extensions_test
package api_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
guuid "github.com/gofrs/uuid"
"github.com/project-zot/mockoidc"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
@ -18,14 +21,16 @@ import (
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
localCtx "zotregistry.io/zot/pkg/requestcontext"
"zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
)
var ErrUnexpectedError = errors.New("error: unexpected error")
type (
apiKeyResponse struct {
mTypes.APIKeyDetails
@ -33,7 +38,30 @@ type (
}
)
var ErrUnexpectedError = errors.New("unexpected err")
func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
defaultVal := true
Convey("Test http options response", t, func() {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.HTTP.Auth.APIKey = defaultVal
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctrlManager := test.NewControllerManager(ctlr)
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.APIKeyPath)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,DELETE,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}
func TestAPIKeys(t *testing.T) {
Convey("Make a new controller", t, func() {
@ -59,6 +87,8 @@ func TestAPIKeys(t *testing.T) {
}()
mockOIDCConfig := mockOIDCServer.Config()
defaultVal := true
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
@ -74,23 +104,17 @@ func TestAPIKeys(t *testing.T) {
},
},
},
APIKey: defaultVal,
}
conf.HTTP.AccessControl = &config.AccessControlConfig{}
defaultVal := true
apiKeyConfig := &extconf.APIKeyConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
mgmtConfg := &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
APIKey: apiKeyConfig,
Mgmt: mgmtConfg,
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
ctlr := api.NewController(conf)
dir := t.TempDir()
@ -103,7 +127,7 @@ func TestAPIKeys(t *testing.T) {
defer cm.StopServer()
test.WaitTillServerReady(baseURL)
payload := extensions.APIKeyPayload{
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
}
@ -115,7 +139,7 @@ func TestAPIKeys(t *testing.T) {
resp, err := resty.R().
SetBody(reqBody).
SetBasicAuth("test", "test").
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -141,7 +165,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = resty.R().
SetBody(reqBody).
SetBasicAuth("test", "test").
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -175,7 +199,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@ -186,7 +210,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -260,7 +284,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@ -270,8 +294,7 @@ func TestAPIKeys(t *testing.T) {
client := resty.New()
// mgmt should work both unauthenticated and authenticated
resp, err := client.R().
Get(baseURL + constants.FullMgmtPrefix)
resp, err := client.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -292,7 +315,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -326,7 +349,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -334,7 +357,7 @@ func TestAPIKeys(t *testing.T) {
// invalid api keys
resp, err = client.R().
SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@ -366,7 +389,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
@ -375,8 +398,7 @@ func TestAPIKeys(t *testing.T) {
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
// without creds should work
resp, err = client.R().
Get(baseURL + constants.FullMgmtPrefix)
resp, err = client.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -395,7 +417,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix)
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -406,7 +428,7 @@ func TestAPIKeys(t *testing.T) {
// should work with session
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -414,7 +436,7 @@ func TestAPIKeys(t *testing.T) {
// should work with api key
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -433,14 +455,14 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("id", apiKeyResponse.UUID).
Delete(baseURL + constants.FullAPIKeyPrefix)
Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Delete(baseURL + constants.FullAPIKeyPrefix)
Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
@ -455,18 +477,31 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R().
SetBasicAuth("test", "test").
SetQueryParam("id", apiKeyResponse.UUID).
Delete(baseURL + constants.FullAPIKeyPrefix)
Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// unsupported method
resp, err = client.R().
Put(baseURL + constants.FullAPIKeyPrefix)
Put(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
})
Convey("Test error handling when API Key handler reads the request body", func() {
request, _ := http.NewRequestWithContext(context.TODO(),
http.MethodPost, "baseURL", errReader(0))
response := httptest.NewRecorder()
rthdlr := api.NewRouteHandler(ctlr)
rthdlr.CreateAPIKey(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
})
}
@ -489,6 +524,8 @@ func TestAPIKeysOpenDBError(t *testing.T) {
}()
mockOIDCConfig := mockOIDCServer.Config()
defaultVal := true
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
@ -505,14 +542,8 @@ func TestAPIKeysOpenDBError(t *testing.T) {
},
},
},
}
defaultVal := true
apiKeyConfig := &extconf.APIKeyConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
APIKey: apiKeyConfig,
APIKey: defaultVal,
}
ctlr := api.NewController(conf)
@ -529,3 +560,58 @@ func TestAPIKeysOpenDBError(t *testing.T) {
}, ShouldPanic)
})
}
func TestAPIKeysGeneratorErrors(t *testing.T) {
Convey("Test API keys - unable to generate API keys and API Key IDs", t, func() {
log := log.NewLogger("debug", "")
apiKey, apiKeyID, err := api.GenerateAPIKey(guuid.DefaultGenerator, log)
So(err, ShouldBeNil)
So(apiKey, ShouldNotEqual, "")
So(apiKeyID, ShouldNotEqual, "")
generator := &mockUUIDGenerator{
guuid.DefaultGenerator, 0, 0,
}
apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
So(err, ShouldNotBeNil)
So(apiKey, ShouldEqual, "")
So(apiKeyID, ShouldEqual, "")
generator = &mockUUIDGenerator{
guuid.DefaultGenerator, 1, 0,
}
apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
So(err, ShouldNotBeNil)
So(apiKey, ShouldEqual, "")
So(apiKeyID, ShouldEqual, "")
})
}
type mockUUIDGenerator struct {
guuid.Generator
succeedAttempts int
attemptCount int
}
func (gen *mockUUIDGenerator) NewV4() (
guuid.UUID, error,
) {
defer func() {
gen.attemptCount += 1
}()
if gen.attemptCount >= gen.succeedAttempts {
return guuid.UUID{}, ErrUnexpectedError
}
return guuid.DefaultGenerator.NewV4()
}
type errReader int
func (errReader) Read(p []byte) (int, error) {
return 0, fmt.Errorf("test error") //nolint:goerr113
}

View file

@ -50,6 +50,7 @@ type AuthConfig struct {
LDAP *LDAPConfig
Bearer *BearerConfig
OpenID *OpenIDConfig
APIKey bool
}
type BearerConfig struct {
@ -274,8 +275,7 @@ func (c *Config) IsOpenIDAuthEnabled() bool {
}
func (c *Config) IsAPIKeyEnabled() bool {
if c.Extensions != nil && c.Extensions.APIKey != nil &&
*c.Extensions.APIKey.Enable {
if c.HTTP.Auth != nil && c.HTTP.Auth.APIKey {
return true
}
@ -308,6 +308,38 @@ func isOpenIDAuthProviderEnabled(config *Config, provider string) bool {
return false
}
func (c *Config) IsSearchEnabled() bool {
return c.Extensions != nil && c.Extensions.Search != nil && *c.Extensions.Search.Enable
}
func (c *Config) IsUIEnabled() bool {
return c.Extensions != nil && c.Extensions.UI != nil && *c.Extensions.UI.Enable
}
func (c *Config) AreUserPrefsEnabled() bool {
return c.IsSearchEnabled() && c.IsUIEnabled()
}
func (c *Config) IsMgmtEnabled() bool {
return c.IsSearchEnabled() && c.IsUIEnabled()
}
func (c *Config) IsImageTrustEnabled() bool {
return c.Extensions != nil && c.Extensions.Trust != nil && *c.Extensions.Trust.Enable
}
func (c *Config) IsCosignEnabled() bool {
return c.IsImageTrustEnabled() && c.Extensions.Trust.Cosign
}
func (c *Config) IsNotationEnabled() bool {
return c.IsImageTrustEnabled() && c.Extensions.Trust.Notation
}
func (c *Config) IsSyncEnabled() bool {
return c.Extensions != nil && c.Extensions.Sync != nil && *c.Extensions.Sync.Enable
}
func IsOpenIDSupported(provider string) bool {
for _, supportedProvider := range openIDSupportedProviders {
if supportedProvider == provider {

View file

@ -15,6 +15,7 @@ const (
CallbackBasePath = "/auth/callback"
LoginPath = "/auth/login"
LogoutPath = "/auth/logout"
APIKeyPath = "/auth/apikey" //nolint: gosec
SessionClientHeaderName = "X-ZOT-API-CLIENT"
SessionClientHeaderValue = "zot-ui"
APIKeysPrefix = "zak_"

View file

@ -4,21 +4,31 @@ package constants
const (
ExtCatalogPrefix = "/_catalog"
ExtOciDiscoverPrefix = "/_oci/ext/discover"
// zot specific extensions.
ExtPrefix = "/_zot/ext"
// zot specific extensions.
BasePrefix = "/_zot"
ExtPrefix = BasePrefix + "/ext"
// search extension.
ExtSearch = "/search"
ExtSearchPrefix = ExtPrefix + ExtSearch
FullSearchPrefix = RoutePrefix + ExtSearchPrefix
ExtMgmt = "/mgmt"
ExtMgmtPrefix = ExtPrefix + ExtMgmt
FullMgmtPrefix = RoutePrefix + ExtMgmtPrefix
// mgmt extension.
Mgmt = "/mgmt"
ExtMgmt = ExtPrefix + Mgmt
FullMgmt = RoutePrefix + ExtMgmt
ExtUserPreferences = "/userprefs"
ExtUserPreferencesPrefix = ExtPrefix + ExtUserPreferences
FullUserPreferencesPrefix = RoutePrefix + ExtUserPreferencesPrefix
ExtAPIKey = "/apikey"
ExtAPIKeyPrefix = ExtPrefix + ExtAPIKey //nolint: gosec
FullAPIKeyPrefix = RoutePrefix + ExtAPIKeyPrefix
// signatures extension.
Notation = "/notation"
ExtNotation = ExtPrefix + Notation
FullNotation = RoutePrefix + ExtNotation
Cosign = "/cosign"
ExtCosign = ExtPrefix + Cosign
FullCosign = RoutePrefix + ExtCosign
// user preferences extension.
UserPrefs = "/userprefs"
ExtUserPrefs = ExtPrefix + UserPrefs
FullUserPrefs = RoutePrefix + ExtUserPrefs
)

View file

@ -258,9 +258,8 @@ func (c *Controller) InitImageStore() error {
}
func (c *Controller) InitMetaDB(reloadCtx context.Context) error {
// init metaDB if search is enabled or authn enabled (need to store user profiles) or apikey ext is enabled
if (c.Config.Extensions != nil && c.Config.Extensions.Search != nil && *c.Config.Extensions.Search.Enable) ||
c.Config.IsBasicAuthnEnabled() {
// init metaDB if search is enabled or we need to store user profiles, api keys or signatures
if c.Config.IsSearchEnabled() || c.Config.IsBasicAuthnEnabled() || c.Config.IsImageTrustEnabled() {
driver, err := meta.New(c.Config.Storage.StorageConfig, c.Log) //nolint:contextcheck
if err != nil {
return err
@ -368,11 +367,8 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
c.SyncOnDemand = syncOnDemand
}
if c.Config.Extensions != nil {
if c.Config.Extensions.Mgmt != nil && *c.Config.Extensions.Mgmt.Enable {
ext.EnablePeriodicSignaturesVerification(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
}
}
// we can later move enabling the other scheduled tasks inside the call below
ext.EnableScheduledTasks(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
}
type SyncOnDemand interface {

View file

@ -1,5 +1,5 @@
//go:build sync && scrub && metrics && search && lint && apikey && mgmt
// +build sync,scrub,metrics,search,lint,apikey,mgmt
//go:build sync && scrub && metrics && search && lint && userprefs && mgmt && imagetrust && ui
// +build sync,scrub,metrics,search,lint,userprefs,mgmt,imagetrust,ui
package api_test
@ -2659,12 +2659,18 @@ func TestOpenIDMiddleware(t *testing.T) {
},
}
mgmtConfg := &extconf.MgmtConfig{
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
// UI is enabled because we also want to test access on the mgmt route
uiConfig := &extconf.UIConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
Mgmt: mgmtConfg,
Search: searchConfig,
UI: uiConfig,
}
ctlr := api.NewController(conf)
@ -2769,7 +2775,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
@ -2778,7 +2784,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().
SetBasicAuth(htpasswdUsername, passphrase).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2795,7 +2801,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2835,7 +2841,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
@ -2844,7 +2850,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().
SetBasicAuth(username, passphrase).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2861,7 +2867,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2888,7 +2894,7 @@ func TestOpenIDMiddleware(t *testing.T) {
// mgmt should work both unauthenticated and authenticated
resp, err := client.R().
Get(baseURL + constants.FullMgmtPrefix)
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -3063,12 +3069,17 @@ func TestAuthnSessionErrors(t *testing.T) {
},
}
mgmtConfg := &extconf.MgmtConfig{
uiConfig := &extconf.UIConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
Mgmt: mgmtConfg,
UI: uiConfig,
Search: searchConfig,
}
ctlr := api.NewController(conf)
@ -8391,7 +8402,7 @@ func TestSearchRoutes(t *testing.T) {
}
func TestDistSpecExtensions(t *testing.T) {
Convey("start zot server with search extension", t, func(c C) {
Convey("start zot server with search, ui and trust extensions", t, func(c C) {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
@ -8400,13 +8411,16 @@ func TestDistSpecExtensions(t *testing.T) {
defaultVal := true
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultVal
conf.Extensions.Trust.Cosign = defaultVal
conf.Extensions.Trust.Notation = defaultVal
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
@ -8427,16 +8441,23 @@ func TestDistSpecExtensions(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil)
t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
// Verify the endpoints below are enabled by search
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPreferencesPrefix)
// Verify the endpoints below are enabled by trust
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullCosign)
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullNotation)
// Verify the endpint below are enabled by having both the UI and the Search enabled
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPrefs)
})
Convey("start zot server with search and mgmt extensions", t, func(c C) {
Convey("start zot server with only the search extension enabled", t, func(c C) {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
@ -8445,18 +8466,9 @@ func TestDistSpecExtensions(t *testing.T) {
defaultVal := true
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
mgmtConfg := &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
Mgmt: mgmtConfg,
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
@ -8477,14 +8489,51 @@ func TestDistSpecExtensions(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil)
t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 3)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 1)
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
// Verify the endpoints below are enabled by search
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPreferencesPrefix)
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmtPrefix)
// Verify the endpoints below are not enabled since trust is not enabled
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullCosign)
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullNotation)
// Verify the endpoints below are not enabled since the UI is not enabled
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullMgmt)
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullUserPrefs)
})
Convey("start zot server with no enabled extensions", t, func(c C) {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf.HTTP.Port = port
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // clean up
ctlr := makeController(conf, t.TempDir(), "")
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
var extensionList distext.ExtensionList
resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil)
t.Log(extensionList.Extensions)
// Verify all endpoints which are disabled (even signing urls depend on search being enabled)
So(len(extensionList.Extensions), ShouldEqual, 0)
})
Convey("start minimal zot server", t, func(c C) {

View file

@ -19,9 +19,12 @@ import (
"sort"
"strconv"
"strings"
"time"
guuid "github.com/gofrs/uuid"
"github.com/google/go-github/v52/github"
"github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
"github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -40,6 +43,7 @@ import (
syncConstants "zotregistry.io/zot/pkg/extensions/sync/constants"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta"
mTypes "zotregistry.io/zot/pkg/meta/types"
zreg "zotregistry.io/zot/pkg/regexp"
localCtx "zotregistry.io/zot/pkg/requestcontext"
storageCommon "zotregistry.io/zot/pkg/storage/common"
@ -80,6 +84,19 @@ func (rh *RouteHandler) SetupRoutes() {
}
}
if rh.c.Config.IsAPIKeyEnabled() {
// enable api key management urls
apiKeyRouter := rh.c.Router.PathPrefix(constants.APIKeyPath).Subrouter()
apiKeyRouter.Use(authHandler)
apiKeyRouter.Use(BaseAuthzHandler(rh.c))
apiKeyRouter.Use(zcommon.ACHeadersMiddleware(rh.c.Config,
http.MethodPost, http.MethodDelete, http.MethodOptions))
apiKeyRouter.Use(zcommon.CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin))
apiKeyRouter.Methods(http.MethodPost, http.MethodOptions).HandlerFunc(rh.CreateAPIKey)
apiKeyRouter.Methods(http.MethodDelete).HandlerFunc(rh.RevokeAPIKey)
}
/* on every route which may be used by UI we set OPTIONS as allowed METHOD
to enable preflight request from UI to backend */
if rh.c.Config.IsBasicAuthnEnabled() {
@ -157,61 +174,42 @@ func (rh *RouteHandler) SetupRoutes() {
// swagger
debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
// gql playground
gqlPlayground.SetupGQLPlaygroundRoutes(prefixedRouter, rh.c.StoreController, rh.c.Log)
// Setup Extensions Routes
// setup extension routes
if rh.c.Config != nil {
// This logic needs to be reviewed, it should depend on build options
// not the general presence of the extensions in config
if rh.c.Config.Extensions == nil {
// minimal build
prefixedRouter.HandleFunc("/metrics", rh.GetMetrics).Methods("GET")
} else {
// extended build
prefixedExtensionsRouter := prefixedRouter.PathPrefix(constants.ExtPrefix).Subrouter()
prefixedExtensionsRouter.Use(CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin))
ext.SetupMgmtRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.Log)
ext.SetupSearchRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo,
rh.c.Log)
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.MetaDB,
rh.c.CveInfo, rh.c.Log)
ext.SetupAPIKeyRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.MetaDB, rh.c.CookieStore, rh.c.Log)
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, authHandler, rh.c.Log)
gqlPlayground.SetupGQLPlaygroundRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.Log)
// last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer.
ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log)
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
}
}
}
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
addCORSHeaders(allowOrigin, response)
next.ServeHTTP(response, request)
})
}
// Preconditions for enabling the actual extension routes are part of extensions themselves
ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo,
rh.c.Log)
ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
// last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer.
ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.Log)
}
func getCORSHeadersHandler(allowOrigin string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
addCORSHeaders(allowOrigin, response)
zcommon.AddCORSHeaders(allowOrigin, response)
next.ServeHTTP(response, request)
})
}
}
func addCORSHeaders(allowOrigin string, response http.ResponseWriter) {
if allowOrigin == "" {
response.Header().Set("Access-Control-Allow-Origin", "*")
} else {
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
}
}
func getUIHeadersHandler(config *config.Config, allowedMethods ...string) func(http.HandlerFunc) http.HandlerFunc {
allowedMethodsValue := strings.Join(allowedMethods, ",")
@ -1980,6 +1978,123 @@ func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request *
zcommon.WriteJSON(response, http.StatusOK, rs)
}
type APIKeyPayload struct { //nolint:revive
Label string `json:"label"`
Scopes []string `json:"scopes"`
}
// CreateAPIKey godoc
// @Summary Create an API key for the current user
// @Description Can create an api key for a logged in user, based on the provided label and scopes.
// @Accept json
// @Produce json
// @Param id body APIKeyPayload true "api token id (UUID)"
// @Success 201 {string} string "created"
// @Failure 400 {string} string "bad request"
// @Failure 401 {string} string "unauthorized"
// @Failure 500 {string} string "internal server error"
// @Router /auth/apikey [post].
func (rh *RouteHandler) CreateAPIKey(resp http.ResponseWriter, req *http.Request) {
var payload APIKeyPayload
body, err := io.ReadAll(req.Body)
if err != nil {
rh.c.Log.Error().Msg("unable to read request body")
resp.WriteHeader(http.StatusInternalServerError)
return
}
err = json.Unmarshal(body, &payload)
if err != nil {
resp.WriteHeader(http.StatusBadRequest)
return
}
apiKey, apiKeyID, err := GenerateAPIKey(guuid.DefaultGenerator, rh.c.Log)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
return
}
hashedAPIKey := hashUUID(apiKey)
apiKeyDetails := &mTypes.APIKeyDetails{
CreatedAt: time.Now(),
LastUsed: time.Now(),
CreatorUA: req.UserAgent(),
GeneratedBy: "manual",
Label: payload.Label,
Scopes: payload.Scopes,
UUID: apiKeyID,
}
err = rh.c.MetaDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)
if err != nil {
rh.c.Log.Error().Err(err).Msg("error storing API key")
resp.WriteHeader(http.StatusInternalServerError)
return
}
apiKeyResponse := struct {
mTypes.APIKeyDetails
APIKey string `json:"apiKey"`
}{
APIKey: fmt.Sprintf("%s%s", constants.APIKeysPrefix, apiKey),
APIKeyDetails: *apiKeyDetails,
}
json := jsoniter.ConfigCompatibleWithStandardLibrary
data, err := json.Marshal(apiKeyResponse)
if err != nil {
rh.c.Log.Error().Err(err).Msg("unable to marshal api key response")
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.Header().Set("Content-Type", constants.DefaultMediaType)
resp.WriteHeader(http.StatusCreated)
_, _ = resp.Write(data)
}
// RevokeAPIKey godoc
// @Summary Revokes one current user API key
// @Description Revokes one current user API key based on given key ID
// @Accept json
// @Produce json
// @Param id query string true "api token id (UUID)"
// @Success 200 {string} string "ok"
// @Failure 500 {string} string "internal server error"
// @Failure 401 {string} string "unauthorized"
// @Failure 400 {string} string "bad request"
// @Router /auth/apikey [delete].
func (rh *RouteHandler) RevokeAPIKey(resp http.ResponseWriter, req *http.Request) {
ids, ok := req.URL.Query()["id"]
if !ok || len(ids) != 1 {
resp.WriteHeader(http.StatusBadRequest)
return
}
keyID := ids[0]
err := rh.c.MetaDB.DeleteUserAPIKey(req.Context(), keyID)
if err != nil {
rh.c.Log.Error().Err(err).Str("keyID", keyID).Msg("error deleting API key")
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.WriteHeader(http.StatusOK)
}
// GetBlobUploadSessionLocation returns actual blob location to start/resume uploading blobs.
// e.g. /v2/<name>/blobs/uploads/<session-id>.
func getBlobUploadSessionLocation(url *url.URL, sessionID string) string {
@ -2009,9 +2124,7 @@ func getBlobUploadLocation(url *url.URL, name string, digest godigest.Digest) st
}
func isSyncOnDemandEnabled(ctlr Controller) bool {
if ctlr.Config.Extensions != nil &&
ctlr.Config.Extensions.Sync != nil &&
*ctlr.Config.Extensions.Sync.Enable &&
if ctlr.Config.IsSyncEnabled() &&
fmt.Sprintf("%v", ctlr.SyncOnDemand) != fmt.Sprintf("%v", nil) {
return true
}

View file

@ -1,5 +1,5 @@
//go:build sync && scrub && metrics && search && lint && apikey && mgmt
// +build sync,scrub,metrics,search,lint,apikey,mgmt
//go:build sync && scrub && metrics && search && lint && mgmt
// +build sync,scrub,metrics,search,lint,mgmt
package api_test
@ -7,7 +7,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
@ -28,8 +27,6 @@ import (
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config"
mTypes "zotregistry.io/zot/pkg/meta/types"
localCtx "zotregistry.io/zot/pkg/requestcontext"
storageTypes "zotregistry.io/zot/pkg/storage/types"
@ -37,8 +34,6 @@ import (
"zotregistry.io/zot/pkg/test/mocks"
)
var ErrUnexpectedError = errors.New("error: unexpected error")
const sessionStr = "session"
func TestRoutes(t *testing.T) {
@ -62,6 +57,8 @@ func TestRoutes(t *testing.T) {
}()
mockOIDCConfig := mockOIDCServer.Config()
defaultVal := true
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
@ -77,14 +74,7 @@ func TestRoutes(t *testing.T) {
},
},
},
}
defaultVal := true
apiKeyConfig := &extconf.APIKeyConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
APIKey: apiKeyConfig,
APIKey: defaultVal,
}
ctlr := api.NewController(conf)
@ -1434,14 +1424,14 @@ func TestRoutes(t *testing.T) {
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
response := httptest.NewRecorder()
extensions.CreateAPIKey(response, request, ctlr.MetaDB, ctlr.CookieStore, ctlr.Log)
rthdlr.CreateAPIKey(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
acCtx := localCtx.AccessControlContext{
Username: username,
Username: "test",
}
ctx = context.TODO()
@ -1451,14 +1441,14 @@ func TestRoutes(t *testing.T) {
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
response = httptest.NewRecorder()
extensions.CreateAPIKey(response, request, ctlr.MetaDB, ctlr.CookieStore, ctlr.Log)
rthdlr.CreateAPIKey(response, request)
resp = response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
payload := extensions.APIKeyPayload{
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
}
@ -1468,11 +1458,12 @@ func TestRoutes(t *testing.T) {
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(reqBody))
response = httptest.NewRecorder()
extensions.CreateAPIKey(response, request, mocks.MetaDBMock{
ctlr.MetaDB = mocks.MetaDBMock{
AddUserAPIKeyFn: func(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
return ErrUnexpectedError
},
}, ctlr.CookieStore, ctlr.Log)
}
rthdlr.CreateAPIKey(response, request)
resp = response.Result()
defer resp.Body.Close()
@ -1486,11 +1477,12 @@ func TestRoutes(t *testing.T) {
q.Add("id", "apikeyid")
request.URL.RawQuery = q.Encode()
extensions.RevokeAPIKey(response, request, mocks.MetaDBMock{
ctlr.MetaDB = mocks.MetaDBMock{
DeleteUserAPIKeyFn: func(ctx context.Context, id string) error {
return ErrUnexpectedError
},
}, ctlr.CookieStore, ctlr.Log)
}
rthdlr.RevokeAPIKey(response, request)
resp = response.Result()
defer resp.Body.Close()

View file

@ -1,14 +1,12 @@
//go:build sync && scrub && metrics && search && apikey
// +build sync,scrub,metrics,search,apikey
//go:build sync && scrub && metrics && search && userprefs && mgmt && imagetrust
// +build sync,scrub,metrics,search,userprefs,mgmt,imagetrust
package cli_test
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"testing"
"time"
@ -640,7 +638,7 @@ func TestServeSearchEnabled(t *testing.T) {
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}`
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
if !found {
data, err := os.ReadFile(logPath)
@ -691,7 +689,7 @@ func TestServeSearchEnabledCVE(t *testing.T) {
substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"}}}"
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
defer func() {
if !found {
@ -704,7 +702,7 @@ func TestServeSearchEnabledCVE(t *testing.T) {
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = readLogFileAndSearchString(logPath, "updating the CVE database", readLogFileTimeout)
found, err = ReadLogFileAndSearchString(logPath, "updating the CVE database", readLogFileTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
})
@ -741,7 +739,7 @@ func TestServeSearchEnabledNoCVE(t *testing.T) {
defer os.Remove(logPath) // clean up
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}` //nolint:lll // gofumpt conflicts with lll
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
if !found {
data, err := os.ReadFile(logPath)
@ -815,20 +813,31 @@ func TestServeMgmtExtension(t *testing.T) {
"output": "%s"
},
"extensions": {
"Mgmt": {
"ui": {
"enable": true
},
"search": {
"enable": true
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":true}")
found, err := ReadLogFileAndSearchString(logPath, "setting up mgmt routes", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Mgmt disabled", t, func(c C) {
Convey("Mgmt disabled - UI unconfigured", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
@ -842,27 +851,66 @@ func TestServeMgmtExtension(t *testing.T) {
"output": "%s"
},
"extensions": {
"Mgmt": {
"enable": "false"
"search": {
"enable": true
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
defer os.Remove(logPath) // clean up
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Mgmt disabled - extensions missing", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":false}")
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
}
func TestServeAPIKeyExtension(t *testing.T) {
func TestServeImageTrustExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("apikey implicitly enabled", t, func(c C) {
Convey("Trust explicitly disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
@ -876,20 +924,29 @@ func TestServeAPIKeyExtension(t *testing.T) {
"output": "%s"
},
"extensions": {
"apikey": {
"trust": {
"enable": false
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":true}")
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("apikey disabled", t, func(c C) {
Convey("Trust explicitly enabled - but cosign and notation disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
@ -903,79 +960,75 @@ func TestServeAPIKeyExtension(t *testing.T) {
"output": "%s"
},
"extensions": {
"apikey": {
"enable": "false"
"trust": {
"enable": true
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
defer os.Remove(logPath) // clean up
found, err := ReadLogFileAndSearchString(logPath,
"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
Convey("Trust explicitly enabled - cosign and notation enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"trust": {
"enable": true,
"cosign": true,
"notation": true
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":false}")
found, err := ReadLogFileAndSearchString(logPath,
"setting up image trust routes", 10*time.Second)
defer func() {
if !found {
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = ReadLogFileAndSearchString(logPath,
"setting up notation route", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = ReadLogFileAndSearchString(logPath,
"setting up cosign route", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
}
func readLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { //nolint:unparam,lll
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
for {
select {
case <-ctx.Done():
return false, nil
default:
content, err := os.ReadFile(logPath)
if err != nil {
return false, err
}
if strings.Contains(string(content), stringToMatch) {
return true, nil
}
}
}
}
// run cli and return output.
func runCLIWithConfig(tempDir string, config string) (string, error) {
port := GetFreePort()
baseURL := GetBaseURL(port)
logFile, err := os.CreateTemp(tempDir, "zot-log*.txt")
if err != nil {
return "", err
}
cfgfile, err := os.CreateTemp(tempDir, "zot-test*.json")
if err != nil {
return "", err
}
config = fmt.Sprintf(config, tempDir, port, logFile.Name())
_, err = cfgfile.Write([]byte(config))
if err != nil {
return "", err
}
err = cfgfile.Close()
if err != nil {
return "", err
}
os.Args = []string{"cli_test", "serve", cfgfile.Name()}
go func() {
err = cli.NewServerRootCmd().Execute()
if err != nil {
panic(err)
}
}()
WaitTillServerReady(baseURL)
return logFile.Name(), nil
}

View file

@ -757,10 +757,10 @@ func TestOutputFormat(t *testing.T) {
`"variant":""},"isSigned":false,"downloadCount":0,`+
`"layers":[{"size":"","digest":"sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6",`+
`"score":0}],"history":null,"vulnerabilities":{"maxSeverity":"","count":0},`+
`"referrers":null,"artifactType":""}],"size":"123445",`+
`"referrers":null,"artifactType":"","signatureInfo":null}],"size":"123445",`+
`"downloadCount":0,"lastUpdated":"0001-01-01T00:00:00Z","description":"","isSigned":false,"licenses":"",`+
`"labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",`+
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}`+"\n")
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}`+"\n")
So(err, ShouldBeNil)
})
@ -788,10 +788,10 @@ func TestOutputFormat(t *testing.T) {
`issigned: false downloadcount: 0 layers: - size: "" `+
`digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+
`size: "123445" downloadcount: 0 `+
`signatureinfo: [] size: "123445" downloadcount: 0 `+
`lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+
`title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: "" `+
`count: 0 referrers: []`,
`count: 0 referrers: [] signatureinfo: []`,
)
So(err, ShouldBeNil)
@ -822,10 +822,10 @@ func TestOutputFormat(t *testing.T) {
`issigned: false downloadcount: 0 layers: - size: "" `+
`digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+
`size: "123445" downloadcount: 0 `+
`signatureinfo: [] size: "123445" downloadcount: 0 `+
`lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+
`title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: `+
`"" count: 0 referrers: []`,
`"" count: 0 referrers: [] signatureinfo: []`,
)
So(err, ShouldBeNil)
})
@ -886,10 +886,11 @@ func TestOutputFormatGQL(t *testing.T) {
`"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` +
`"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` +
`"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` +
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"artifactType":""}],` +
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` +
`"referrers":null,"artifactType":"","signatureInfo":null}],` +
`"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` +
`"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` +
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}` + "\n" +
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n" +
`{"repoName":"repo7","tag":"test:2.0",` +
`"digest":"sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06",` +
`"mediaType":"application/vnd.oci.image.manifest.v1+json",` +
@ -898,10 +899,11 @@ func TestOutputFormatGQL(t *testing.T) {
`"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` +
`"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` +
`"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` +
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"artifactType":""}],` +
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` +
`"referrers":null,"artifactType":"","signatureInfo":null}],` +
`"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` +
`"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` +
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}` + "\n"
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n"
// Output is supposed to be in json lines format, keep all spaces as is for verification
So(buff.String(), ShouldEqual, expectedStr)
So(err, ShouldBeNil)
@ -928,10 +930,11 @@ func TestOutputFormatGQL(t *testing.T) {
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
`issigned: false downloadcount: 0 layers: - size: "15" ` +
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
`history: [] vulnerabilities: maxseverity: "" ` +
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] ` +
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: [] ` +
`--- reponame: repo7 tag: test:2.0 ` +
`digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` +
`mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` +
@ -940,10 +943,11 @@ func TestOutputFormatGQL(t *testing.T) {
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
`issigned: false downloadcount: 0 layers: - size: "15" ` +
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
`history: [] vulnerabilities: maxseverity: "" ` +
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: []`
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []`
So(strings.TrimSpace(str), ShouldEqual, expectedStr)
So(err, ShouldBeNil)
})
@ -969,10 +973,12 @@ func TestOutputFormatGQL(t *testing.T) {
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
`issigned: false downloadcount: 0 layers: - size: "15" ` +
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
`history: [] vulnerabilities: maxseverity: "" ` +
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] ` +
`authors: "" vendor: "" vulnerabilities: maxseverity: "" ` +
`count: 0 referrers: [] signatureinfo: [] ` +
`--- reponame: repo7 tag: test:2.0 ` +
`digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` +
`mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` +
@ -981,10 +987,11 @@ func TestOutputFormatGQL(t *testing.T) {
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
`issigned: false downloadcount: 0 layers: - size: "15" ` +
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
`history: [] vulnerabilities: maxseverity: "" ` +
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: []`
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []`
So(strings.TrimSpace(str), ShouldEqual, expectedStr)
So(err, ShouldBeNil)
})

View file

@ -313,13 +313,18 @@ func validateCacheConfig(cfg *config.Config) error {
}
func validateExtensionsConfig(cfg *config.Config) error {
if cfg.Extensions != nil && cfg.Extensions.Mgmt != nil {
log.Warn().Msg("The mgmt extensions configuration option has been made redundant and will be ignored.")
}
if cfg.Extensions != nil && cfg.Extensions.APIKey != nil {
log.Warn().Msg("The apikey extension configuration will be ignored as API keys " +
"are now configurable in the HTTP settings.")
}
if cfg.Extensions != nil && cfg.Extensions.UI != nil && cfg.Extensions.UI.Enable != nil && *cfg.Extensions.UI.Enable {
if cfg.Extensions.Mgmt == nil || !*cfg.Extensions.Mgmt.Enable {
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without mgmt extension.")
return errors.ErrBadConfig
}
// it would make sense to also check for mgmt and user prefs to be enabled,
// but those are both enabled by having the search and ui extensions enabled
if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
@ -513,18 +518,18 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
config.Extensions.Scrub = &extconf.ScrubConfig{}
}
_, ok = extMap["mgmt"]
_, ok = extMap["trust"]
if ok {
// we found a config like `"extensions": {"mgmt:": {}}`
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.Mgmt = &extconf.MgmtConfig{}
// we found a config like `"extensions": {"trust:": {}}`
// Note: In case trust is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.Trust = &extconf.ImageTrustConfig{}
}
_, ok = extMap["apikey"]
_, ok = extMap["ui"]
if ok {
// we found a config like `"extensions": {"mgmt:": {}}`
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.APIKey = &extconf.APIKeyConfig{}
// we found a config like `"extensions": {"ui:": {}}`
// Note: In case UI is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.UI = &extconf.UIConfig{}
}
}
@ -586,18 +591,6 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
}
}
if config.Extensions.Mgmt != nil {
if config.Extensions.Mgmt.Enable == nil {
config.Extensions.Mgmt.Enable = &defaultVal
}
}
if config.Extensions.APIKey != nil {
if config.Extensions.APIKey.Enable == nil {
config.Extensions.APIKey.Enable = &defaultVal
}
}
if config.Extensions.Scrub != nil {
if config.Extensions.Scrub.Enable == nil {
config.Extensions.Scrub.Enable = &defaultVal
@ -607,6 +600,18 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
config.Extensions.Scrub.Interval = 24 * time.Hour //nolint: gomnd
}
}
if config.Extensions.UI != nil {
if config.Extensions.UI.Enable == nil {
config.Extensions.UI.Enable = &defaultVal
}
}
if config.Extensions.Trust != nil {
if config.Extensions.Trust.Enable == nil {
config.Extensions.Trust.Enable = &defaultVal
}
}
}
if !config.Storage.GC && viperInstance.Get("storage::gcdelay") == nil {
@ -663,6 +668,12 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
config.Storage.SubPaths[name] = storageConfig
}
}
// if OpenID authentication is enabled,
// API Keys are also enabled in order to provide data path authentication
if config.HTTP.Auth != nil && config.HTTP.Auth.OpenID != nil {
config.HTTP.Auth.APIKey = true
}
}
func updateDistSpecVersion(config *config.Config) {

View file

@ -1083,7 +1083,7 @@ func TestVerify(t *testing.T) {
}
func TestValidateExtensionsConfig(t *testing.T) {
Convey("Test missing extensions for UI to work", t, func(c C) {
Convey("Legacy extensions should not error", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
@ -1100,40 +1100,39 @@ func TestValidateExtensionsConfig(t *testing.T) {
"level": "debug"
},
"extensions": {
"ui": {
"enable": "true"
}
}
}`)
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldNotBeNil)
})
Convey("Test missing extensions for UI to work", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
content := []byte(`{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"ui": {
"enable": "true"
},
"mgmt": {
"enable": "true"
},
"apikey": {
"enable": "true"
}
}
}`)
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldBeNil)
})
Convey("Test missing extensions for UI to work", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
content := []byte(`{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"ui": {
"enable": "true"
}
}
}`)
@ -1143,7 +1142,7 @@ func TestValidateExtensionsConfig(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("Test missing mgmt extension for UI to work", t, func(c C) {
Convey("Test enabling UI extension with all prerequisites", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
@ -1172,7 +1171,165 @@ func TestValidateExtensionsConfig(t *testing.T) {
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldNotBeNil)
So(err, ShouldBeNil)
})
Convey("Test extension are implicitly enabled", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
content := []byte(`{
"storage": {
"rootDirectory": "%/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"ui": {},
"search": {},
"metrics": {},
"trust": {},
"scrub": {}
}
}`)
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldBeNil)
So(config.Extensions.UI, ShouldNotBeNil)
So(*config.Extensions.UI.Enable, ShouldBeTrue)
So(config.Extensions.Search, ShouldNotBeNil)
So(*config.Extensions.Search.Enable, ShouldBeTrue)
So(config.Extensions.Trust, ShouldNotBeNil)
So(*config.Extensions.Trust.Enable, ShouldBeTrue)
So(*config.Extensions.Metrics, ShouldNotBeNil)
So(*config.Extensions.Metrics.Enable, ShouldBeTrue)
So(config.Extensions.Scrub, ShouldNotBeNil)
So(*config.Extensions.Scrub.Enable, ShouldBeTrue)
})
}
func TestApiKeyConfig(t *testing.T) {
Convey("Test API Keys are enabled if OpenID is enabled", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex",
"clientid":"client_id","scopes":["openid"]}}}}},
"log":{"level":"debug"}}`)
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldBeNil)
So(config.HTTP.Auth, ShouldNotBeNil)
So(config.HTTP.Auth.APIKey, ShouldBeTrue)
})
Convey("Test API Keys are not enabled by default", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot"},
"log":{"level":"debug"}}`)
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldBeNil)
So(config.HTTP.Auth, ShouldNotBeNil)
So(config.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Test API Keys are not enabled if OpenID is not enabled", t, func(c C) {
config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"htpasswd":{"path":"test/data/htpasswd"}}},
"log":{"level":"debug"}}`)
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name())
So(err, ShouldBeNil)
So(config.HTTP.Auth, ShouldNotBeNil)
So(config.HTTP.Auth.APIKey, ShouldBeFalse)
})
}
func TestServeAPIKey(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("apikey implicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s",
"auth": {
"apikey": true
}
},
"log": {
"level": "debug",
"output": "%s"
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"APIKey\":true")
})
Convey("apikey disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s",
"auth": {
"apikey": false
}
},
"log": {
"level": "debug",
"output": "%s"
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"APIKey\":false")
})
}
@ -1557,3 +1714,44 @@ func TestScrub(t *testing.T) {
})
})
}
// run cli and return output.
func runCLIWithConfig(tempDir string, config string) (string, error) {
port := GetFreePort()
baseURL := GetBaseURL(port)
logFile, err := os.CreateTemp(tempDir, "zot-log*.txt")
if err != nil {
return "", err
}
cfgfile, err := os.CreateTemp(tempDir, "zot-test*.json")
if err != nil {
return "", err
}
config = fmt.Sprintf(config, tempDir, port, logFile.Name())
_, err = cfgfile.Write([]byte(config))
if err != nil {
return "", err
}
err = cfgfile.Close()
if err != nil {
return "", err
}
os.Args = []string{"cli_test", "serve", cfgfile.Name()}
go func() {
err = cli.NewServerRootCmd().Execute()
if err != nil {
panic(err)
}
}()
WaitTillServerReady(baseURL)
return logFile.Name(), nil
}

View file

@ -13,6 +13,7 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
apiErr "zotregistry.io/zot/pkg/api/errors"
localCtx "zotregistry.io/zot/pkg/requestcontext"
)
func AllowedMethods(methods ...string) []string {
@ -29,7 +30,7 @@ func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen
}
}
func ACHeadersHandler(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
allowedMethodsValue := strings.Join(allowedMethods, ",")
return func(next http.Handler) http.Handler {
@ -50,6 +51,54 @@ func ACHeadersHandler(config *config.Config, allowedMethods ...string) mux.Middl
}
}
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
AddCORSHeaders(allowOrigin, response)
next.ServeHTTP(response, request)
})
}
}
func AddCORSHeaders(allowOrigin string, response http.ResponseWriter) {
if allowOrigin == "" {
response.Header().Set("Access-Control-Allow-Origin", "*")
} else {
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
}
}
// AuthzOnlyAdminsMiddleware permits only admin user access if auth is enabled.
func AuthzOnlyAdminsMiddleware(conf *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if !conf.IsBasicAuthnEnabled() {
next.ServeHTTP(response, request)
return
}
// get acCtx built in previous authn/authz middlewares
acCtx, err := localCtx.GetAccessControlContext(request.Context())
if err != nil { // should not happen as this has been previously checked for errors
AuthzFail(response, request, conf.HTTP.Realm, conf.HTTP.Auth.FailDelay)
return
}
// reject non-admin access if authentication is enabled
if acCtx != nil && !acCtx.IsAdmin {
AuthzFail(response, request, conf.HTTP.Realm, conf.HTTP.Auth.FailDelay)
return
}
next.ServeHTTP(response, request)
})
}
}
func AuthzFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
time.Sleep(time.Duration(delay) * time.Second)

View file

@ -52,6 +52,7 @@ type ImageSummary struct {
Vendor string `json:"vendor"`
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
Referrers []Referrer `json:"referrers"`
SignatureInfo []SignatureSummary `json:"signatureInfo"`
}
type ManifestSummary struct {
@ -67,6 +68,13 @@ type ManifestSummary struct {
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
Referrers []Referrer `json:"referrers"`
ArtifactType string `json:"artifactType"`
SignatureInfo []SignatureSummary `json:"signatureInfo"`
}
type SignatureSummary struct {
Tool string `json:"tool"`
IsTrusted bool `json:"isTrusted"`
Author string `json:"author"`
}
type Platform struct {

View file

@ -10,7 +10,6 @@ import (
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
debugCst "zotregistry.io/zot/pkg/debug/constants"
"zotregistry.io/zot/pkg/log"
@ -21,7 +20,7 @@ import (
var playgroundHTML embed.FS
// SetupGQLPlaygroundRoutes ...
func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router,
func SetupGQLPlaygroundRoutes(router *mux.Router,
storeController storage.StoreController, l log.Logger,
) {
log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()}

View file

@ -6,13 +6,12 @@ package debug
import (
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// SetupGQLPlaygroundRoutes ...
func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router,
func SetupGQLPlaygroundRoutes(router *mux.Router,
storeController storage.StoreController, log log.Logger,
) {
log.Warn().Msg("skipping enabling graphql playground extension because given zot binary " +

View file

@ -1,66 +0,0 @@
# `API keys`
zot allows authentication for REST API calls using your API key as an alternative to your password.
* User can create/revoke his API key.
* Can not be retrieved, it is shown to the user only the first time is created.
* An API key has the same rights as the user who generated it.
## API keys REST API
### Create API Key
**Description**: Create an API key for the current user.
**Usage**: POST /v2/_zot/ext/apikey
**Produces**: application/json
**Sample input**:
```
POST /api/security/apiKey
Body: {"label": "git", "scopes": ["repo1", "repo2"]}'
```
**Example cURL**
```
curl -u user:password -X POST http://localhost:8080/v2/_zot/ext/apikey -d '{"label": "myLabel", "scopes": ["repo1", "repo2"]}'
```
**Sample output**:
```json
{
"createdAt": "2023-05-05T15:39:28.420926+03:00",
"creatorUa": "curl/7.68.0",
"generatedBy": "manual",
"lastUsed": "2023-05-05T15:39:28.4209282+03:00",
"label": "git",
"scopes": [
"repo1",
"repo2"
],
"uuid": "46a45ce7-5d92-498a-a9cb-9654b1da3da1",
"apiKey": "zak_e77bcb9e9f634f1581756abbf9ecd269"
}
```
**Using API keys cURL**
```
curl -u user:zak_e77bcb9e9f634f1581756abbf9ecd269 http://localhost:8080/v2/_catalog
```
### Revoke API Key
**Description**: Revokes one current user API key by api key UUID
**Usage**: DELETE /api/security/apiKey?id=$uuid
**Produces**: application/json
**Example cURL**
```
curl -u user:password -X DELETE http://localhost:8080/v2/_zot/ext/apikey?id=46a45ce7-5d92-498a-a9cb-9654b1da3da1
```

View file

@ -1,48 +1,77 @@
# Verifying signatures
# Image Trust
The `imagetrust` extension provides a mechanism to verify image signatures using certificates and public keys
## How to configure zot for verifying signatures
In order to configure zot for verifying signatures, the user should provide:
In order to configure zot for verifying signatures, the user should first enable this feature:
1. public keys (which correspond to the private keys used to sign images with `cosign`)
```json
"extensions": {
"trust": {
"enable": true,
"cosign": true,
"notation": true
}
}
```
or
In order for verification to run, the user needs to enable at least one of the cosign or notation options above.
2. certificates (used to sign images with `notation`)
## Uploading public keys or certificates
These files could be uploaded using one of these requests:
Next the user needs to upload the keys or certificates used for the verification.
1. upload a public key
| Supported queries | Input | Output | Description |
| --- | --- | --- | --- |
| Upload a certificate | certificate | None | Add certificate for verifying notation signatures|
| Upload a public key | public key | None | Add public key for verifying cosign signatures |
***Example of request***
```
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=cosign"
```
### Uploading a Cosign public key
2. upload a certificate
The Cosign public keys uploaded correspond to the private keys used to sign images with `cosign`.
***Example of request***
```
curl --data-binary @filet.crt -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=notation&truststoreType=ca&truststoreName=upload-cert"
```
***Example of request***
Besides the requested files, the user should also specify the `tool` which should be :
- `cosign` for uploading public keys
- `notation` for uploading certificates
```bash
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/cosign
```
Also, if the uploaded file is a certificate then the user should also specify the type of the truststore through `truststoreType` param and also its name through `truststoreName` param.
As a result of this request, the uploaded file will be stored in `_cosign` directory
under the rootDir specified in the zot config.
### Uploading a Notation certificate
Notation certificates are used to sign images with the `notation` tool.
The user needs to specify the type of the truststore through the `truststoreType`
query parameter and its name through the `truststoreName` parameter.
`truststoreType` defaults to `ca`, while `truststoreName` is a mandatory parameter.
***Example of request***
```bash
curl --data-binary @certificate.crt -X POST "http://localhost:8080/v2/_zot/ext/notation?truststoreType=ca&truststoreName=upload-cert"
```
As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/{truststoreName}`
directory under the rootDir specified in the zot config.
The `truststores` field found in `_notation/trustpolicy.json` file will be updated automatically as well.
## Verification and results
Based on the uploaded files, signatures verification will be performed for all the signed images.
The information determined about the signatures will be:
Based on the uploaded files, signatures verification will be performed for all the signed images. Then the information known about the signatures will be:
- the tool used to generate the signature (`cosign` or `notation`)
- info about the trustworthiness of the signature (if there is a certificate or a public key which can successfully verify the signature)
- the author of the signature which will be:
- the public key -> for signatures generated using `cosign`
- the subject of the certificate -> for signatures generated using `notation`
**Example of GraphQL output**
- the public key -> for signatures generated using `cosign`
- the subject of the certificate -> for signatures generated using `notation`
The information above will be included in the ManifestSummary objects returned by the `search` extension.
***Example of GraphQL output***
```json
{
@ -90,12 +119,13 @@ Besides the requested files, the user should also specify the `tool` which shoul
## Notes
- The files (public keys and certificates) uploaded using the exposed routes will be stored in some specific directories called `_cosign` and `_notation` under `$rootDir`.
- `_cosign` directory will contain the uploaded public keys
```
_cosign
├── $publicKey1
└── $publicKey2
└── $publicKey2
```
- `_notation` directory will have this structure:
@ -103,15 +133,16 @@ Besides the requested files, the user should also specify the `tool` which shoul
```
_notation
├── trustpolicy.json
└── truststore
└── x509
└── $truststoreType
└── $truststoreName
└── $certificate
└── truststore
└── x509
└── $truststoreType
└── $truststoreName
└── $certificate
```
where `trustpolicy.json` file has this default content which can not be modified by the user and which is updated each time a new certificate is added to a new truststore:
```
```json
{
"version": "1.0",
"trustPolicies": [
@ -127,6 +158,5 @@ Besides the requested files, the user should also specify the `tool` which shoul
]
}
]
}
}
```

View file

@ -10,12 +10,6 @@ Response depends on the user privileges:
| Supported queries | Input | Output | Description |
| --- | --- | --- | --- |
| [Get current configuration](#get-current-configuration) | None | config json | Get current zot configuration |
| [Upload a certificate](#post-certificate) | certificate | None | Add certificate for verifying notation signatures|
| [Upload a public key](#post-public-key) | public key | None | Add public key for verifying cosign signatures |
## General usage
The mgmt endpoint accepts as a query parameter what `resource` is targeted by the request and then all other required parameters for the specified resource. The default value of this
query parameter is `config`.
## Get current configuration
@ -46,35 +40,3 @@ curl http://localhost:8080/v2/_zot/ext/mgmt | jq
If ldap or htpasswd are enabled mgmt will return `{"htpasswd": {}}` indicating that clients can authenticate with basic auth credentials.
If any key is present under `'auth'` key, in the mgmt response, it means that particular authentication method is enabled.
## Configure zot for verifying signatures
If the `resource` is `signatures` then the mgmt endpoint accepts as a query parameter the `tool` that corresponds to the uploaded file and then all other required parameters for the specified tool.
### Upload a certificate
**Sample request**
| Tool | Parameter | Parameter Type | Parameter Description |
| --- | --- | --- | --- |
| notation | truststoreType | string | The type of the truststore. This parameter is optional and its default value is `ca` |
| | truststoreName | string | The name of the truststore |
```bash
curl --data-binary @certificate.crt -X POST http://localhost:8080/v2/_zot/ext/mgmt?resource=signature&tool=notation&truststoreType=ca&truststoreName=newtruststore
```
As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/{truststoreName}` directory under $rootDir. And `truststores` field from `_notation/trustpolicy.json` file will be updated.
### Upload a public key
**Sample request**
| Tool | Parameter | Parameter Type | Parameter Description |
| --- | --- | --- | --- |
| cosign |
```bash
curl --data-binary @publicKey.pub -X POST http://localhost:8080/v2/_zot/ext/mgmt?resource=signature&tool=cosign
```
As a result of this request, the uploaded file will be stored in `_cosign` directory under $rootDir.

View file

@ -6,9 +6,10 @@
Component | Endpoint | Description
--- | --- | ---
[`search`](search/search.md) | `/v2/_zot/ext/search` | efficient and enhanced registry search capabilities using graphQL backend
[`mgmt`](mgmt.md) | `/v2/_zot/ext/mgmt` | config management
[`userprefs`](userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences
[`apikey`](README_apikey.md) | `/v2/_zot/ext/apikey` | user api keys management
[`mgmt`](README_mgmt.md) | `/v2/_zot/ext/mgmt` | config management
[`userprefs`](README_userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences
[`imagetrust`](README_imagetrust.md) | `/v2/_zot/ext/cosign` | cosign public key management
[`imagetrust`](README_imagetrust.md) | `/v2/_zot/ext/notation` | notation certificate management
# References

View file

@ -20,6 +20,13 @@ type ExtensionConfig struct {
UI *UIConfig
Mgmt *MgmtConfig
APIKey *APIKeyConfig
Trust *ImageTrustConfig
}
type ImageTrustConfig struct {
BaseConfig `mapstructure:",squash"`
Cosign bool
Notation bool
}
type APIKeyConfig struct {

View file

@ -1,197 +0,0 @@
//go:build apikey
// +build apikey
package extensions
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
guuid "github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
jsoniter "github.com/json-iterator/go"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
)
func SetupAPIKeyRoutes(config *config.Config, router *mux.Router, metaDB mTypes.MetaDB,
cookieStore sessions.Store, log log.Logger,
) {
if config.Extensions.APIKey != nil && *config.Extensions.APIKey.Enable {
log.Info().Msg("setting up api key routes")
allowedMethods := zcommon.AllowedMethods(http.MethodPost, http.MethodDelete)
apiKeyRouter := router.PathPrefix(constants.ExtAPIKey).Subrouter()
apiKeyRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
apiKeyRouter.Use(zcommon.AddExtensionSecurityHeaders())
apiKeyRouter.Methods(allowedMethods...).Handler(HandleAPIKeyRequest(metaDB, cookieStore, log))
}
}
type APIKeyPayload struct { //nolint:revive
Label string `json:"label"`
Scopes []string `json:"scopes"`
}
func HandleAPIKeyRequest(metaDB mTypes.MetaDB, cookieStore sessions.Store,
log log.Logger,
) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodPost:
CreateAPIKey(resp, req, metaDB, cookieStore, log) //nolint:contextcheck
return
case http.MethodDelete:
RevokeAPIKey(resp, req, metaDB, cookieStore, log) //nolint:contextcheck
return
}
})
}
// CreateAPIKey godoc
// @Summary Create an API key for the current user
// @Description Can create an api key for a logged in user, based on the provided label and scopes.
// @Accept json
// @Produce json
// @Success 201 {string} string "created"
// @Failure 401 {string} string "unauthorized"
// @Failure 500 {string} string "internal server error"
// @Router /v2/_zot/ext/apikey [post].
func CreateAPIKey(resp http.ResponseWriter, req *http.Request, metaDB mTypes.MetaDB,
cookieStore sessions.Store, log log.Logger,
) {
var payload APIKeyPayload
body, err := io.ReadAll(req.Body)
if err != nil {
log.Error().Msg("unable to read request body")
resp.WriteHeader(http.StatusInternalServerError)
return
}
err = json.Unmarshal(body, &payload)
if err != nil {
log.Error().Err(err).Msg("unable to unmarshal body")
resp.WriteHeader(http.StatusInternalServerError)
return
}
apiKeyBase, err := guuid.NewV4()
if err != nil {
log.Error().Err(err).Msg("unable to generate uuid")
resp.WriteHeader(http.StatusInternalServerError)
return
}
apiKey := strings.ReplaceAll(apiKeyBase.String(), "-", "")
hashedAPIKey := hashUUID(apiKey)
// will be used for identifying a specific api key
apiKeyID, err := guuid.NewV4()
if err != nil {
log.Error().Err(err).Msg("unable to generate uuid")
resp.WriteHeader(http.StatusInternalServerError)
return
}
apiKeyDetails := &mTypes.APIKeyDetails{
CreatedAt: time.Now(),
LastUsed: time.Now(),
CreatorUA: req.UserAgent(),
GeneratedBy: "manual",
Label: payload.Label,
Scopes: payload.Scopes,
UUID: apiKeyID.String(),
}
err = metaDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)
if err != nil {
log.Error().Err(err).Msg("error storing API key")
resp.WriteHeader(http.StatusInternalServerError)
return
}
apiKeyResponse := struct {
mTypes.APIKeyDetails
APIKey string `json:"apiKey"`
}{
APIKey: fmt.Sprintf("%s%s", constants.APIKeysPrefix, apiKey),
APIKeyDetails: *apiKeyDetails,
}
json := jsoniter.ConfigCompatibleWithStandardLibrary
data, err := json.Marshal(apiKeyResponse)
if err != nil {
log.Error().Err(err).Msg("unable to marshal api key response")
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.Header().Set("Content-Type", constants.DefaultMediaType)
resp.WriteHeader(http.StatusCreated)
_, _ = resp.Write(data)
}
// RevokeAPIKey godoc
// @Summary Revokes one current user API key
// @Description Revokes one current user API key based on given key ID
// @Accept json
// @Produce json
// @Param id path string true "api token id (UUID)"
// @Success 200 {string} string "ok"
// @Failure 500 {string} string "internal server error"
// @Failure 401 {string} string "unauthorized"
// @Failure 400 {string} string "bad request"
// @Router /v2/_zot/ext/apikey?id=UUID [delete].
func RevokeAPIKey(resp http.ResponseWriter, req *http.Request, metaDB mTypes.MetaDB,
cookieStore sessions.Store, log log.Logger,
) {
ids, ok := req.URL.Query()["id"]
if !ok || len(ids) != 1 {
resp.WriteHeader(http.StatusBadRequest)
return
}
keyID := ids[0]
err := metaDB.DeleteUserAPIKey(req.Context(), keyID)
if err != nil {
log.Error().Err(err).Str("keyID", keyID).Msg("error deleting API key")
resp.WriteHeader(http.StatusInternalServerError)
return
}
resp.WriteHeader(http.StatusOK)
}
func hashUUID(uuid string) string {
digester := sha256.New()
digester.Write([]byte(uuid))
return godigest.NewDigestFromEncoded(godigest.SHA256, fmt.Sprintf("%x", digester.Sum(nil))).Encoded()
}

View file

@ -1,20 +0,0 @@
//go:build !apikey
// +build !apikey
package extensions
import (
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
)
func SetupAPIKeyRoutes(config *config.Config, router *mux.Router, metaDB mTypes.MetaDB,
cookieStore sessions.Store, log log.Logger,
) {
log.Warn().Msg("skipping setting up API key routes because given zot binary doesn't include this feature," +
"please build a binary that does so")
}

View file

@ -0,0 +1,183 @@
//go:build imagetrust
// +build imagetrust
package extensions
import (
"errors"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/signatures"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
const (
ConfigResource = "config"
SignaturesResource = "signatures"
)
func IsBuiltWithImageTrustExtension() bool {
return true
}
func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, log log.Logger) {
if !conf.IsImageTrustEnabled() || (!conf.IsCosignEnabled() && !conf.IsNotationEnabled()) {
log.Info().Msg("skip enabling the image trust routes as the config prerequisites are not met")
return
}
log.Info().Msg("setting up image trust routes")
trust := ImageTrust{Conf: conf, Log: log}
allowedMethods := zcommon.AllowedMethods(http.MethodPost)
if conf.IsNotationEnabled() {
log.Info().Msg("setting up notation route")
notationRouter := router.PathPrefix(constants.ExtNotation).Subrouter()
notationRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
notationRouter.Use(zcommon.AddExtensionSecurityHeaders())
notationRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
// The endpoints for uploading signatures should be available only to admins
notationRouter.Use(zcommon.AuthzOnlyAdminsMiddleware(conf))
notationRouter.Methods(allowedMethods...).HandlerFunc(trust.HandleNotationCertificateUpload)
}
if conf.IsCosignEnabled() {
log.Info().Msg("setting up cosign route")
cosignRouter := router.PathPrefix(constants.ExtCosign).Subrouter()
cosignRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
cosignRouter.Use(zcommon.AddExtensionSecurityHeaders())
cosignRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
// The endpoints for uploading signatures should be available only to admins
cosignRouter.Use(zcommon.AuthzOnlyAdminsMiddleware(conf))
cosignRouter.Methods(allowedMethods...).HandlerFunc(trust.HandleCosignPublicKeyUpload)
}
log.Info().Msg("finished setting up image trust routes")
}
type ImageTrust struct {
Conf *config.Config
Log log.Logger
}
// Cosign handler godoc
// @Summary Upload cosign public keys for verifying signatures
// @Description Upload cosign public keys for verifying signatures
// @Router /v2/_zot/ext/cosign [post]
// @Accept octet-stream
// @Produce json
// @Param requestBody body string true "Public key content"
// @Success 200 {string} string "ok"
// @Failure 400 {string} string "bad request".
// @Failure 500 {string} string "internal server error".
func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWriter, request *http.Request) {
body, err := io.ReadAll(request.Body)
if err != nil {
trust.Log.Error().Err(err).Msg("image trust: couldn't read cosign key body")
response.WriteHeader(http.StatusInternalServerError)
return
}
err = signatures.UploadPublicKey(body)
if err != nil {
if errors.Is(err, zerr.ErrInvalidPublicKeyContent) {
response.WriteHeader(http.StatusBadRequest)
} else {
trust.Log.Error().Err(err).Msg("image trust: failed to save cosign key")
response.WriteHeader(http.StatusInternalServerError)
}
return
}
response.WriteHeader(http.StatusOK)
}
// Notation handler godoc
// @Summary Upload notation certificates for verifying signatures
// @Description Upload notation certificates for verifying signatures
// @Router /v2/_zot/ext/notation [post]
// @Accept octet-stream
// @Produce json
// @Param truststoreType query string false "truststore type"
// @Param truststoreName query string false "truststore name"
// @Param requestBody body string true "Certificate content"
// @Success 200 {string} string "ok"
// @Failure 400 {string} string "bad request".
// @Failure 500 {string} string "internal server error".
func (trust *ImageTrust) HandleNotationCertificateUpload(response http.ResponseWriter, request *http.Request) {
var truststoreType string
if !zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreName"}) {
response.WriteHeader(http.StatusBadRequest)
return
}
if zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreType"}) {
truststoreType = request.URL.Query().Get("truststoreType")
} else {
truststoreType = "ca" // default value of "truststoreType" query param
}
truststoreName := request.URL.Query().Get("truststoreName")
if truststoreType == "" || truststoreName == "" {
response.WriteHeader(http.StatusBadRequest)
return
}
body, err := io.ReadAll(request.Body)
if err != nil {
trust.Log.Error().Err(err).Msg("image trust: couldn't read notation certificate body")
response.WriteHeader(http.StatusInternalServerError)
return
}
err = signatures.UploadCertificate(body, truststoreType, truststoreName)
if err != nil {
if errors.Is(err, zerr.ErrInvalidTruststoreType) ||
errors.Is(err, zerr.ErrInvalidTruststoreName) ||
errors.Is(err, zerr.ErrInvalidCertificateContent) {
response.WriteHeader(http.StatusBadRequest)
} else {
trust.Log.Error().Err(err).Msg("image trust: failed to save notation certificate")
response.WriteHeader(http.StatusInternalServerError)
}
return
}
response.WriteHeader(http.StatusOK)
}
func EnableImageTrustVerification(conf *config.Config, taskScheduler *scheduler.Scheduler,
metaDB mTypes.MetaDB, log log.Logger,
) {
if !conf.IsImageTrustEnabled() {
return
}
generator := signatures.NewTaskGenerator(metaDB, log)
numberOfHours := 2
interval := time.Duration(numberOfHours) * time.Minute
taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
}

View file

@ -0,0 +1,29 @@
//go:build !imagetrust
// +build !imagetrust
package extensions
import (
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
func IsBuiltWithImageTrustExtension() bool {
return false
}
func SetupImageTrustRoutes(config *config.Config, router *mux.Router, log log.Logger) {
log.Warn().Msg("skipping setting up image trust routes because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
func EnableImageTrustVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
metaDB mTypes.MetaDB, log log.Logger,
) {
log.Warn().Msg("skipping adding to the scheduler a generator for updating signatures validity because " +
"given binary doesn't include this feature, please build a binary that does so")
}

View file

@ -1,4 +1,4 @@
//go:build !mgmt
//go:build !imagetrust
package extensions_test
@ -14,8 +14,8 @@ import (
"zotregistry.io/zot/pkg/test"
)
func TestMgmtExtension(t *testing.T) {
Convey("periodic signature verification is skipped when binary doesn't include mgmt", t, func() {
func TestImageTrustExtension(t *testing.T) {
Convey("periodic signature verification is skipped when binary doesn't include imagetrust", t, func() {
conf := config.New()
port := test.GetFreePort()
@ -30,11 +30,10 @@ func TestMgmtExtension(t *testing.T) {
conf.Storage.RootDirectory = globalDir
conf.Storage.Commit = true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
conf.Extensions.Trust.Notation = defaultValue
conf.Log.Level = "warn"
conf.Log.Output = logFile.Name()

View file

@ -0,0 +1,958 @@
//go:build search && imagetrust
// +build search,imagetrust
package extensions_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"testing"
"time"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/test"
)
type errReader int
func (errReader) Read(p []byte) (int, error) {
return 0, fmt.Errorf("test error") //nolint:goerr113
}
func TestSignatureHandlers(t *testing.T) {
conf := config.New()
log := log.NewLogger("debug", "")
trust := extensions.ImageTrust{
Conf: conf,
Log: log,
}
Convey("Test error handling when Cosign handler reads the request body", t, func() {
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, "baseURL", errReader(0))
response := httptest.NewRecorder()
trust.HandleCosignPublicKeyUpload(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
Convey("Test error handling when Notation handler reads the request body", t, func() {
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, "baseURL", errReader(0))
query := request.URL.Query()
query.Add("truststoreName", "someName")
request.URL.RawQuery = query.Encode()
response := httptest.NewRecorder()
trust.HandleNotationCertificateUpload(response, request)
resp := response.Result()
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
})
}
func TestSignaturesAllowedMethodsHeader(t *testing.T) {
defaultVal := true
Convey("Test http options response", t, func() {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultVal
conf.Extensions.Trust.Cosign = defaultVal
conf.Extensions.Trust.Notation = defaultVal
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctrlManager := test.NewControllerManager(ctlr)
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullCosign)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
resp, _ = resty.R().Options(baseURL + constants.FullNotation)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}
func TestSignatureUploadAndVerification(t *testing.T) {
repo := "repo"
tag := "0.0.1"
certName := "test"
defaultValue := true
imageQuery := `
{
Image(image:"%s:%s"){
RepoName Tag Digest IsSigned
Manifests {
Digest
SignatureInfo { Tool IsTrusted Author }
}
SignatureInfo { Tool IsTrusted Author }
}
}`
Convey("Verify cosign public key upload without search or notation being enabled", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := test.CreateRandomImage()
err = test.WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// generate a keypair
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// upload the public key
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
options.KeyOpts{KeyRef: path.Join(keyDir, "cosign.key"), PassFunc: generate.GetPass},
options.SignOptions{
Registry: options.RegistryOptions{AllowInsecure: true},
AnnotationOptions: options.AnnotationOptions{Annotations: []string{fmt.Sprintf("tag=%s", tag)}},
Upload: true,
},
[]string{fmt.Sprintf("localhost:%s/%s@%s", port, repo, image.DigestStr())})
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
Convey("Verify notation certificate upload without search or cosign being enabled", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Notation = defaultValue
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := test.CreateRandomImage()
err = test.WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
rootDir := t.TempDir()
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(rootDir)
// generate a keypair
err = test.GenerateNotationCerts(rootDir, certName)
So(err, ShouldBeNil)
// upload the certificate
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", fmt.Sprintf("%s.crt", certName)))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", certName).
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag))
err = test.SignWithNotation(certName, imageURL, rootDir)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", "").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", "test").
SetQueryParam("truststoreType", "signatureAuthority").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
Convey("Verify uploading notation certificates", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Notation = defaultValue
baseURL := test.GetBaseURL(port)
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, constants.FullSearchPrefix)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := test.CreateRandomImage()
err = test.WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
strQuery := fmt.Sprintf(imageQuery, repo, tag)
gqlTargetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
// Verify the image is initially shown as not being signed
resp, err := resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse := zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.IsSigned, ShouldEqual, false)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.SignatureInfo), ShouldEqual, 0)
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 0)
rootDir := t.TempDir()
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(rootDir)
// generate a keypair
err = test.GenerateNotationCerts(rootDir, certName)
So(err, ShouldBeNil)
// upload the certificate
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", fmt.Sprintf("%s.crt", certName)))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
client := resty.New()
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", certName).
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
imageURL := fmt.Sprintf("localhost:%s/%s", port, fmt.Sprintf("%s:%s", repo, tag))
err = test.SignWithNotation(certName, imageURL, rootDir)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// verify the image is shown as signed and trusted
resp, err = resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse = zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary = imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
t.Log(imgSummary.SignatureInfo)
So(imgSummary.IsSigned, ShouldEqual, true)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.SignatureInfo), ShouldEqual, 1)
So(imgSummary.SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.SignatureInfo[0].Tool, ShouldEqual, "notation")
So(imgSummary.SignatureInfo[0].Author,
ShouldEqual, "CN=cert,O=Notary,L=Seattle,ST=WA,C=US")
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 1)
t.Log(imgSummary.Manifests[0].SignatureInfo)
So(imgSummary.Manifests[0].SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.Manifests[0].SignatureInfo[0].Tool, ShouldEqual, "notation")
So(imgSummary.Manifests[0].SignatureInfo[0].Author,
ShouldEqual, "CN=cert,O=Notary,L=Seattle,ST=WA,C=US")
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", "").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", "test").
SetQueryParam("truststoreType", "signatureAuthority").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
Convey("Verify uploading cosign public keys", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, constants.FullSearchPrefix)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
image := test.CreateRandomImage()
err = test.WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
strQuery := fmt.Sprintf(imageQuery, repo, tag)
gqlTargetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
// Verify the image is initially shown as not being signed
resp, err := resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse := zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.IsSigned, ShouldEqual, false)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.SignatureInfo), ShouldEqual, 0)
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 0)
// generate a keypair
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// upload the public key
client := resty.New()
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// sign the image
err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
options.KeyOpts{KeyRef: path.Join(keyDir, "cosign.key"), PassFunc: generate.GetPass},
options.SignOptions{
Registry: options.RegistryOptions{AllowInsecure: true},
AnnotationOptions: options.AnnotationOptions{Annotations: []string{fmt.Sprintf("tag=%s", tag)}},
Upload: true,
},
[]string{fmt.Sprintf("localhost:%s/%s@%s", port, repo, image.DigestStr())})
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// verify the image is shown as signed and trusted
resp, err = resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse = zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary = imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
t.Log(imgSummary.SignatureInfo)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(imgSummary.IsSigned, ShouldEqual, true)
So(len(imgSummary.SignatureInfo), ShouldEqual, 1)
So(imgSummary.SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.SignatureInfo[0].Author, ShouldEqual, string(publicKeyContent))
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 1)
t.Log(imgSummary.Manifests[0].SignatureInfo)
So(imgSummary.Manifests[0].SignatureInfo[0].IsTrusted, ShouldEqual, true)
So(imgSummary.Manifests[0].SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.Manifests[0].SignatureInfo[0].Author, ShouldEqual, string(publicKeyContent))
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().Get(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = client.R().Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
Convey("Verify uploading cosign public keys with auth configured", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
testCreds := test.GetCredString("admin", "admin") + "\n" + test.GetCredString("test", "test")
htpasswdPath := test.MakeHtpasswdFileFromString(testCreds)
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.AccessControl = &config.AccessControlConfig{
AdminPolicy: config.Policy{
Users: []string{"admin"},
Actions: []string{},
},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// generate a keypair
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// fail to upload the public key without credentials
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// fail to upload the public key with bad credentials
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// upload the public key using credentials and non-admin user
resp, err = client.R().SetBasicAuth("test", "test").SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
// upload the public key using credentials and admin user
resp, err = client.R().SetBasicAuth("admin", "admin").SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
Convey("Verify signatures are read from the disk and updated in the DB when zot starts", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, constants.FullSearchPrefix)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
defer os.Remove(logFile.Name()) // cleanup
So(err, ShouldBeNil)
logger := log.NewLogger("debug", logFile.Name())
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
// Write image
image := test.CreateRandomImage()
err = test.WriteImageToFileSystem(image, repo, tag, storeController)
So(err, ShouldBeNil)
// Write signature
signature := test.CreateImageWith().RandomLayers(1, 2).RandomConfig().Build()
So(err, ShouldBeNil)
ref, err := test.GetCosignSignatureTagForManifest(image.Manifest)
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(signature, repo, ref, storeController)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
strQuery := fmt.Sprintf(imageQuery, repo, tag)
gqlTargetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery))
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up image trust routes", time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// verify the image is shown as signed and trusted
resp, err := resty.R().Get(gqlTargetURL)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.Body(), ShouldNotBeNil)
imgSummaryResponse := zcommon.ImageSummaryResult{}
err = json.Unmarshal(resp.Body(), &imgSummaryResponse)
So(err, ShouldBeNil)
So(imgSummaryResponse, ShouldNotBeNil)
So(imgSummaryResponse.ImageSummary, ShouldNotBeNil)
imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary
So(imgSummary.RepoName, ShouldContainSubstring, repo)
So(imgSummary.Tag, ShouldContainSubstring, tag)
So(imgSummary.Digest, ShouldContainSubstring, image.Digest().Encoded())
So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, image.Digest().Encoded())
t.Log(imgSummary.SignatureInfo)
So(imgSummary.SignatureInfo, ShouldNotBeNil)
So(imgSummary.IsSigned, ShouldEqual, true)
So(len(imgSummary.SignatureInfo), ShouldEqual, 1)
So(imgSummary.SignatureInfo[0].IsTrusted, ShouldEqual, false)
So(imgSummary.SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.SignatureInfo[0].Author, ShouldEqual, "")
So(imgSummary.Manifests[0].SignatureInfo, ShouldNotBeNil)
So(len(imgSummary.Manifests[0].SignatureInfo), ShouldEqual, 1)
t.Log(imgSummary.Manifests[0].SignatureInfo)
So(imgSummary.Manifests[0].SignatureInfo[0].IsTrusted, ShouldEqual, false)
So(imgSummary.Manifests[0].SignatureInfo[0].Tool, ShouldEqual, "cosign")
So(imgSummary.Manifests[0].SignatureInfo[0].Author, ShouldEqual, "")
})
Convey("Verify failures when saving uploaded certificates and public keys", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
conf.Extensions.Trust.Enable = &defaultValue
conf.Extensions.Trust.Notation = defaultValue
conf.Extensions.Trust.Cosign = defaultValue
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
rootDir := t.TempDir()
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(rootDir)
// generate Notation cert
err := test.GenerateNotationCerts(rootDir, "test")
So(err, ShouldBeNil)
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "test.crt"))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
// generate Cosign keys
keyDir := t.TempDir()
cwd, err := os.Getwd()
So(err, ShouldBeNil)
_ = os.Chdir(keyDir)
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
So(err, ShouldBeNil)
_ = os.Chdir(cwd)
publicKeyContent, err := os.ReadFile(path.Join(keyDir, "cosign.pub"))
So(err, ShouldBeNil)
So(publicKeyContent, ShouldNotBeNil)
// Make sure the write to disk fails
So(os.Chmod(globalDir, 0o000), ShouldBeNil)
defer func() {
So(os.Chmod(globalDir, 0o755), ShouldBeNil)
}()
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("truststoreName", "test").
SetBody(certificateContent).Post(baseURL + constants.FullNotation)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetBody(publicKeyContent).Post(baseURL + constants.FullCosign)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
}

View file

@ -9,7 +9,6 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir string) {
@ -26,7 +25,7 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
}
}
func SetupMetricsRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
func SetupMetricsRoutes(config *config.Config, router *mux.Router,
authFunc mux.MiddlewareFunc, log log.Logger,
) {
log.Info().Msg("setting up metrics routes")

View file

@ -8,7 +8,6 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// EnableMetricsExtension ...
@ -19,7 +18,7 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
// SetupMetricsRoutes ...
func SetupMetricsRoutes(conf *config.Config, router *mux.Router,
storeController storage.StoreController, authFunc mux.MiddlewareFunc, log log.Logger,
authFunc mux.MiddlewareFunc, log log.Logger,
) {
log.Warn().Msg("skipping setting up metrics routes because given zot binary doesn't include this feature," +
"please build a binary that does so")

View file

@ -4,27 +4,15 @@
package extensions
import (
"context"
"encoding/json"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/signatures"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
const (
ConfigResource = "config"
SignaturesResource = "signatures"
)
type HTPasswd struct {
@ -90,246 +78,51 @@ func (auth Auth) MarshalJSON() ([]byte, error) {
return json.Marshal((localAuth)(auth))
}
type mgmt struct {
config *config.Config
log log.Logger
}
func SetupMgmtRoutes(conf *config.Config, router *mux.Router, log log.Logger) {
if !conf.IsMgmtEnabled() {
log.Info().Msg("skip enabling the mgmt route as the config prerequisites are not met")
func (mgmt *mgmt) handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var resource string
if zcommon.QueryHasParams(r.URL.Query(), []string{"resource"}) {
resource = r.URL.Query().Get("resource")
} else {
resource = ConfigResource // default value of "resource" query param
}
switch resource {
case ConfigResource:
if r.Method == http.MethodGet {
mgmt.HandleGetConfig(w, r)
} else {
w.WriteHeader(http.StatusBadRequest)
}
return
case SignaturesResource:
if r.Method == http.MethodPost {
HandleCertificatesAndPublicKeysUploads(w, r) //nolint: contextcheck
} else {
w.WriteHeader(http.StatusBadRequest)
}
return
default:
w.WriteHeader(http.StatusBadRequest)
return
}
})
}
func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger) {
if config.Extensions.Mgmt != nil && *config.Extensions.Mgmt.Enable {
log.Info().Msg("setting up mgmt routes")
mgmt := mgmt{config: config, log: log}
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
mgmtRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders())
mgmtRouter.Methods(allowedMethods...).Handler(mgmt.handler())
return
}
log.Info().Msg("setting up mgmt routes")
mgmt := Mgmt{Conf: conf, Log: log}
// The endpoint for reading configuration should be available to all users
allowedMethods := zcommon.AllowedMethods(http.MethodGet)
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
mgmtRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders())
mgmtRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
mgmtRouter.Methods(allowedMethods...).HandlerFunc(mgmt.HandleGetConfig)
log.Info().Msg("finished setting up mgmt routes")
}
type Mgmt struct {
Conf *config.Config
Log log.Logger
}
// mgmtHandler godoc
// @Summary Get current server configuration
// @Description Get current server configuration
// @Router /v2/_zot/ext/mgmt [get]
// @Router /v2/_zot/ext/mgmt [get]
// @Accept json
// @Produce json
// @Param resource query string false "specify resource" Enums(config)
// @Success 200 {object} extensions.StrippedConfig
// @Failure 500 {string} string "internal server error".
func (mgmt *mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
sanitizedConfig := mgmt.config.Sanitize()
// @Param resource query string false "specify resource" Enums(config)
// @Success 200 {object} extensions.StrippedConfig
// @Failure 500 {string} string "internal server error".
func (mgmt *Mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
sanitizedConfig := mgmt.Conf.Sanitize()
buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
if err != nil {
mgmt.log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
mgmt.Log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
w.WriteHeader(http.StatusInternalServerError)
}
_, _ = w.Write(buf)
}
// mgmtHandler godoc
// @Summary Upload certificates and public keys for verifying signatures
// @Description Upload certificates and public keys for verifying signatures
// @Router /v2/_zot/ext/mgmt [post]
// @Accept octet-stream
// @Produce json
// @Param resource query string true "specify resource" Enums(signatures)
// @Param tool query string true "specify signing tool" Enums(cosign, notation)
// @Param truststoreType query string false "truststore type"
// @Param truststoreName query string false "truststore name"
// @Param requestBody body string true "Public key or Certificate content"
// @Success 200 {string} string "ok"
// @Failure 400 {string} string "bad request".
// @Failure 500 {string} string "internal server error".
func HandleCertificatesAndPublicKeysUploads(response http.ResponseWriter, request *http.Request) {
if !zcommon.QueryHasParams(request.URL.Query(), []string{"tool"}) {
response.WriteHeader(http.StatusBadRequest)
return
}
body, err := io.ReadAll(request.Body)
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
tool := request.URL.Query().Get("tool")
switch tool {
case signatures.CosignSignature:
err := signatures.UploadPublicKey(body)
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
case signatures.NotationSignature:
var truststoreType string
if !zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreName"}) {
response.WriteHeader(http.StatusBadRequest)
return
}
if zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreType"}) {
truststoreType = request.URL.Query().Get("truststoreType")
} else {
truststoreType = "ca" // default value of "truststoreType" query param
}
truststoreName := request.URL.Query().Get("truststoreName")
if truststoreType == "" || truststoreName == "" {
response.WriteHeader(http.StatusBadRequest)
return
}
err = signatures.UploadCertificate(body, truststoreType, truststoreName)
if err != nil {
response.WriteHeader(http.StatusInternalServerError)
return
}
default:
response.WriteHeader(http.StatusBadRequest)
return
}
response.WriteHeader(http.StatusOK)
}
func EnablePeriodicSignaturesVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
metaDB mTypes.MetaDB, log log.Logger,
) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
ctx := context.Background()
repos, err := metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool {
return true
})
if err != nil {
return
}
generator := &taskGeneratorSigValidity{
repos: repos,
metaDB: metaDB,
repoIndex: -1,
log: log,
}
numberOfHours := 2
interval := time.Duration(numberOfHours) * time.Minute
taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
}
}
type taskGeneratorSigValidity struct {
repos []mTypes.RepoMetadata
metaDB mTypes.MetaDB
repoIndex int
done bool
log log.Logger
}
func (gen *taskGeneratorSigValidity) Next() (scheduler.Task, error) {
gen.repoIndex++
if gen.repoIndex >= len(gen.repos) {
gen.done = true
return nil, nil
}
return NewValidityTask(gen.metaDB, gen.repos[gen.repoIndex], gen.log), nil
}
func (gen *taskGeneratorSigValidity) IsDone() bool {
return gen.done
}
func (gen *taskGeneratorSigValidity) Reset() {
gen.done = false
gen.repoIndex = -1
ctx := context.Background()
repos, err := gen.metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool { return true })
if err != nil {
return
}
gen.repos = repos
}
type validityTask struct {
metaDB mTypes.MetaDB
repo mTypes.RepoMetadata
log log.Logger
}
func NewValidityTask(metaDB mTypes.MetaDB, repo mTypes.RepoMetadata, log log.Logger) *validityTask {
return &validityTask{metaDB, repo, log}
}
func (validityT *validityTask) DoWork() error {
validityT.log.Info().Msg("updating signatures validity")
for signedManifest, sigs := range validityT.repo.Signatures {
if len(sigs[signatures.CosignSignature]) != 0 || len(sigs[signatures.NotationSignature]) != 0 {
err := validityT.metaDB.UpdateSignaturesValidity(validityT.repo.Name, digest.Digest(signedManifest))
if err != nil {
validityT.log.Info().Msg("error while verifying signatures")
return err
}
}
}
validityT.log.Info().Msg("verifying signatures successfully completed")
return nil
}

View file

@ -8,8 +8,6 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
func IsBuiltWithMGMTExtension() bool {
@ -20,10 +18,3 @@ func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger)
log.Warn().Msg("skipping setting up mgmt routes because given zot binary doesn't include this feature," +
"please build a binary that does so")
}
func EnablePeriodicSignaturesVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
metaDB mTypes.MetaDB, log log.Logger,
) {
log.Warn().Msg("skipping adding to the scheduler a generator for updating signatures validity because " +
"given binary doesn't include this feature, please build a binary that does so")
}

View file

@ -156,20 +156,27 @@ func (trivyT *trivyTask) DoWork() error {
return nil
}
func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
) {
if !conf.IsSearchEnabled() {
log.Info().Msg("skip enabling the search route as the config prerequisites are not met")
return
}
log.Info().Msg("setting up search routes")
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
extRouter := router.PathPrefix(constants.ExtSearch).Subrouter()
extRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
extRouter.Methods(allowedMethods...).
Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig)))
}
extRouter := router.PathPrefix(constants.ExtSearchPrefix).Subrouter()
extRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
extRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
extRouter.Methods(allowedMethods...).
Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig)))
log.Info().Msg("finished setting up search routes")
}

View file

@ -12,8 +12,8 @@ import (
"github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
// content is our static web server content.
@ -57,19 +57,38 @@ func addUISecurityHeaders(h http.Handler) http.HandlerFunc { //nolint:varnamelen
}
}
func SetupUIRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
func SetupUIRoutes(conf *config.Config, router *mux.Router,
log log.Logger,
) {
if config.Extensions.UI != nil {
fsub, _ := fs.Sub(content, "build")
uih := uiHandler{log: log}
if !conf.IsUIEnabled() {
log.Info().Msg("skip enabling the ui route as the config prerequisites are not met")
router.PathPrefix("/login").Handler(addUISecurityHeaders(uih))
router.PathPrefix("/home").Handler(addUISecurityHeaders(uih))
router.PathPrefix("/explore").Handler(addUISecurityHeaders(uih))
router.PathPrefix("/image").Handler(addUISecurityHeaders(uih))
router.PathPrefix("/").Handler(addUISecurityHeaders(http.FileServer(http.FS(fsub))))
log.Info().Msg("setting up ui routes")
return
}
log.Info().Msg("setting up ui routes")
fsub, _ := fs.Sub(content, "build")
uih := uiHandler{log: log}
// See https://go-review.googlesource.com/c/go/+/482635/2/src/net/http/fs.go
// See https://github.com/golang/go/issues/59469
// In go 1.20.4 they decided to allow any method in the FileServer handler.
// In order to be consistent with the status codes returned when the UI is disabled
// we need to be explicit about the methods we allow on UI routes.
// If we don't add this, all unmatched http methods on any urls would match the UI routes.
allowedMethods := zcommon.AllowedMethods(http.MethodGet)
router.PathPrefix("/login").Methods(allowedMethods...).
Handler(addUISecurityHeaders(uih))
router.PathPrefix("/home").Methods(allowedMethods...).
Handler(addUISecurityHeaders(uih))
router.PathPrefix("/explore").Methods(allowedMethods...).
Handler(addUISecurityHeaders(uih))
router.PathPrefix("/image").Methods(allowedMethods...).
Handler(addUISecurityHeaders(uih))
router.PathPrefix("/").Methods(allowedMethods...).
Handler(addUISecurityHeaders(http.FileServer(http.FS(fsub))))
log.Info().Msg("finished setting up ui routes")
}

View file

@ -8,10 +8,9 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
)
func SetupUIRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
func SetupUIRoutes(conf *config.Config, router *mux.Router,
log log.Logger,
) {
log.Warn().Msg("skipping setting up ui routes because given zot binary doesn't include this feature," +

View file

@ -15,7 +15,6 @@ import (
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/storage"
)
const (
@ -27,37 +26,43 @@ func IsBuiltWithUserPrefsExtension() bool {
return true
}
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
func SetupUserPreferencesRoutes(conf *config.Config, router *mux.Router,
metaDB mTypes.MetaDB, log log.Logger,
) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
log.Info().Msg("setting up user preferences routes")
if !conf.AreUserPrefsEnabled() {
log.Info().Msg("skip enabling the user preferences route as the config prerequisites are not met")
allowedMethods := zcommon.AllowedMethods(http.MethodPut)
userprefsRouter := router.PathPrefix(constants.ExtUserPreferences).Subrouter()
userprefsRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
userprefsRouter.Use(zcommon.AddExtensionSecurityHeaders())
userprefsRouter.HandleFunc("", HandleUserPrefs(metaDB, log)).Methods(allowedMethods...)
return
}
log.Info().Msg("setting up user preferences routes")
allowedMethods := zcommon.AllowedMethods(http.MethodPut)
userPrefsRouter := router.PathPrefix(constants.ExtUserPrefs).Subrouter()
userPrefsRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
userPrefsRouter.Use(zcommon.AddExtensionSecurityHeaders())
userPrefsRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
userPrefsRouter.Methods(allowedMethods...).Handler(HandleUserPrefs(metaDB, log))
log.Info().Msg("finished setting up user preferences routes")
}
// ListTags godoc
// Repo preferences godoc
// @Summary Add bookmarks/stars info
// @Description Add bookmarks/stars info
// @Router /v2/_zot/ext/userprefs [put]
// @Router /v2/_zot/ext/userprefs [put]
// @Accept json
// @Produce json
// @Param action query string true "specify action" Enums(toggleBookmark, toggleStar)
// @Param repo query string true "repository name"
// @Success 200 {string} string "ok"
// @Failure 404 {string} string "not found"
// @Failure 403 {string} string "forbidden"
// @Failure 500 {string} string "internal server error"
// @Failure 400 {string} string "bad request".
func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) func(w http.ResponseWriter, r *http.Request) {
return func(rsp http.ResponseWriter, req *http.Request) {
// @Param action query string true "specify action" Enums(toggleBookmark, toggleStar)
// @Param repo query string true "repository name"
// @Success 200 {string} string "ok"
// @Failure 404 {string} string "not found"
// @Failure 403 {string} string "forbidden"
// @Failure 500 {string} string "internal server error"
// @Failure 400 {string} string "bad request".
func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) http.Handler {
return http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) {
if !zcommon.QueryHasParams(req.URL.Query(), []string{"action"}) {
rsp.WriteHeader(http.StatusBadRequest)
@ -80,7 +85,7 @@ func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) func(w http.ResponseW
return
}
}
})
}
func PutStar(rsp http.ResponseWriter, req *http.Request, metaDB mTypes.MetaDB, log log.Logger) {

View file

@ -9,15 +9,14 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/storage"
)
func IsBuiltWithUserPrefsExtension() bool {
return false
}
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router,
metaDB mTypes.MetaDB, log log.Logger,
) {
log.Warn().Msg("userprefs extension is disabled because given zot binary doesn't" +
"include this feature please build a binary that does so")

View file

@ -36,11 +36,13 @@ func TestAllowedMethodsHeaderUserPrefs(t *testing.T) {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
@ -51,7 +53,7 @@ func TestAllowedMethodsHeaderUserPrefs(t *testing.T) {
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullUserPreferencesPrefix)
resp, _ := resty.R().Options(baseURL + constants.FullUserPrefs)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "PUT,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)

View file

@ -1,5 +1,5 @@
//go:build sync || metrics || mgmt || apikey
// +build sync metrics mgmt apikey
//go:build sync && metrics && mgmt && userprefs && search
// +build sync,metrics,mgmt,userprefs,search
package extensions_test
@ -9,7 +9,6 @@ import (
"net/http"
"net/url"
"os"
"path"
"testing"
"time"
@ -22,10 +21,6 @@ import (
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config"
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/test"
)
@ -125,6 +120,7 @@ func TestMgmtExtension(t *testing.T) {
if err != nil {
panic(err)
}
mgmtReadyTimeout := 5 * time.Second
defaultValue := true
@ -142,16 +138,16 @@ func TestMgmtExtension(t *testing.T) {
mockOIDCConfig := mockOIDCServer.Config()
Convey("Verify mgmt route enabled with htpasswd", t, func() {
Convey("Verify mgmt auth info route enabled with htpasswd", t, func() {
htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -168,19 +164,31 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
So(err, ShouldBeNil)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
Convey("unsupported http method call", func() {
// without credentials
resp, err := resty.R().Patch(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
})
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Patch(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
// without credentials
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -193,7 +201,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
// with credentials
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -206,12 +214,12 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
// with wrong credentials
resp, err = resty.R().SetBasicAuth("test", "wrong").Get(baseURL + constants.FullMgmtPrefix)
resp, err = resty.R().SetBasicAuth("test", "wrong").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
})
Convey("Verify mgmt route enabled with ldap", t, func() {
Convey("Verify mgmt auth info route enabled with ldap", t, func() {
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
BindDN: "binddn",
BaseDN: "basedn",
@ -219,11 +227,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -240,12 +248,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -258,7 +279,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
})
Convey("Verify mgmt route enabled with htpasswd + ldap", t, func() {
Convey("Verify mgmt auth info route enabled with htpasswd + ldap", t, func() {
htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
@ -268,11 +289,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -289,12 +310,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -307,7 +341,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
// with credentials
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -320,7 +354,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
})
Convey("Verify mgmt route enabled with htpasswd + ldap + bearer", t, func() {
Convey("Verify mgmt auth info route enabled with htpasswd + ldap + bearer", t, func() {
htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
@ -335,11 +369,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -352,12 +386,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -372,7 +419,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
// with credentials
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -387,7 +434,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
})
Convey("Verify mgmt route enabled with ldap + bearer", t, func() {
Convey("Verify mgmt auth info route enabled with ldap + bearer", t, func() {
conf.HTTP.Auth.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
BindDN: "binddn",
@ -401,11 +448,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -422,12 +469,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -442,7 +502,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
})
Convey("Verify mgmt route enabled with bearer", t, func() {
Convey("Verify mgmt auth info route enabled with bearer", t, func() {
conf.HTTP.Auth.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = &config.BearerConfig{
@ -451,11 +511,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -468,12 +528,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -487,7 +560,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
})
Convey("Verify mgmt route enabled with openID", t, func() {
Convey("Verify mgmt auth info route enabled with openID", t, func() {
conf.HTTP.Auth.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = nil
@ -504,11 +577,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -521,12 +594,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -541,7 +627,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.OpenID.Providers, ShouldNotBeEmpty)
})
Convey("Verify mgmt route enabled with empty openID provider list", t, func() {
Convey("Verify mgmt auth info route enabled with empty openID provider list", t, func() {
htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
@ -555,11 +641,11 @@ func TestMgmtExtension(t *testing.T) {
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -572,12 +658,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
data, _ := os.ReadFile(logFile.Name())
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -591,7 +690,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.OpenID, ShouldBeNil)
})
Convey("Verify mgmt route enabled without any auth", t, func() {
Convey("Verify mgmt auth info route enabled without any auth", t, func() {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
@ -605,11 +704,11 @@ func TestMgmtExtension(t *testing.T) {
conf.Commit = "v1.0.0"
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
@ -622,7 +721,7 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -634,158 +733,22 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
data, _ := os.ReadFile(logFile.Name())
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
})
Convey("Verify mgmt route enabled for uploading certificates and public keys", t, func() {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defaultValue := true
conf.Commit = "v1.0.0"
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
log.NewLogger("debug", logFile.Name()), monitoring.NewMetricsServer(false,
log.NewLogger("debug", logFile.Name())), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
config, layers, manifest, err := test.GetRandomImageComponents(10) //nolint:staticcheck
So(err, ShouldBeNil)
err = test.WriteImageToFileSystem(
test.Image{
Manifest: manifest,
Layers: layers,
Config: config,
}, "repo", "0.0.1", storeController,
)
So(err, ShouldBeNil)
sigConfig, sigLayers, sigManifest, err := test.GetRandomImageComponents(10) //nolint:staticcheck
So(err, ShouldBeNil)
ref, _ := test.GetCosignSignatureTagForManifest(manifest)
err = test.WriteImageToFileSystem(
test.Image{
Manifest: sigManifest,
Layers: sigLayers,
Config: sigConfig,
}, "repo", ref, storeController,
)
So(err, ShouldBeNil)
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
rootDir := t.TempDir()
test.NotationPathLock.Lock()
defer test.NotationPathLock.Unlock()
test.LoadNotationPath(rootDir)
// generate a keypair
err = test.GenerateNotationCerts(rootDir, "test")
So(err, ShouldBeNil)
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "test.crt"))
So(err, ShouldBeNil)
So(certificateContent, ShouldNotBeNil)
client := resty.New()
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "test").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "test").
SetQueryParam("truststoreType", "signatureAuthority").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "invalidTool").
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
SetQueryParam("resource", "signatures").SetQueryParam("tool", "cosign").
SetBody([]byte("wrong content")).Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
resp, err = client.R().SetQueryParam("resource", "signatures").SetQueryParam("tool", "cosign").
Get(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetQueryParam("resource", "signatures").Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetQueryParam("resource", "config").Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
resp, err = client.R().SetQueryParam("resource", "invalid").Post(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up mgmt routes", time.Second)
So(err, ShouldBeNil)
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
time.Second)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
})
}
@ -816,11 +779,11 @@ func TestMgmtWithBearer(t *testing.T) {
defaultValue := true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Storage.RootDirectory = t.TempDir()
@ -909,7 +872,7 @@ func TestMgmtWithBearer(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// test mgmt route
resp, err = resty.R().Get(baseURL + constants.FullMgmtPrefix)
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -923,7 +886,7 @@ func TestMgmtWithBearer(t *testing.T) {
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmtPrefix)
resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -946,11 +909,13 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{
Mgmt: &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
@ -961,38 +926,9 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullMgmtPrefix)
resp, _ := resty.R().Options(baseURL + constants.FullMgmt)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}
func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
defaultVal := true
Convey("Test http options response", t, func() {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{
APIKey: &extconf.APIKeyConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctrlManager := test.NewControllerManager(ctlr)
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullAPIKeyPrefix)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,DELETE,OPTIONS")
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}

View file

@ -5,6 +5,9 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
func GetExtensions(config *config.Config) distext.ExtensionList {
@ -13,18 +16,24 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
endpoints := []string{}
extensions := []distext.Extension{}
if config.Extensions != nil && config.Extensions.Search != nil {
if IsBuiltWithSearchExtension() {
endpoints = append(endpoints, constants.FullSearchPrefix)
}
if IsBuiltWithUserPrefsExtension() {
endpoints = append(endpoints, constants.FullUserPreferencesPrefix)
}
if config.IsNotationEnabled() && IsBuiltWithImageTrustExtension() {
endpoints = append(endpoints, constants.FullNotation)
}
if IsBuiltWithMGMTExtension() && config.Extensions != nil && config.Extensions.Mgmt != nil {
endpoints = append(endpoints, constants.FullMgmtPrefix)
if config.IsCosignEnabled() && IsBuiltWithImageTrustExtension() {
endpoints = append(endpoints, constants.FullCosign)
}
if config.IsSearchEnabled() && IsBuiltWithSearchExtension() {
endpoints = append(endpoints, constants.FullSearchPrefix)
}
if config.AreUserPrefsEnabled() && IsBuiltWithUserPrefsExtension() {
endpoints = append(endpoints, constants.FullUserPrefs)
}
if config.IsMgmtEnabled() && IsBuiltWithMGMTExtension() {
endpoints = append(endpoints, constants.FullMgmt)
}
if len(endpoints) > 0 {
@ -40,3 +49,9 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
return extensionList
}
func EnableScheduledTasks(conf *config.Config, taskScheduler *scheduler.Scheduler,
metaDB mTypes.MetaDB, log log.Logger,
) {
EnableImageTrustVerification(conf, taskScheduler, metaDB, log)
}

View file

@ -28,18 +28,12 @@ func TestGetExensionsDisabled(t *testing.T) {
defaultVal := true
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
mgmtConfg := &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
Mgmt: mgmtConfg,
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)

View file

@ -83,9 +83,12 @@ func TestUserData(t *testing.T) {
Actions: []string{"read", "create", "update"},
},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
ctlr := api.NewController(conf)
@ -134,7 +137,7 @@ func TestUserData(t *testing.T) {
}
}`
userprefsBaseURL := baseURL + constants.FullUserPreferencesPrefix
userprefsBaseURL := baseURL + constants.FullUserPrefs
Convey("Flip starred repo authorized", func(c C) {
clientHTTP := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
@ -499,9 +502,12 @@ func TestChangingRepoState(t *testing.T) {
},
},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
gqlStarredRepos := `
{
@ -563,7 +569,7 @@ func TestChangingRepoState(t *testing.T) {
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
anonynousClient := resty.R()
userprefsBaseURL := baseURL + constants.FullUserPreferencesPrefix
userprefsBaseURL := baseURL + constants.FullUserPrefs
Convey("PutStars", t, func() {
resp, err := simpleUserClient.Put(userprefsBaseURL + PutRepoStarURL(accesibleRepo))
@ -647,9 +653,12 @@ func TestGlobalSearchWithUserPrefFiltering(t *testing.T) {
}
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
ctlr := api.NewController(conf)
@ -657,7 +666,7 @@ func TestGlobalSearchWithUserPrefFiltering(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
preferencesBaseURL := baseURL + constants.FullUserPreferencesPrefix
preferencesBaseURL := baseURL + constants.FullUserPrefs
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
// ------ Add simple repo
@ -840,9 +849,12 @@ func TestExpandedRepoInfoWithUserPrefs(t *testing.T) {
}
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
ctlr := api.NewController(conf)
@ -850,7 +862,7 @@ func TestExpandedRepoInfoWithUserPrefs(t *testing.T) {
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
preferencesBaseURL := baseURL + constants.FullUserPreferencesPrefix
preferencesBaseURL := baseURL + constants.FullUserPrefs
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
// ------ Add sbrepo and star/bookmark it

View file

@ -5,6 +5,7 @@ import (
"context"
"crypto"
"encoding/base64"
"fmt"
"io"
"os"
"path"
@ -136,7 +137,7 @@ func UploadPublicKey(publicKeyContent []byte) error {
func validatePublicKey(publicKeyContent []byte) (bool, error) {
_, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyContent)
if err != nil {
return false, err
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidPublicKeyContent, err)
}
return true, nil

View file

@ -302,7 +302,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
// data may be in DER format
derCerts, err := x509.ParseCertificates(certificateContent)
if err != nil {
return false, err
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, derCerts...)
@ -311,7 +311,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return false, err
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
@ -319,7 +319,8 @@ func validateCertificate(certificateContent []byte) (bool, error) {
}
if len(certs) == 0 {
return false, zerr.ErrInvalidCertificateContent
return false, fmt.Errorf("%w: no valid certificates found in payload",
zerr.ErrInvalidCertificateContent)
}
return true, nil

View file

@ -1,6 +1,7 @@
package signatures
import (
"context"
"encoding/json"
"time"
@ -8,6 +9,9 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
const (
@ -58,3 +62,84 @@ func VerifySignature(
return "", time.Time{}, false, zerr.ErrInvalidSignatureType
}
}
func NewTaskGenerator(metaDB mTypes.MetaDB, log log.Logger) scheduler.TaskGenerator {
return &sigValidityTaskGenerator{
repos: []mTypes.RepoMetadata{},
metaDB: metaDB,
repoIndex: -1,
log: log,
}
}
type sigValidityTaskGenerator struct {
repos []mTypes.RepoMetadata
metaDB mTypes.MetaDB
repoIndex int
done bool
log log.Logger
}
func (gen *sigValidityTaskGenerator) Next() (scheduler.Task, error) {
if len(gen.repos) == 0 {
ctx := context.Background()
repos, err := gen.metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool {
return true
})
if err != nil {
return nil, err
}
gen.repos = repos
}
gen.repoIndex++
if gen.repoIndex >= len(gen.repos) {
gen.done = true
return nil, nil
}
return NewValidityTask(gen.metaDB, gen.repos[gen.repoIndex], gen.log), nil
}
func (gen *sigValidityTaskGenerator) IsDone() bool {
return gen.done
}
func (gen *sigValidityTaskGenerator) Reset() {
gen.done = false
gen.repoIndex = -1
gen.repos = []mTypes.RepoMetadata{}
}
type validityTask struct {
metaDB mTypes.MetaDB
repo mTypes.RepoMetadata
log log.Logger
}
func NewValidityTask(metaDB mTypes.MetaDB, repo mTypes.RepoMetadata, log log.Logger) *validityTask {
return &validityTask{metaDB, repo, log}
}
func (validityT *validityTask) DoWork() error {
validityT.log.Info().Msg("updating signatures validity")
for signedManifest, sigs := range validityT.repo.Signatures {
if len(sigs[CosignSignature]) != 0 || len(sigs[NotationSignature]) != 0 {
err := validityT.metaDB.UpdateSignaturesValidity(validityT.repo.Name, godigest.Digest(signedManifest))
if err != nil {
validityT.log.Info().Msg("error while verifying signatures")
return err
}
}
}
validityT.log.Info().Msg("verifying signatures successfully completed")
return nil
}

View file

@ -43,6 +43,7 @@ import (
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"golang.org/x/crypto/bcrypt"
"gopkg.in/resty.v1"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
@ -126,6 +127,17 @@ func MakeHtpasswdFile() string {
return MakeHtpasswdFileFromString(content)
}
func GetCredString(username, password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
return usernameAndHash
}
func MakeHtpasswdFileFromString(fileContent string) string {
htpasswdFile, err := os.CreateTemp("", "htpasswd-")
if err != nil {

View file

@ -19,7 +19,6 @@ import (
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"golang.org/x/crypto/bcrypt"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
@ -610,7 +609,7 @@ func TestUploadImage(t *testing.T) {
user1 := "test"
password1 := "test"
testString1 := getCredString(user1, password1)
testString1 := test.GetCredString(user1, password1)
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
@ -768,17 +767,6 @@ func TestUploadImage(t *testing.T) {
})
}
func getCredString(username, password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
panic(err)
}
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
return usernameAndHash
}
func TestInjectUploadImage(t *testing.T) {
Convey("Inject failures for unreachable lines", t, func() {
port := test.GetFreePort()
@ -909,7 +897,7 @@ func TestInjectUploadImageWithBasicAuth(t *testing.T) {
user := "user"
password := "password"
testString := getCredString(user, password)
testString := test.GetCredString(user, password)
htpasswdPath := test.MakeHtpasswdFileFromString(testString)
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{

View file

@ -20,6 +20,126 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/auth/apikey": {
"post": {
"description": "Can create an api key for a logged in user, based on the provided label and scopes.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Create an API key for the current user",
"parameters": [
{
"description": "api token id (UUID)",
"name": "id",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.APIKeyPayload"
}
}
],
"responses": {
"201": {
"description": "created",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request",
"schema": {
"type": "string"
}
},
"401": {
"description": "unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "string"
}
}
}
},
"delete": {
"description": "Revokes one current user API key based on given key ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Revokes one current user API key",
"parameters": [
{
"type": "string",
"description": "api token id (UUID)",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request",
"schema": {
"type": "string"
}
},
"401": {
"description": "unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/auth/logout": {
"post": {
"description": "Logout by removing current session",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Logout by removing current session",
"responses": {
"200": {
"description": "ok\".",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error\".",
"schema": {
"type": "string"
}
}
}
}
},
"/oras/artifacts/v1/{name}/manifests/{digest}/referrers": {
"get": {
"description": "Get references for an image given a digest and artifact type",
@ -141,6 +261,49 @@ const docTemplate = `{
}
}
},
"/v2/_zot/ext/cosign": {
"post": {
"description": "Upload cosign public keys for verifying signatures",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"summary": "Upload cosign public keys for verifying signatures",
"parameters": [
{
"description": "Public key content",
"name": "requestBody",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request\".",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error\".",
"schema": {
"type": "string"
}
}
}
}
},
"/v2/_zot/ext/mgmt": {
"get": {
"description": "Get current server configuration",
@ -176,38 +339,19 @@ const docTemplate = `{
}
}
}
},
}
},
"/v2/_zot/ext/notation": {
"post": {
"description": "Upload certificates and public keys for verifying signatures",
"description": "Upload notation certificates for verifying signatures",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"summary": "Upload certificates and public keys for verifying signatures",
"summary": "Upload notation certificates for verifying signatures",
"parameters": [
{
"enum": [
"signatures"
],
"type": "string",
"description": "specify resource",
"name": "resource",
"in": "query",
"required": true
},
{
"enum": [
"cosign",
"notation"
],
"type": "string",
"description": "specify signing tool",
"name": "tool",
"in": "query",
"required": true
},
{
"type": "string",
"description": "truststore type",
@ -221,7 +365,7 @@ const docTemplate = `{
"in": "query"
},
{
"description": "Public key or Certificate content",
"description": "Certificate content",
"name": "requestBody",
"in": "body",
"required": true,
@ -992,6 +1136,20 @@ const docTemplate = `{
}
},
"definitions": {
"api.APIKeyPayload": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"scopes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"api.ExtensionList": {
"type": "object",
"properties": {
@ -1013,6 +1171,10 @@ const docTemplate = `{
"type": "string"
}
},
"artifactType": {
"description": "ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact.",
"type": "string"
},
"manifests": {
"description": "Manifests references platform specific manifests.",
"type": "array",
@ -1027,6 +1189,14 @@ const docTemplate = `{
"schemaVersion": {
"description": "SchemaVersion is the image manifest schema that this image follows",
"type": "integer"
},
"subject": {
"description": "Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest.",
"allOf": [
{
"$ref": "#/definitions/github_com_opencontainers_image-spec_specs-go_v1.Descriptor"
}
]
}
}
},
@ -1118,6 +1288,9 @@ const docTemplate = `{
"type": "string"
}
}
},
"openid": {
"$ref": "#/definitions/extensions.OpenIDConfig"
}
}
},
@ -1160,6 +1333,20 @@ const docTemplate = `{
}
}
},
"extensions.OpenIDConfig": {
"type": "object",
"properties": {
"providers": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/extensions.OpenIDProviderConfig"
}
}
}
},
"extensions.OpenIDProviderConfig": {
"type": "object"
},
"extensions.StrippedConfig": {
"type": "object",
"properties": {

View file

@ -11,6 +11,126 @@
"version": "v1.1.0-dev"
},
"paths": {
"/auth/apikey": {
"post": {
"description": "Can create an api key for a logged in user, based on the provided label and scopes.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Create an API key for the current user",
"parameters": [
{
"description": "api token id (UUID)",
"name": "id",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.APIKeyPayload"
}
}
],
"responses": {
"201": {
"description": "created",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request",
"schema": {
"type": "string"
}
},
"401": {
"description": "unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "string"
}
}
}
},
"delete": {
"description": "Revokes one current user API key based on given key ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Revokes one current user API key",
"parameters": [
{
"type": "string",
"description": "api token id (UUID)",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request",
"schema": {
"type": "string"
}
},
"401": {
"description": "unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/auth/logout": {
"post": {
"description": "Logout by removing current session",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Logout by removing current session",
"responses": {
"200": {
"description": "ok\".",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error\".",
"schema": {
"type": "string"
}
}
}
}
},
"/oras/artifacts/v1/{name}/manifests/{digest}/referrers": {
"get": {
"description": "Get references for an image given a digest and artifact type",
@ -132,6 +252,49 @@
}
}
},
"/v2/_zot/ext/cosign": {
"post": {
"description": "Upload cosign public keys for verifying signatures",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"summary": "Upload cosign public keys for verifying signatures",
"parameters": [
{
"description": "Public key content",
"name": "requestBody",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "ok",
"schema": {
"type": "string"
}
},
"400": {
"description": "bad request\".",
"schema": {
"type": "string"
}
},
"500": {
"description": "internal server error\".",
"schema": {
"type": "string"
}
}
}
}
},
"/v2/_zot/ext/mgmt": {
"get": {
"description": "Get current server configuration",
@ -167,38 +330,19 @@
}
}
}
},
}
},
"/v2/_zot/ext/notation": {
"post": {
"description": "Upload certificates and public keys for verifying signatures",
"description": "Upload notation certificates for verifying signatures",
"consumes": [
"application/octet-stream"
],
"produces": [
"application/json"
],
"summary": "Upload certificates and public keys for verifying signatures",
"summary": "Upload notation certificates for verifying signatures",
"parameters": [
{
"enum": [
"signatures"
],
"type": "string",
"description": "specify resource",
"name": "resource",
"in": "query",
"required": true
},
{
"enum": [
"cosign",
"notation"
],
"type": "string",
"description": "specify signing tool",
"name": "tool",
"in": "query",
"required": true
},
{
"type": "string",
"description": "truststore type",
@ -212,7 +356,7 @@
"in": "query"
},
{
"description": "Public key or Certificate content",
"description": "Certificate content",
"name": "requestBody",
"in": "body",
"required": true,
@ -983,6 +1127,20 @@
}
},
"definitions": {
"api.APIKeyPayload": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"scopes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"api.ExtensionList": {
"type": "object",
"properties": {
@ -1004,6 +1162,10 @@
"type": "string"
}
},
"artifactType": {
"description": "ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact.",
"type": "string"
},
"manifests": {
"description": "Manifests references platform specific manifests.",
"type": "array",
@ -1018,6 +1180,14 @@
"schemaVersion": {
"description": "SchemaVersion is the image manifest schema that this image follows",
"type": "integer"
},
"subject": {
"description": "Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest.",
"allOf": [
{
"$ref": "#/definitions/github_com_opencontainers_image-spec_specs-go_v1.Descriptor"
}
]
}
}
},
@ -1109,6 +1279,9 @@
"type": "string"
}
}
},
"openid": {
"$ref": "#/definitions/extensions.OpenIDConfig"
}
}
},
@ -1151,6 +1324,20 @@
}
}
},
"extensions.OpenIDConfig": {
"type": "object",
"properties": {
"providers": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/extensions.OpenIDProviderConfig"
}
}
}
},
"extensions.OpenIDProviderConfig": {
"type": "object"
},
"extensions.StrippedConfig": {
"type": "object",
"properties": {

View file

@ -1,4 +1,13 @@
definitions:
api.APIKeyPayload:
properties:
label:
type: string
scopes:
items:
type: string
type: array
type: object
api.ExtensionList:
properties:
extensions:
@ -13,6 +22,10 @@ definitions:
type: string
description: Annotations contains arbitrary metadata for the image index.
type: object
artifactType:
description: ArtifactType specifies the IANA media type of artifact when the
manifest is used for an artifact.
type: string
manifests:
description: Manifests references platform specific manifests.
items:
@ -25,6 +38,12 @@ definitions:
schemaVersion:
description: SchemaVersion is the image manifest schema that this image follows
type: integer
subject:
allOf:
- $ref: '#/definitions/github_com_opencontainers_image-spec_specs-go_v1.Descriptor'
description: Subject is an optional link from the image manifest to another
manifest forming an association between the image manifest and the other
manifest.
type: object
api.ImageManifest:
properties:
@ -89,6 +108,8 @@ definitions:
address:
type: string
type: object
openid:
$ref: '#/definitions/extensions.OpenIDConfig'
type: object
extensions.BearerConfig:
properties:
@ -115,6 +136,15 @@ definitions:
path:
type: string
type: object
extensions.OpenIDConfig:
properties:
providers:
additionalProperties:
$ref: '#/definitions/extensions.OpenIDProviderConfig'
type: object
type: object
extensions.OpenIDProviderConfig:
type: object
extensions.StrippedConfig:
properties:
binaryType:
@ -206,6 +236,86 @@ info:
title: Open Container Initiative Distribution Specification
version: v1.1.0-dev
paths:
/auth/apikey:
delete:
consumes:
- application/json
description: Revokes one current user API key based on given key ID
parameters:
- description: api token id (UUID)
in: query
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: ok
schema:
type: string
"400":
description: bad request
schema:
type: string
"401":
description: unauthorized
schema:
type: string
"500":
description: internal server error
schema:
type: string
summary: Revokes one current user API key
post:
consumes:
- application/json
description: Can create an api key for a logged in user, based on the provided
label and scopes.
parameters:
- description: api token id (UUID)
in: body
name: id
required: true
schema:
$ref: '#/definitions/api.APIKeyPayload'
produces:
- application/json
responses:
"201":
description: created
schema:
type: string
"400":
description: bad request
schema:
type: string
"401":
description: unauthorized
schema:
type: string
"500":
description: internal server error
schema:
type: string
summary: Create an API key for the current user
/auth/logout:
post:
consumes:
- application/json
description: Logout by removing current session
produces:
- application/json
responses:
"200":
description: ok".
schema:
type: string
"500":
description: internal server error".
schema:
type: string
summary: Logout by removing current session
/oras/artifacts/v1/{name}/manifests/{digest}/referrers:
get:
consumes:
@ -286,6 +396,34 @@ paths:
schema:
$ref: '#/definitions/api.ExtensionList'
summary: List Registry level extensions
/v2/_zot/ext/cosign:
post:
consumes:
- application/octet-stream
description: Upload cosign public keys for verifying signatures
parameters:
- description: Public key content
in: body
name: requestBody
required: true
schema:
type: string
produces:
- application/json
responses:
"200":
description: ok
schema:
type: string
"400":
description: bad request".
schema:
type: string
"500":
description: internal server error".
schema:
type: string
summary: Upload cosign public keys for verifying signatures
/v2/_zot/ext/mgmt:
get:
consumes:
@ -310,26 +448,12 @@ paths:
schema:
type: string
summary: Get current server configuration
/v2/_zot/ext/notation:
post:
consumes:
- application/octet-stream
description: Upload certificates and public keys for verifying signatures
description: Upload notation certificates for verifying signatures
parameters:
- description: specify resource
enum:
- signatures
in: query
name: resource
required: true
type: string
- description: specify signing tool
enum:
- cosign
- notation
in: query
name: tool
required: true
type: string
- description: truststore type
in: query
name: truststoreType
@ -338,7 +462,7 @@ paths:
in: query
name: truststoreName
type: string
- description: Public key or Certificate content
- description: Certificate content
in: body
name: requestBody
required: true
@ -359,7 +483,7 @@ paths:
description: internal server error".
schema:
type: string
summary: Upload certificates and public keys for verifying signatures
summary: Upload notation certificates for verifying signatures
/v2/_zot/ext/userprefs:
put:
consumes:

View file

@ -25,6 +25,9 @@ function setup_file() {
"extensions": {
"search": {
"enable": true
},
"ui": {
"enable": true
}
},
"http": {