0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -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 # 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: env:
CGO_ENABLED: 0 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: steps:
- name: Checkout repository - name: Checkout repository

View file

@ -32,7 +32,7 @@ jobs:
# Optional: golangci-lint command line arguments. # Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0 # 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`. # Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true # only-new-issues: true

1
.gitignore vendored
View file

@ -30,3 +30,4 @@ tags
vendor/ vendor/
.vscode/ .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 OS ?= linux
ARCH ?= amd64 ARCH ?= amd64
BENCH_OUTPUT ?= stdout BENCH_OUTPUT ?= stdout
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,apikey EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,imagetrust
UI_DEPENDENCIES := search,mgmt,userprefs,apikey UI_DEPENDENCIES := search,mgmt,userprefs
# freebsd/arm64 not supported for pie builds # freebsd/arm64 not supported for pie builds
BUILDMODE_FLAGS := -buildmode=pie BUILDMODE_FLAGS := -buildmode=pie
ifeq ($(OS),freebsd) ifeq ($(OS),freebsd)

View file

@ -107,6 +107,7 @@ var (
ErrInvalidTruststoreType = errors.New("signatures: invalid truststore type") ErrInvalidTruststoreType = errors.New("signatures: invalid truststore type")
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name") ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content") 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") 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") 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 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 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 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! 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 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 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: 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 ### 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 filesystem storage sessions are saved in zot's root directory.
In case of using cloud storage sessions are saved in memory. 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 #### Authentication Failures
Should authentication fail, to prevent automated attacks, a delayed response can be configured with: 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 "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 ## Identity-based Authorization
Allowing actions on one or more repository paths can be tied to user Allowing actions on one or more repository paths can be tied to user

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@ import (
"time" "time"
"github.com/chartmuseum/auth" "github.com/chartmuseum/auth"
guuid "github.com/gofrs/uuid"
"github.com/google/go-github/v52/github" "github.com/google/go-github/v52/github"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -353,7 +354,7 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
return return
} }
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix isMgmtRequested := request.RequestURI == constants.FullMgmt
allowAnonymous := ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() allowAnonymous := ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists()
// try basic auth if authorization header is given // try basic auth if authorization header is given
@ -443,7 +444,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
name := vars["name"] name := vars["name"]
// we want to bypass auth for mgmt route // we want to bypass auth for mgmt route
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix isMgmtRequested := request.RequestURI == constants.FullMgmt
header := request.Header.Get("Authorization") header := request.Header.Get("Authorization")
@ -849,3 +850,25 @@ func GetAuthUserFromRequestSession(cookieStore sessions.Store, request *http.Req
return identity, true 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 //go:build mgmt
// +build apikey // +build mgmt
package extensions_test package api_test
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/http/httptest"
"os" "os"
"testing" "testing"
guuid "github.com/gofrs/uuid"
"github.com/project-zot/mockoidc" "github.com/project-zot/mockoidc"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
@ -18,14 +21,16 @@ import (
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types" mTypes "zotregistry.io/zot/pkg/meta/types"
localCtx "zotregistry.io/zot/pkg/requestcontext" localCtx "zotregistry.io/zot/pkg/requestcontext"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks" "zotregistry.io/zot/pkg/test/mocks"
) )
var ErrUnexpectedError = errors.New("error: unexpected error")
type ( type (
apiKeyResponse struct { apiKeyResponse struct {
mTypes.APIKeyDetails 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) { func TestAPIKeys(t *testing.T) {
Convey("Make a new controller", t, func() { Convey("Make a new controller", t, func() {
@ -59,6 +87,8 @@ func TestAPIKeys(t *testing.T) {
}() }()
mockOIDCConfig := mockOIDCServer.Config() mockOIDCConfig := mockOIDCServer.Config()
defaultVal := true
conf.HTTP.Auth = &config.AuthConfig{ conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{ HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath, Path: htpasswdPath,
@ -74,23 +104,17 @@ func TestAPIKeys(t *testing.T) {
}, },
}, },
}, },
APIKey: defaultVal,
} }
conf.HTTP.AccessControl = &config.AccessControlConfig{} conf.HTTP.AccessControl = &config.AccessControlConfig{}
defaultVal := true conf.Extensions = &extconf.ExtensionConfig{}
apiKeyConfig := &extconf.APIKeyConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, conf.Extensions.Search.Enable = &defaultVal
} conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
mgmtConfg := &extconf.MgmtConfig{ conf.Extensions.UI.Enable = &defaultVal
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
APIKey: apiKeyConfig,
Mgmt: mgmtConfg,
}
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
dir := t.TempDir() dir := t.TempDir()
@ -103,7 +127,7 @@ func TestAPIKeys(t *testing.T) {
defer cm.StopServer() defer cm.StopServer()
test.WaitTillServerReady(baseURL) test.WaitTillServerReady(baseURL)
payload := extensions.APIKeyPayload{ payload := api.APIKeyPayload{
Label: "test", Label: "test",
Scopes: []string{"test"}, Scopes: []string{"test"},
} }
@ -115,7 +139,7 @@ func TestAPIKeys(t *testing.T) {
resp, err := resty.R(). resp, err := resty.R().
SetBody(reqBody). SetBody(reqBody).
SetBasicAuth("test", "test"). SetBasicAuth("test", "test").
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated) So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -141,7 +165,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = resty.R(). resp, err = resty.R().
SetBody(reqBody). SetBody(reqBody).
SetBasicAuth("test", "test"). SetBasicAuth("test", "test").
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated) So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -175,7 +199,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBody(reqBody). SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@ -186,7 +210,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBody(reqBody). SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated) So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -260,7 +284,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBody(reqBody). SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@ -270,8 +294,7 @@ func TestAPIKeys(t *testing.T) {
client := resty.New() client := resty.New()
// mgmt should work both unauthenticated and authenticated // mgmt should work both unauthenticated and authenticated
resp, err := client.R(). resp, err := client.R().Get(baseURL + constants.FullMgmt)
Get(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -292,7 +315,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBody(reqBody). SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated) So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -326,7 +349,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey). SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -334,7 +357,7 @@ func TestAPIKeys(t *testing.T) {
// invalid api keys // invalid api keys
resp, err = client.R(). resp, err = client.R().
SetBasicAuth("invalidEmail", apiKeyResponse.APIKey). SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@ -366,7 +389,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey). SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
@ -375,8 +398,7 @@ func TestAPIKeys(t *testing.T) {
client.SetRedirectPolicy(test.CustomRedirectPolicy(20)) client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
// without creds should work // without creds should work
resp, err = client.R(). resp, err = client.R().Get(baseURL + constants.FullMgmt)
Get(baseURL + constants.FullMgmtPrefix)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -395,7 +417,7 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBody(reqBody). SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.FullAPIKeyPrefix) Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated) So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
@ -406,7 +428,7 @@ func TestAPIKeys(t *testing.T) {
// should work with session // should work with session
resp, err = client.R(). resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -414,7 +436,7 @@ func TestAPIKeys(t *testing.T) {
// should work with api key // should work with api key
resp, err = client.R(). resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey). SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -433,14 +455,14 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("id", apiKeyResponse.UUID). SetQueryParam("id", apiKeyResponse.UUID).
Delete(baseURL + constants.FullAPIKeyPrefix) Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R(). resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Delete(baseURL + constants.FullAPIKeyPrefix) Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
@ -455,18 +477,31 @@ func TestAPIKeys(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBasicAuth("test", "test"). SetBasicAuth("test", "test").
SetQueryParam("id", apiKeyResponse.UUID). SetQueryParam("id", apiKeyResponse.UUID).
Delete(baseURL + constants.FullAPIKeyPrefix) Delete(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// unsupported method // unsupported method
resp, err = client.R(). resp, err = client.R().
Put(baseURL + constants.FullAPIKeyPrefix) Put(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed) 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() mockOIDCConfig := mockOIDCServer.Config()
defaultVal := true
conf.HTTP.Auth = &config.AuthConfig{ conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{ HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath, Path: htpasswdPath,
@ -505,14 +542,8 @@ func TestAPIKeysOpenDBError(t *testing.T) {
}, },
}, },
}, },
}
defaultVal := true APIKey: defaultVal,
apiKeyConfig := &extconf.APIKeyConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
APIKey: apiKeyConfig,
} }
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
@ -529,3 +560,58 @@ func TestAPIKeysOpenDBError(t *testing.T) {
}, ShouldPanic) }, 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 LDAP *LDAPConfig
Bearer *BearerConfig Bearer *BearerConfig
OpenID *OpenIDConfig OpenID *OpenIDConfig
APIKey bool
} }
type BearerConfig struct { type BearerConfig struct {
@ -274,8 +275,7 @@ func (c *Config) IsOpenIDAuthEnabled() bool {
} }
func (c *Config) IsAPIKeyEnabled() bool { func (c *Config) IsAPIKeyEnabled() bool {
if c.Extensions != nil && c.Extensions.APIKey != nil && if c.HTTP.Auth != nil && c.HTTP.Auth.APIKey {
*c.Extensions.APIKey.Enable {
return true return true
} }
@ -308,6 +308,38 @@ func isOpenIDAuthProviderEnabled(config *Config, provider string) bool {
return false 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 { func IsOpenIDSupported(provider string) bool {
for _, supportedProvider := range openIDSupportedProviders { for _, supportedProvider := range openIDSupportedProviders {
if supportedProvider == provider { if supportedProvider == provider {

View file

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

View file

@ -4,21 +4,31 @@ package constants
const ( const (
ExtCatalogPrefix = "/_catalog" ExtCatalogPrefix = "/_catalog"
ExtOciDiscoverPrefix = "/_oci/ext/discover" ExtOciDiscoverPrefix = "/_oci/ext/discover"
// zot specific extensions.
ExtPrefix = "/_zot/ext"
// zot specific extensions.
BasePrefix = "/_zot"
ExtPrefix = BasePrefix + "/ext"
// search extension.
ExtSearch = "/search" ExtSearch = "/search"
ExtSearchPrefix = ExtPrefix + ExtSearch ExtSearchPrefix = ExtPrefix + ExtSearch
FullSearchPrefix = RoutePrefix + ExtSearchPrefix FullSearchPrefix = RoutePrefix + ExtSearchPrefix
ExtMgmt = "/mgmt" // mgmt extension.
ExtMgmtPrefix = ExtPrefix + ExtMgmt Mgmt = "/mgmt"
FullMgmtPrefix = RoutePrefix + ExtMgmtPrefix ExtMgmt = ExtPrefix + Mgmt
FullMgmt = RoutePrefix + ExtMgmt
ExtUserPreferences = "/userprefs" // signatures extension.
ExtUserPreferencesPrefix = ExtPrefix + ExtUserPreferences Notation = "/notation"
FullUserPreferencesPrefix = RoutePrefix + ExtUserPreferencesPrefix ExtNotation = ExtPrefix + Notation
ExtAPIKey = "/apikey" FullNotation = RoutePrefix + ExtNotation
ExtAPIKeyPrefix = ExtPrefix + ExtAPIKey //nolint: gosec Cosign = "/cosign"
FullAPIKeyPrefix = RoutePrefix + ExtAPIKeyPrefix 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 { 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 // init metaDB if search is enabled or we need to store user profiles, api keys or signatures
if (c.Config.Extensions != nil && c.Config.Extensions.Search != nil && *c.Config.Extensions.Search.Enable) || if c.Config.IsSearchEnabled() || c.Config.IsBasicAuthnEnabled() || c.Config.IsImageTrustEnabled() {
c.Config.IsBasicAuthnEnabled() {
driver, err := meta.New(c.Config.Storage.StorageConfig, c.Log) //nolint:contextcheck driver, err := meta.New(c.Config.Storage.StorageConfig, c.Log) //nolint:contextcheck
if err != nil { if err != nil {
return err return err
@ -368,11 +367,8 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
c.SyncOnDemand = syncOnDemand c.SyncOnDemand = syncOnDemand
} }
if c.Config.Extensions != nil { // we can later move enabling the other scheduled tasks inside the call below
if c.Config.Extensions.Mgmt != nil && *c.Config.Extensions.Mgmt.Enable { ext.EnableScheduledTasks(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
ext.EnablePeriodicSignaturesVerification(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
}
}
} }
type SyncOnDemand interface { type SyncOnDemand interface {

View file

@ -1,5 +1,5 @@
//go: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,apikey,mgmt // +build sync,scrub,metrics,search,lint,userprefs,mgmt,imagetrust,ui
package api_test 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}, BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
} }
conf.Extensions = &extconf.ExtensionConfig{ conf.Extensions = &extconf.ExtensionConfig{
Mgmt: mgmtConfg, Search: searchConfig,
UI: uiConfig,
} }
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
@ -2769,7 +2775,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL) resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) 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/") resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -2778,7 +2784,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBasicAuth(htpasswdUsername, passphrase). SetBasicAuth(htpasswdUsername, passphrase).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2795,7 +2801,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2835,7 +2841,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL) resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) 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/") resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -2844,7 +2850,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetBasicAuth(username, passphrase). SetBasicAuth(username, passphrase).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2861,7 +2867,7 @@ func TestOpenIDMiddleware(t *testing.T) {
resp, err = client.R(). resp, err = client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -2888,7 +2894,7 @@ func TestOpenIDMiddleware(t *testing.T) {
// mgmt should work both unauthenticated and authenticated // mgmt should work both unauthenticated and authenticated
resp, err := client.R(). resp, err := client.R().
Get(baseURL + constants.FullMgmtPrefix) Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) 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}, BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
} }
conf.Extensions = &extconf.ExtensionConfig{ conf.Extensions = &extconf.ExtensionConfig{
Mgmt: mgmtConfg, UI: uiConfig,
Search: searchConfig,
} }
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
@ -8391,7 +8402,7 @@ func TestSearchRoutes(t *testing.T) {
} }
func TestDistSpecExtensions(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() conf := config.New()
port := test.GetFreePort() port := test.GetFreePort()
baseURL := test.GetBaseURL(port) baseURL := test.GetBaseURL(port)
@ -8400,13 +8411,16 @@ func TestDistSpecExtensions(t *testing.T) {
defaultVal := true defaultVal := true
searchConfig := &extconf.SearchConfig{ conf.Extensions = &extconf.ExtensionConfig{}
BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, conf.Extensions.Search = &extconf.SearchConfig{}
} conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
conf.Extensions = &extconf.ExtensionConfig{ conf.Extensions.UI = &extconf.UIConfig{}
Search: searchConfig, 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") logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -8427,16 +8441,23 @@ func TestDistSpecExtensions(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &extensionList) err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil) So(err, ShouldBeNil)
t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1) 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].Name, ShouldEqual, "_zot")
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md") So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty) 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.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() conf := config.New()
port := test.GetFreePort() port := test.GetFreePort()
baseURL := test.GetBaseURL(port) baseURL := test.GetBaseURL(port)
@ -8445,18 +8466,9 @@ func TestDistSpecExtensions(t *testing.T) {
defaultVal := true defaultVal := true
searchConfig := &extconf.SearchConfig{ conf.Extensions = &extconf.ExtensionConfig{}
BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, conf.Extensions.Search = &extconf.SearchConfig{}
} conf.Extensions.Search.Enable = &defaultVal
mgmtConfg := &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
Mgmt: mgmtConfg,
}
logFile, err := os.CreateTemp("", "zot-log*.txt") logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -8477,14 +8489,51 @@ func TestDistSpecExtensions(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &extensionList) err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil) So(err, ShouldBeNil)
t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1) 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].Name, ShouldEqual, "_zot")
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md") So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty) 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.FullSearchPrefix)
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPreferencesPrefix) // Verify the endpoints below are not enabled since trust is not enabled
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmtPrefix) 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) { Convey("start minimal zot server", t, func(c C) {

View file

@ -19,9 +19,12 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
guuid "github.com/gofrs/uuid"
"github.com/google/go-github/v52/github" "github.com/google/go-github/v52/github"
"github.com/gorilla/mux" "github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
"github.com/opencontainers/distribution-spec/specs-go/v1/extensions" "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -40,6 +43,7 @@ import (
syncConstants "zotregistry.io/zot/pkg/extensions/sync/constants" syncConstants "zotregistry.io/zot/pkg/extensions/sync/constants"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta" "zotregistry.io/zot/pkg/meta"
mTypes "zotregistry.io/zot/pkg/meta/types"
zreg "zotregistry.io/zot/pkg/regexp" zreg "zotregistry.io/zot/pkg/regexp"
localCtx "zotregistry.io/zot/pkg/requestcontext" localCtx "zotregistry.io/zot/pkg/requestcontext"
storageCommon "zotregistry.io/zot/pkg/storage/common" 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 /* on every route which may be used by UI we set OPTIONS as allowed METHOD
to enable preflight request from UI to backend */ to enable preflight request from UI to backend */
if rh.c.Config.IsBasicAuthnEnabled() { if rh.c.Config.IsBasicAuthnEnabled() {
@ -157,61 +174,42 @@ func (rh *RouteHandler) SetupRoutes() {
// swagger // swagger
debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log) 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 { 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 { if rh.c.Config.Extensions == nil {
// minimal build // minimal build
prefixedRouter.HandleFunc("/metrics", rh.GetMetrics).Methods("GET") prefixedRouter.HandleFunc("/metrics", rh.GetMetrics).Methods("GET")
} else { } else {
// extended build // extended build
prefixedExtensionsRouter := prefixedRouter.PathPrefix(constants.ExtPrefix).Subrouter() ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
prefixedExtensionsRouter.Use(CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin)) }
}
ext.SetupMgmtRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.Log) // Preconditions for enabling the actual extension routes are part of extensions themselves
ext.SetupSearchRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo, ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo,
rh.c.Log) rh.c.Log)
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.MetaDB, ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
rh.c.CveInfo, rh.c.Log) ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
ext.SetupAPIKeyRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.MetaDB, rh.c.CookieStore, rh.c.Log) ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, 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. // 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.SetupUIRoutes(rh.c.Config, rh.c.Router, 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)
})
}
} }
func getCORSHeadersHandler(allowOrigin string) func(http.HandlerFunc) http.HandlerFunc { func getCORSHeadersHandler(allowOrigin string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
addCORSHeaders(allowOrigin, response) zcommon.AddCORSHeaders(allowOrigin, response)
next.ServeHTTP(response, request) 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 { func getUIHeadersHandler(config *config.Config, allowedMethods ...string) func(http.HandlerFunc) http.HandlerFunc {
allowedMethodsValue := strings.Join(allowedMethods, ",") allowedMethodsValue := strings.Join(allowedMethods, ",")
@ -1980,6 +1978,123 @@ func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request *
zcommon.WriteJSON(response, http.StatusOK, rs) 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. // GetBlobUploadSessionLocation returns actual blob location to start/resume uploading blobs.
// e.g. /v2/<name>/blobs/uploads/<session-id>. // e.g. /v2/<name>/blobs/uploads/<session-id>.
func getBlobUploadSessionLocation(url *url.URL, sessionID string) string { 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 { func isSyncOnDemandEnabled(ctlr Controller) bool {
if ctlr.Config.Extensions != nil && if ctlr.Config.IsSyncEnabled() &&
ctlr.Config.Extensions.Sync != nil &&
*ctlr.Config.Extensions.Sync.Enable &&
fmt.Sprintf("%v", ctlr.SyncOnDemand) != fmt.Sprintf("%v", nil) { fmt.Sprintf("%v", ctlr.SyncOnDemand) != fmt.Sprintf("%v", nil) {
return true return true
} }

View file

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

View file

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

View file

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

View file

@ -313,13 +313,18 @@ func validateCacheConfig(cfg *config.Config) error {
} }
func validateExtensionsConfig(cfg *config.Config) error { func validateExtensionsConfig(cfg *config.Config) error {
if cfg.Extensions != nil && cfg.Extensions.UI != nil && cfg.Extensions.UI.Enable != nil && *cfg.Extensions.UI.Enable { if cfg.Extensions != nil && cfg.Extensions.Mgmt != nil {
if cfg.Extensions.Mgmt == nil || !*cfg.Extensions.Mgmt.Enable { log.Warn().Msg("The mgmt extensions configuration option has been made redundant and will be ignored.")
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without mgmt extension.")
return errors.ErrBadConfig
} }
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 {
// 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 { if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.") 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{} config.Extensions.Scrub = &extconf.ScrubConfig{}
} }
_, ok = extMap["mgmt"] _, ok = extMap["trust"]
if ok { if ok {
// we found a config like `"extensions": {"mgmt:": {}}` // we found a config like `"extensions": {"trust:": {}}`
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here // Note: In case trust is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.Mgmt = &extconf.MgmtConfig{} config.Extensions.Trust = &extconf.ImageTrustConfig{}
} }
_, ok = extMap["apikey"] _, ok = extMap["ui"]
if ok { if ok {
// we found a config like `"extensions": {"mgmt:": {}}` // we found a config like `"extensions": {"ui:": {}}`
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here // Note: In case UI is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.APIKey = &extconf.APIKeyConfig{} 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 != nil {
if config.Extensions.Scrub.Enable == nil { if config.Extensions.Scrub.Enable == nil {
config.Extensions.Scrub.Enable = &defaultVal 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 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 { 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 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) { func updateDistSpecVersion(config *config.Config) {

View file

@ -1083,7 +1083,7 @@ func TestVerify(t *testing.T) {
} }
func TestValidateExtensionsConfig(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() config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json") tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -1100,40 +1100,39 @@ func TestValidateExtensionsConfig(t *testing.T) {
"level": "debug" "level": "debug"
}, },
"extensions": { "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": { "mgmt": {
"enable": "true" "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) 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() config := config.New()
tmpfile, err := os.CreateTemp("", "zot-test*.json") tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -1172,7 +1171,165 @@ func TestValidateExtensionsConfig(t *testing.T) {
err = os.WriteFile(tmpfile.Name(), content, 0o0600) err = os.WriteFile(tmpfile.Name(), content, 0o0600)
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = cli.LoadConfiguration(config, tmpfile.Name()) 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/config"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
apiErr "zotregistry.io/zot/pkg/api/errors" apiErr "zotregistry.io/zot/pkg/api/errors"
localCtx "zotregistry.io/zot/pkg/requestcontext"
) )
func AllowedMethods(methods ...string) []string { 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, ",") allowedMethodsValue := strings.Join(allowedMethods, ",")
return func(next http.Handler) http.Handler { 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) { func AuthzFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
time.Sleep(time.Duration(delay) * time.Second) time.Sleep(time.Duration(delay) * time.Second)

View file

@ -52,6 +52,7 @@ type ImageSummary struct {
Vendor string `json:"vendor"` Vendor string `json:"vendor"`
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"` Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
Referrers []Referrer `json:"referrers"` Referrers []Referrer `json:"referrers"`
SignatureInfo []SignatureSummary `json:"signatureInfo"`
} }
type ManifestSummary struct { type ManifestSummary struct {
@ -67,6 +68,13 @@ type ManifestSummary struct {
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"` Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
Referrers []Referrer `json:"referrers"` Referrers []Referrer `json:"referrers"`
ArtifactType string `json:"artifactType"` 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 { type Platform struct {

View file

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

View file

@ -6,13 +6,12 @@ package debug
import ( import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
) )
// SetupGQLPlaygroundRoutes ... // SetupGQLPlaygroundRoutes ...
func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router, func SetupGQLPlaygroundRoutes(router *mux.Router,
storeController storage.StoreController, log log.Logger, storeController storage.StoreController, log log.Logger,
) { ) {
log.Warn().Msg("skipping enabling graphql playground extension because given zot binary " + 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,39 +1,66 @@
# 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 ## 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 |
### Uploading a Cosign public key
The Cosign public keys uploaded correspond to the private keys used to sign images with `cosign`.
***Example of request*** ***Example of request***
```
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=cosign" ```bash
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/cosign
``` ```
2. upload a certificate 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*** ***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" ```bash
curl --data-binary @certificate.crt -X POST "http://localhost:8080/v2/_zot/ext/notation?truststoreType=ca&truststoreName=upload-cert"
``` ```
Besides the requested files, the user should also specify the `tool` which should be : 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.
- `cosign` for uploading public keys ## Verification and results
- `notation` for uploading certificates
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. 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`) - 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) - info about the trustworthiness of the signature (if there is a certificate or a public key which can successfully verify the signature)
@ -42,7 +69,9 @@ Besides the requested files, the user should also specify the `tool` which shoul
- the public key -> for signatures generated using `cosign` - the public key -> for signatures generated using `cosign`
- the subject of the certificate -> for signatures generated using `notation` - the subject of the certificate -> for signatures generated using `notation`
**Example of GraphQL output** The information above will be included in the ManifestSummary objects returned by the `search` extension.
***Example of GraphQL output***
```json ```json
{ {
@ -92,6 +121,7 @@ Besides the requested files, the user should also specify the `tool` which shoul
- The files (public keys and certificates) uploaded using the exposed routes will be stored in some specific directories called `_cosign` and `_notation` under `$rootDir`. - 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` directory will contain the uploaded public keys
``` ```
_cosign _cosign
├── $publicKey1 ├── $publicKey1
@ -111,7 +141,8 @@ Besides the requested files, the user should also specify the `tool` which shoul
``` ```
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: 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", "version": "1.0",
"trustPolicies": [ "trustPolicies": [
@ -129,4 +160,3 @@ 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 | | Supported queries | Input | Output | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [Get current configuration](#get-current-configuration) | None | config json | Get current zot configuration | | [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 ## 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 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. 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 Component | Endpoint | Description
--- | --- | --- --- | --- | ---
[`search`](search/search.md) | `/v2/_zot/ext/search` | efficient and enhanced registry search capabilities using graphQL backend [`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 [`mgmt`](README_mgmt.md) | `/v2/_zot/ext/mgmt` | config management
[`userprefs`](userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences [`userprefs`](README_userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences
[`apikey`](README_apikey.md) | `/v2/_zot/ext/apikey` | user api keys management [`imagetrust`](README_imagetrust.md) | `/v2/_zot/ext/cosign` | cosign public key management
[`imagetrust`](README_imagetrust.md) | `/v2/_zot/ext/notation` | notation certificate management
# References # References

View file

@ -20,6 +20,13 @@ type ExtensionConfig struct {
UI *UIConfig UI *UIConfig
Mgmt *MgmtConfig Mgmt *MgmtConfig
APIKey *APIKeyConfig APIKey *APIKeyConfig
Trust *ImageTrustConfig
}
type ImageTrustConfig struct {
BaseConfig `mapstructure:",squash"`
Cosign bool
Notation bool
} }
type APIKeyConfig struct { 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 package extensions_test
@ -14,8 +14,8 @@ import (
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
) )
func TestMgmtExtension(t *testing.T) { func TestImageTrustExtension(t *testing.T) {
Convey("periodic signature verification is skipped when binary doesn't include mgmt", t, func() { Convey("periodic signature verification is skipped when binary doesn't include imagetrust", t, func() {
conf := config.New() conf := config.New()
port := test.GetFreePort() port := test.GetFreePort()
@ -30,11 +30,10 @@ func TestMgmtExtension(t *testing.T) {
conf.Storage.RootDirectory = globalDir conf.Storage.RootDirectory = globalDir
conf.Storage.Commit = true conf.Storage.Commit = true
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Trust = &extconf.ImageTrustConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Trust.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Trust.Cosign = defaultValue
}, conf.Extensions.Trust.Notation = defaultValue
}
conf.Log.Level = "warn" conf.Log.Level = "warn"
conf.Log.Output = logFile.Name() 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/api/config"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
) )
func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir string) { 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, authFunc mux.MiddlewareFunc, log log.Logger,
) { ) {
log.Info().Msg("setting up metrics routes") log.Info().Msg("setting up metrics routes")

View file

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

View file

@ -4,27 +4,15 @@
package extensions package extensions
import ( import (
"context"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common" zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log" "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 { type HTPasswd struct {
@ -90,59 +78,32 @@ func (auth Auth) MarshalJSON() ([]byte, error) {
return json.Marshal((localAuth)(auth)) return json.Marshal((localAuth)(auth))
} }
type mgmt struct { func SetupMgmtRoutes(conf *config.Config, router *mux.Router, log log.Logger) {
config *config.Config if !conf.IsMgmtEnabled() {
log log.Logger 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 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") log.Info().Msg("setting up mgmt routes")
mgmt := mgmt{config: config, log: log} mgmt := Mgmt{Conf: conf, Log: log}
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost) // The endpoint for reading configuration should be available to all users
allowedMethods := zcommon.AllowedMethods(http.MethodGet)
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter() mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
mgmtRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...)) mgmtRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders()) mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders())
mgmtRouter.Methods(allowedMethods...).Handler(mgmt.handler()) 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 // mgmtHandler godoc
@ -154,182 +115,14 @@ func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger)
// @Param resource query string false "specify resource" Enums(config) // @Param resource query string false "specify resource" Enums(config)
// @Success 200 {object} extensions.StrippedConfig // @Success 200 {object} extensions.StrippedConfig
// @Failure 500 {string} string "internal server error". // @Failure 500 {string} string "internal server error".
func (mgmt *mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) { func (mgmt *Mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
sanitizedConfig := mgmt.config.Sanitize() sanitizedConfig := mgmt.Conf.Sanitize()
buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{}) buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
if err != nil { 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.WriteHeader(http.StatusInternalServerError)
} }
_, _ = w.Write(buf) _, _ = 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/api/config"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
) )
func IsBuiltWithMGMTExtension() bool { 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," + log.Warn().Msg("skipping setting up mgmt routes because given zot binary doesn't include this feature," +
"please build a binary that does so") "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 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, 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") 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 := router.PathPrefix(constants.ExtSearchPrefix).Subrouter()
extRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...)) extRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
extRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
extRouter.Use(zcommon.AddExtensionSecurityHeaders()) extRouter.Use(zcommon.AddExtensionSecurityHeaders())
extRouter.Methods(allowedMethods...). extRouter.Methods(allowedMethods...).
Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig))) 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" "github.com/gorilla/mux"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
) )
// content is our static web server content. // 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, log log.Logger,
) { ) {
if config.Extensions.UI != nil { if !conf.IsUIEnabled() {
log.Info().Msg("skip enabling the ui route as the config prerequisites are not met")
return
}
log.Info().Msg("setting up ui routes")
fsub, _ := fs.Sub(content, "build") fsub, _ := fs.Sub(content, "build")
uih := uiHandler{log: log} uih := uiHandler{log: log}
router.PathPrefix("/login").Handler(addUISecurityHeaders(uih)) // See https://go-review.googlesource.com/c/go/+/482635/2/src/net/http/fs.go
router.PathPrefix("/home").Handler(addUISecurityHeaders(uih)) // See https://github.com/golang/go/issues/59469
router.PathPrefix("/explore").Handler(addUISecurityHeaders(uih)) // In go 1.20.4 they decided to allow any method in the FileServer handler.
router.PathPrefix("/image").Handler(addUISecurityHeaders(uih)) // In order to be consistent with the status codes returned when the UI is disabled
router.PathPrefix("/").Handler(addUISecurityHeaders(http.FileServer(http.FS(fsub)))) // 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)
log.Info().Msg("setting up ui routes") 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/api/config"
"zotregistry.io/zot/pkg/log" "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 log.Logger,
) { ) {
log.Warn().Msg("skipping setting up ui routes because given zot binary doesn't include this feature," + 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" zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types" mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/storage"
) )
const ( const (
@ -27,23 +26,29 @@ func IsBuiltWithUserPrefsExtension() bool {
return true return true
} }
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController, func SetupUserPreferencesRoutes(conf *config.Config, router *mux.Router,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger, metaDB mTypes.MetaDB, log log.Logger,
) { ) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable { if !conf.AreUserPrefsEnabled() {
log.Info().Msg("skip enabling the user preferences route as the config prerequisites are not met")
return
}
log.Info().Msg("setting up user preferences routes") log.Info().Msg("setting up user preferences routes")
allowedMethods := zcommon.AllowedMethods(http.MethodPut) allowedMethods := zcommon.AllowedMethods(http.MethodPut)
userprefsRouter := router.PathPrefix(constants.ExtUserPreferences).Subrouter() userPrefsRouter := router.PathPrefix(constants.ExtUserPrefs).Subrouter()
userprefsRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...)) userPrefsRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
userprefsRouter.Use(zcommon.AddExtensionSecurityHeaders()) userPrefsRouter.Use(zcommon.AddExtensionSecurityHeaders())
userPrefsRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
userPrefsRouter.Methods(allowedMethods...).Handler(HandleUserPrefs(metaDB, log))
userprefsRouter.HandleFunc("", HandleUserPrefs(metaDB, log)).Methods(allowedMethods...) log.Info().Msg("finished setting up user preferences routes")
}
} }
// ListTags godoc // Repo preferences godoc
// @Summary Add bookmarks/stars info // @Summary Add bookmarks/stars info
// @Description Add bookmarks/stars info // @Description Add bookmarks/stars info
// @Router /v2/_zot/ext/userprefs [put] // @Router /v2/_zot/ext/userprefs [put]
@ -56,8 +61,8 @@ func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, store
// @Failure 403 {string} string "forbidden" // @Failure 403 {string} string "forbidden"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Failure 400 {string} string "bad request". // @Failure 400 {string} string "bad request".
func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) func(w http.ResponseWriter, r *http.Request) { func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) http.Handler {
return func(rsp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) {
if !zcommon.QueryHasParams(req.URL.Query(), []string{"action"}) { if !zcommon.QueryHasParams(req.URL.Query(), []string{"action"}) {
rsp.WriteHeader(http.StatusBadRequest) rsp.WriteHeader(http.StatusBadRequest)
@ -80,7 +85,7 @@ func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) func(w http.ResponseW
return return
} }
} })
} }
func PutStar(rsp http.ResponseWriter, req *http.Request, metaDB mTypes.MetaDB, log log.Logger) { 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/api/config"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types" mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/storage"
) )
func IsBuiltWithUserPrefsExtension() bool { func IsBuiltWithUserPrefsExtension() bool {
return false return false
} }
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController, func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger, metaDB mTypes.MetaDB, log log.Logger,
) { ) {
log.Warn().Msg("userprefs extension is disabled because given zot binary doesn't" + log.Warn().Msg("userprefs extension is disabled because given zot binary doesn't" +
"include this feature please build a binary that does so") "include this feature please build a binary that does so")

View file

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

View file

@ -1,5 +1,5 @@
//go:build sync || metrics || mgmt || apikey //go:build sync && metrics && mgmt && userprefs && search
// +build sync metrics mgmt apikey // +build sync,metrics,mgmt,userprefs,search
package extensions_test package extensions_test
@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"testing" "testing"
"time" "time"
@ -22,10 +21,6 @@ import (
"zotregistry.io/zot/pkg/extensions" "zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
syncconf "zotregistry.io/zot/pkg/extensions/config/sync" 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" "zotregistry.io/zot/pkg/test"
) )
@ -125,6 +120,7 @@ func TestMgmtExtension(t *testing.T) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
mgmtReadyTimeout := 5 * time.Second
defaultValue := true defaultValue := true
@ -142,16 +138,16 @@ func TestMgmtExtension(t *testing.T) {
mockOIDCConfig := mockOIDCServer.Config() 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() htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -168,19 +164,31 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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") found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
Convey("unsupported http method call", func() {
// without credentials // without credentials
resp, err := resty.R().Patch(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Patch(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed) So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
})
// without credentials // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err = resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -193,7 +201,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
// with credentials // 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(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -206,12 +214,12 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
// with wrong credentials // 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(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) 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{ conf.HTTP.Auth.LDAP = &config.LDAPConfig{
BindDN: "binddn", BindDN: "binddn",
BaseDN: "basedn", BaseDN: "basedn",
@ -219,11 +227,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -240,12 +248,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -258,7 +279,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil) 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() htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = &config.LDAPConfig{ conf.HTTP.Auth.LDAP = &config.LDAPConfig{
@ -268,11 +289,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -289,12 +310,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -307,7 +341,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil) So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
// with credentials // 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(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -320,7 +354,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil) 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() htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = &config.LDAPConfig{ conf.HTTP.Auth.LDAP = &config.LDAPConfig{
@ -335,11 +369,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -352,12 +386,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -372,7 +419,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service") So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
// with credentials // 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(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -387,7 +434,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service") 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.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = &config.LDAPConfig{ conf.HTTP.Auth.LDAP = &config.LDAPConfig{
BindDN: "binddn", BindDN: "binddn",
@ -401,11 +448,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -422,12 +469,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -442,7 +502,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service") 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.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = nil conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = &config.BearerConfig{ conf.HTTP.Auth.Bearer = &config.BearerConfig{
@ -451,11 +511,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -468,12 +528,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -487,7 +560,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service") 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.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = nil conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = nil conf.HTTP.Auth.Bearer = nil
@ -504,11 +577,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -521,12 +594,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -541,7 +627,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.OpenID.Providers, ShouldNotBeEmpty) 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() htpasswdPath := test.MakeHtpasswdFile()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
@ -555,11 +641,11 @@ func TestMgmtExtension(t *testing.T) {
} }
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -572,12 +658,25 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() 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 // without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -591,7 +690,7 @@ func TestMgmtExtension(t *testing.T) {
So(mgmtResp.HTTP.Auth.OpenID, ShouldBeNil) 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() globalDir := t.TempDir()
conf := config.New() conf := config.New()
port := test.GetFreePort() port := test.GetFreePort()
@ -605,11 +704,11 @@ func TestMgmtExtension(t *testing.T) {
conf.Commit = "v1.0.0" conf.Commit = "v1.0.0"
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name() conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup defer os.Remove(logFile.Name()) // cleanup
@ -622,7 +721,7 @@ func TestMgmtExtension(t *testing.T) {
ctlrManager.StartAndWait(port) ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer() defer ctlrManager.StopServer()
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) 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.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
data, _ := os.ReadFile(logFile.Name()) found, err := test.ReadLogFileAndSearchString(logFile.Name(),
So(string(data), ShouldContainSubstring, "setting up mgmt routes") "setting up mgmt routes", mgmtReadyTimeout)
}) defer func() {
if !found {
Convey("Verify mgmt route enabled for uploading certificates and public keys", t, func() { data, err := os.ReadFile(logFile.Name())
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) So(err, ShouldBeNil)
defaultValue := true t.Log(string(data))
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)
So(found, ShouldBeTrue) So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed", found, err = test.ReadLogFileAndSearchString(logFile.Name(),
time.Second) "finished setting up mgmt routes", mgmtReadyTimeout)
So(err, ShouldBeNil)
So(found, ShouldBeTrue) So(found, ShouldBeTrue)
So(err, ShouldBeNil)
}) })
} }
@ -816,11 +779,11 @@ func TestMgmtWithBearer(t *testing.T) {
defaultValue := true defaultValue := true
conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{ conf.Extensions.Search.Enable = &defaultValue
Enable: &defaultValue, conf.Extensions.Search.CVE = nil
}, conf.Extensions.UI = &extconf.UIConfig{}
} conf.Extensions.UI.Enable = &defaultValue
conf.Storage.RootDirectory = t.TempDir() conf.Storage.RootDirectory = t.TempDir()
@ -909,7 +872,7 @@ func TestMgmtWithBearer(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// test mgmt route // test mgmt route
resp, err = resty.R().Get(baseURL + constants.FullMgmtPrefix) resp, err = resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) 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.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, 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(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
@ -946,11 +909,13 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
conf := config.New() conf := config.New()
port := test.GetFreePort() port := test.GetFreePort()
conf.HTTP.Port = port conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{ conf.Extensions = &extconf.ExtensionConfig{}
Mgmt: &extconf.MgmtConfig{ conf.Extensions.Search = &extconf.SearchConfig{}
BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, conf.Extensions.Search.Enable = &defaultVal
}, conf.Extensions.Search.CVE = nil
} conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultVal
baseURL := test.GetBaseURL(port) baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
@ -961,38 +926,9 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
ctrlManager.StartAndWait(port) ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer() defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullMgmtPrefix) resp, _ := resty.R().Options(baseURL + constants.FullMgmt)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,OPTIONS") So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,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.StatusCode(), ShouldEqual, http.StatusNoContent) So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
}) })
} }

View file

@ -5,6 +5,9 @@ import (
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants" "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 { func GetExtensions(config *config.Config) distext.ExtensionList {
@ -13,18 +16,24 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
endpoints := []string{} endpoints := []string{}
extensions := []distext.Extension{} extensions := []distext.Extension{}
if config.Extensions != nil && config.Extensions.Search != nil { if config.IsNotationEnabled() && IsBuiltWithImageTrustExtension() {
if IsBuiltWithSearchExtension() { endpoints = append(endpoints, constants.FullNotation)
}
if config.IsCosignEnabled() && IsBuiltWithImageTrustExtension() {
endpoints = append(endpoints, constants.FullCosign)
}
if config.IsSearchEnabled() && IsBuiltWithSearchExtension() {
endpoints = append(endpoints, constants.FullSearchPrefix) endpoints = append(endpoints, constants.FullSearchPrefix)
} }
if IsBuiltWithUserPrefsExtension() { if config.AreUserPrefsEnabled() && IsBuiltWithUserPrefsExtension() {
endpoints = append(endpoints, constants.FullUserPreferencesPrefix) endpoints = append(endpoints, constants.FullUserPrefs)
}
} }
if IsBuiltWithMGMTExtension() && config.Extensions != nil && config.Extensions.Mgmt != nil { if config.IsMgmtEnabled() && IsBuiltWithMGMTExtension() {
endpoints = append(endpoints, constants.FullMgmtPrefix) endpoints = append(endpoints, constants.FullMgmt)
} }
if len(endpoints) > 0 { if len(endpoints) > 0 {
@ -40,3 +49,9 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
return 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 defaultVal := true
searchConfig := &extconf.SearchConfig{ conf.Extensions = &extconf.ExtensionConfig{}
BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, conf.Extensions.Search = &extconf.SearchConfig{}
} conf.Extensions.Search.Enable = &defaultVal
conf.Extensions.Search.CVE = nil
mgmtConfg := &extconf.MgmtConfig{ conf.Extensions.UI = &extconf.UIConfig{}
BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, conf.Extensions.UI.Enable = &defaultVal
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
Mgmt: mgmtConfg,
}
logFile, err := os.CreateTemp("", "zot-log*.txt") logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil) So(err, ShouldBeNil)

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package signatures package signatures
import ( import (
"context"
"encoding/json" "encoding/json"
"time" "time"
@ -8,6 +9,9 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
) )
const ( const (
@ -58,3 +62,84 @@ func VerifySignature(
return "", time.Time{}, false, zerr.ErrInvalidSignatureType 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/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"golang.org/x/crypto/bcrypt"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
"oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote"
@ -126,6 +127,17 @@ func MakeHtpasswdFile() string {
return MakeHtpasswdFileFromString(content) 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 { func MakeHtpasswdFileFromString(fileContent string) string {
htpasswdFile, err := os.CreateTemp("", "htpasswd-") htpasswdFile, err := os.CreateTemp("", "htpasswd-")
if err != nil { if err != nil {

View file

@ -19,7 +19,6 @@ import (
"github.com/opencontainers/image-spec/specs-go" "github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"golang.org/x/crypto/bcrypt"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
@ -610,7 +609,7 @@ func TestUploadImage(t *testing.T) {
user1 := "test" user1 := "test"
password1 := "test" password1 := "test"
testString1 := getCredString(user1, password1) testString1 := test.GetCredString(user1, password1)
htpasswdPath := test.MakeHtpasswdFileFromString(testString1) htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
defer os.Remove(htpasswdPath) defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{ 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) { func TestInjectUploadImage(t *testing.T) {
Convey("Inject failures for unreachable lines", t, func() { Convey("Inject failures for unreachable lines", t, func() {
port := test.GetFreePort() port := test.GetFreePort()
@ -909,7 +897,7 @@ func TestInjectUploadImageWithBasicAuth(t *testing.T) {
user := "user" user := "user"
password := "password" password := "password"
testString := getCredString(user, password) testString := test.GetCredString(user, password)
htpasswdPath := test.MakeHtpasswdFileFromString(testString) htpasswdPath := test.MakeHtpasswdFileFromString(testString)
defer os.Remove(htpasswdPath) defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{ conf.HTTP.Auth = &config.AuthConfig{

View file

@ -20,6 +20,126 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "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": { "/oras/artifacts/v1/{name}/manifests/{digest}/referrers": {
"get": { "get": {
"description": "Get references for an image given a digest and artifact type", "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": { "/v2/_zot/ext/mgmt": {
"get": { "get": {
"description": "Get current server configuration", "description": "Get current server configuration",
@ -176,38 +339,19 @@ const docTemplate = `{
} }
} }
} }
}
}, },
"/v2/_zot/ext/notation": {
"post": { "post": {
"description": "Upload certificates and public keys for verifying signatures", "description": "Upload notation certificates for verifying signatures",
"consumes": [ "consumes": [
"application/octet-stream" "application/octet-stream"
], ],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"summary": "Upload certificates and public keys for verifying signatures", "summary": "Upload notation certificates for verifying signatures",
"parameters": [ "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", "type": "string",
"description": "truststore type", "description": "truststore type",
@ -221,7 +365,7 @@ const docTemplate = `{
"in": "query" "in": "query"
}, },
{ {
"description": "Public key or Certificate content", "description": "Certificate content",
"name": "requestBody", "name": "requestBody",
"in": "body", "in": "body",
"required": true, "required": true,
@ -992,6 +1136,20 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"api.APIKeyPayload": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"scopes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"api.ExtensionList": { "api.ExtensionList": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1013,6 +1171,10 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
}, },
"artifactType": {
"description": "ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact.",
"type": "string"
},
"manifests": { "manifests": {
"description": "Manifests references platform specific manifests.", "description": "Manifests references platform specific manifests.",
"type": "array", "type": "array",
@ -1027,6 +1189,14 @@ const docTemplate = `{
"schemaVersion": { "schemaVersion": {
"description": "SchemaVersion is the image manifest schema that this image follows", "description": "SchemaVersion is the image manifest schema that this image follows",
"type": "integer" "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" "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": { "extensions.StrippedConfig": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -11,6 +11,126 @@
"version": "v1.1.0-dev" "version": "v1.1.0-dev"
}, },
"paths": { "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": { "/oras/artifacts/v1/{name}/manifests/{digest}/referrers": {
"get": { "get": {
"description": "Get references for an image given a digest and artifact type", "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": { "/v2/_zot/ext/mgmt": {
"get": { "get": {
"description": "Get current server configuration", "description": "Get current server configuration",
@ -167,38 +330,19 @@
} }
} }
} }
}
}, },
"/v2/_zot/ext/notation": {
"post": { "post": {
"description": "Upload certificates and public keys for verifying signatures", "description": "Upload notation certificates for verifying signatures",
"consumes": [ "consumes": [
"application/octet-stream" "application/octet-stream"
], ],
"produces": [ "produces": [
"application/json" "application/json"
], ],
"summary": "Upload certificates and public keys for verifying signatures", "summary": "Upload notation certificates for verifying signatures",
"parameters": [ "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", "type": "string",
"description": "truststore type", "description": "truststore type",
@ -212,7 +356,7 @@
"in": "query" "in": "query"
}, },
{ {
"description": "Public key or Certificate content", "description": "Certificate content",
"name": "requestBody", "name": "requestBody",
"in": "body", "in": "body",
"required": true, "required": true,
@ -983,6 +1127,20 @@
} }
}, },
"definitions": { "definitions": {
"api.APIKeyPayload": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"scopes": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"api.ExtensionList": { "api.ExtensionList": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1004,6 +1162,10 @@
"type": "string" "type": "string"
} }
}, },
"artifactType": {
"description": "ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact.",
"type": "string"
},
"manifests": { "manifests": {
"description": "Manifests references platform specific manifests.", "description": "Manifests references platform specific manifests.",
"type": "array", "type": "array",
@ -1018,6 +1180,14 @@
"schemaVersion": { "schemaVersion": {
"description": "SchemaVersion is the image manifest schema that this image follows", "description": "SchemaVersion is the image manifest schema that this image follows",
"type": "integer" "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" "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": { "extensions.StrippedConfig": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -1,4 +1,13 @@
definitions: definitions:
api.APIKeyPayload:
properties:
label:
type: string
scopes:
items:
type: string
type: array
type: object
api.ExtensionList: api.ExtensionList:
properties: properties:
extensions: extensions:
@ -13,6 +22,10 @@ definitions:
type: string type: string
description: Annotations contains arbitrary metadata for the image index. description: Annotations contains arbitrary metadata for the image index.
type: object type: object
artifactType:
description: ArtifactType specifies the IANA media type of artifact when the
manifest is used for an artifact.
type: string
manifests: manifests:
description: Manifests references platform specific manifests. description: Manifests references platform specific manifests.
items: items:
@ -25,6 +38,12 @@ definitions:
schemaVersion: schemaVersion:
description: SchemaVersion is the image manifest schema that this image follows description: SchemaVersion is the image manifest schema that this image follows
type: integer 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 type: object
api.ImageManifest: api.ImageManifest:
properties: properties:
@ -89,6 +108,8 @@ definitions:
address: address:
type: string type: string
type: object type: object
openid:
$ref: '#/definitions/extensions.OpenIDConfig'
type: object type: object
extensions.BearerConfig: extensions.BearerConfig:
properties: properties:
@ -115,6 +136,15 @@ definitions:
path: path:
type: string type: string
type: object type: object
extensions.OpenIDConfig:
properties:
providers:
additionalProperties:
$ref: '#/definitions/extensions.OpenIDProviderConfig'
type: object
type: object
extensions.OpenIDProviderConfig:
type: object
extensions.StrippedConfig: extensions.StrippedConfig:
properties: properties:
binaryType: binaryType:
@ -206,6 +236,86 @@ info:
title: Open Container Initiative Distribution Specification title: Open Container Initiative Distribution Specification
version: v1.1.0-dev version: v1.1.0-dev
paths: 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: /oras/artifacts/v1/{name}/manifests/{digest}/referrers:
get: get:
consumes: consumes:
@ -286,6 +396,34 @@ paths:
schema: schema:
$ref: '#/definitions/api.ExtensionList' $ref: '#/definitions/api.ExtensionList'
summary: List Registry level extensions 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: /v2/_zot/ext/mgmt:
get: get:
consumes: consumes:
@ -310,26 +448,12 @@ paths:
schema: schema:
type: string type: string
summary: Get current server configuration summary: Get current server configuration
/v2/_zot/ext/notation:
post: post:
consumes: consumes:
- application/octet-stream - application/octet-stream
description: Upload certificates and public keys for verifying signatures description: Upload notation certificates for verifying signatures
parameters: 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 - description: truststore type
in: query in: query
name: truststoreType name: truststoreType
@ -338,7 +462,7 @@ paths:
in: query in: query
name: truststoreName name: truststoreName
type: string type: string
- description: Public key or Certificate content - description: Certificate content
in: body in: body
name: requestBody name: requestBody
required: true required: true
@ -359,7 +483,7 @@ paths:
description: internal server error". description: internal server error".
schema: schema:
type: string type: string
summary: Upload certificates and public keys for verifying signatures summary: Upload notation certificates for verifying signatures
/v2/_zot/ext/userprefs: /v2/_zot/ext/userprefs:
put: put:
consumes: consumes:

View file

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