mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
refactor(extensions)!: refactor the extensions URLs and errors (#1636)
BREAKING CHANGE: The functionality provided by the mgmt endpoint has beed redesigned - see details below BREAKING CHANGE: The API keys endpoint has been moved - see details below BREAKING CHANGE: The mgmt extension config has been removed - endpoint is now enabled by having both the search and the ui extensions enabled BREAKING CHANGE: The API keys configuration has been moved from extensions to http>auth>apikey mgmt and imagetrust extensions: - separate the _zot/ext/mgmt into 3 separate endpoints: _zot/ext/auth, _zot/ext/notation, _zot/ext/cosign - signature verification logic is in a separate `imagetrust` extension - better hanling or errors in case of signature uploads: logging and error codes (more 400 and less 500 errors) - add authz on signature uploads (and add a new middleware in common for this purpose) - remove the mgmt extension configuration - it is now enabled if the UI and the search extensions are enabled userprefs estension: - userprefs are enabled if both search and ui extensions are enabled (as opposed to just search) apikey extension is removed and logic moved into the api folder - Move apikeys code out of pkg/extensions and into pkg/api - Remove apikey configuration options from the extensions configuration and move it inside the http auth section - remove the build label apikeys other changes: - move most of the logic adding handlers to the extensions endpoints out of routes.go and into the extensions files. - add warnings in case the users are still using configurations with the obsolete settings for mgmt and api keys - add a new function in the extension package which could be a single point of starting backgroud tasks for all extensions - more clear methods for verifying specific extensions are enabled - fix http methods paired with the UI handlers - rebuild swagger docs Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
parent
42f9f78125
commit
77149aa85c
61 changed files with 3405 additions and 1471 deletions
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
|||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOFLAGS: "-tags=sync,search,scrub,metrics,userprefs,apikey,containers_image_openpgp"
|
||||
GOFLAGS: "-tags=sync,search,scrub,metrics,userprefs,mgmt,imagetrust,containers_image_openpgp"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
2
.github/workflows/golangci-lint.yaml
vendored
2
.github/workflows/golangci-lint.yaml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,userprefs,metrics,containers_image_openpgp,lint,mgmt,apikey ./cmd/... ./pkg/...
|
||||
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,userprefs,metrics,containers_image_openpgp,lint,mgmt,imagetrust ./cmd/... ./pkg/...
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -29,4 +29,5 @@ coverage.html
|
|||
tags
|
||||
vendor/
|
||||
.vscode/
|
||||
examples/config-sync-localhost.json
|
||||
examples/config-sync-localhost.json
|
||||
node_modules
|
||||
|
|
4
Makefile
4
Makefile
|
@ -32,8 +32,8 @@ TESTDATA := $(TOP_LEVEL)/test/data
|
|||
OS ?= linux
|
||||
ARCH ?= amd64
|
||||
BENCH_OUTPUT ?= stdout
|
||||
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,apikey
|
||||
UI_DEPENDENCIES := search,mgmt,userprefs,apikey
|
||||
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt,userprefs,imagetrust
|
||||
UI_DEPENDENCIES := search,mgmt,userprefs
|
||||
# freebsd/arm64 not supported for pie builds
|
||||
BUILDMODE_FLAGS := -buildmode=pie
|
||||
ifeq ($(OS),freebsd)
|
||||
|
|
|
@ -107,6 +107,7 @@ var (
|
|||
ErrInvalidTruststoreType = errors.New("signatures: invalid truststore type")
|
||||
ErrInvalidTruststoreName = errors.New("signatures: invalid truststore name")
|
||||
ErrInvalidCertificateContent = errors.New("signatures: invalid certificate content")
|
||||
ErrInvalidPublicKeyContent = errors.New("signatures: invalid public key content")
|
||||
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
|
||||
ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones")
|
||||
)
|
||||
|
|
|
@ -207,12 +207,12 @@ zot can be configured to use the above providers with:
|
|||
}
|
||||
```
|
||||
|
||||
the login with either provider use http://127.0.0.1:8080/auth/login?provider=\<provider\>&callback_ui=http://127.0.0.1:8080/home
|
||||
The login with either provider use http://127.0.0.1:8080/auth/login?provider=\<provider\>&callback_ui=http://127.0.0.1:8080/home
|
||||
for example to login with github use http://127.0.0.1:8080/auth/login?provider=github&callback_ui=http://127.0.0.1:8080/home
|
||||
|
||||
callback_ui query parameter is used by zot to redirect to UI after a successful openid/oauth2 authentication
|
||||
|
||||
the callback url which should be used when making oauth2 provider setup is http://127.0.0.1:8080/auth/callback/\<provider\>
|
||||
The callback url which should be used when making oauth2 provider setup is http://127.0.0.1:8080/auth/callback/\<provider\>
|
||||
for example github callback url would be http://127.0.0.1:8080/auth/callback/github
|
||||
|
||||
If network policy doesn't allow inbound connections, this callback wont work!
|
||||
|
@ -220,7 +220,7 @@ If network policy doesn't allow inbound connections, this callback wont work!
|
|||
dex is an identity service that uses OpenID Connect to drive authentication for other apps https://github.com/dexidp/dex
|
||||
To setup dex service see https://dexidp.io/docs/getting-started/
|
||||
|
||||
to configure zot as a client in dex (assuming zot is hosted at 127.0.0.1:8080), we need to configure dex with:
|
||||
To configure zot as a client in dex (assuming zot is hosted at 127.0.0.1:8080), we need to configure dex with:
|
||||
|
||||
```
|
||||
staticClients:
|
||||
|
@ -251,7 +251,12 @@ zot can be configured to use dex with:
|
|||
}
|
||||
```
|
||||
|
||||
to login using openid dex provider use http://127.0.0.1:8080/auth/login?provider=dex
|
||||
To login using openid dex provider use http://127.0.0.1:8080/auth/login?provider=dex
|
||||
|
||||
NOTE: Social login is not supported by command line tools, or other software responsible for pushing/pulling
|
||||
images to/from zot.
|
||||
Given this limitation, if openif authentication is enabled in the configuration, API keys are also enabled
|
||||
implicitly, as a viable alternative authentication method for pushing and pulling container images.
|
||||
|
||||
### Session based login
|
||||
|
||||
|
@ -261,6 +266,90 @@ Using that cookie on subsequent calls will authenticate them, asumming the cooki
|
|||
In case of using filesystem storage sessions are saved in zot's root directory.
|
||||
In case of using cloud storage sessions are saved in memory.
|
||||
|
||||
#### API keys
|
||||
|
||||
zot allows authentication for REST API calls using your API key as an alternative to your password.
|
||||
The user can create or revoke his API keys after he has already authenticated using a different authentication mechanism.
|
||||
An API key is shown to the user only when it is created. It can not be retrieved from zot with any other call.
|
||||
An API key has the same permissions as the user who generated it.
|
||||
|
||||
Below are several use cases where API keys offer advantages:
|
||||
|
||||
- OpenID/OAuth2 social login is not supported by command-line tools or other such clients. In this case, the user
|
||||
can login to zot using OpenID/OAuth2 and generate API keys to use later when pushing and pulling images.
|
||||
- In cases where LDAP authentication is used and the user has scripts pushing or pulling images, he will probably not
|
||||
want to store his LDAP username and password in a shared environment where there is a chance they are compromised.
|
||||
If he generates and uses an API key instead, the security impact of that key being compromised is limited to zot,
|
||||
the other services he accesses based on LDAP would not be affected.
|
||||
|
||||
To activate API keys use:
|
||||
|
||||
```
|
||||
"http": {
|
||||
"auth": {
|
||||
"apikey: true
|
||||
```
|
||||
|
||||
##### How to create an API Key
|
||||
|
||||
Create an API key for the current user using the REST API
|
||||
|
||||
**Usage**: POST /auth/apikey
|
||||
|
||||
**Produces**: application/json
|
||||
|
||||
**Sample input**:
|
||||
|
||||
```
|
||||
POST /auth/apiKey
|
||||
Body: {"label": "git", "scopes": ["repo1", "repo2"]}'
|
||||
```
|
||||
|
||||
**Example cURL**
|
||||
|
||||
```bash
|
||||
curl -u user:password -X POST http://localhost:8080/auth/apikey -d '{"label": "myLabel"}'
|
||||
```
|
||||
|
||||
**Sample output**:
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": "2023-05-05T15:39:28.420926+03:00",
|
||||
"creatorUa": "curl/7.68.0",
|
||||
"generatedBy": "manual",
|
||||
"lastUsed": "2023-05-05T15:39:28.4209282+03:00",
|
||||
"label": "git",
|
||||
"scopes": null,
|
||||
"uuid": "46a45ce7-5d92-498a-a9cb-9654b1da3da1",
|
||||
"apiKey": "zak_e77bcb9e9f634f1581756abbf9ecd269"
|
||||
}
|
||||
```
|
||||
|
||||
##### How to use API Keys
|
||||
|
||||
**Using API keys with cURL**
|
||||
|
||||
```bash
|
||||
curl -u user:zak_e77bcb9e9f634f1581756abbf9ecd269 http://localhost:8080/v2/_catalog
|
||||
```
|
||||
|
||||
Other command line tools will similarly accept the API key instead of a password.
|
||||
|
||||
##### How to revoke an API Key
|
||||
|
||||
How to revoke an API key for the current user
|
||||
|
||||
**Usage**: DELETE /auth/apiKey?id=$uuid
|
||||
|
||||
**Produces**: application/json
|
||||
|
||||
**Example cURL**
|
||||
|
||||
```bash
|
||||
curl -u user:password -X DELETE http://localhost:8080/v2/auth/apikey?id=46a45ce7-5d92-498a-a9cb-9654b1da3da1
|
||||
```
|
||||
|
||||
#### Authentication Failures
|
||||
|
||||
Should authentication fail, to prevent automated attacks, a delayed response can be configured with:
|
||||
|
@ -271,21 +360,6 @@ Should authentication fail, to prevent automated attacks, a delayed response can
|
|||
"failDelay": 5
|
||||
```
|
||||
|
||||
#### API keys
|
||||
|
||||
zot allows authentication for REST API calls using your API key as an alternative to your password.
|
||||
for more info see [API keys doc](../pkg/extensions/README_apikey.md)
|
||||
|
||||
To activate API keys use:
|
||||
|
||||
```
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Identity-based Authorization
|
||||
|
||||
Allowing actions on one or more repository paths can be tied to user
|
||||
|
|
|
@ -48,9 +48,6 @@
|
|||
"scrub": {
|
||||
"enable": true,
|
||||
"interval": "24h"
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"htpasswd": {
|
||||
"path": "test/data/htpasswd"
|
||||
},
|
||||
"apikey": true,
|
||||
"openid": {
|
||||
"providers": {
|
||||
"github": {
|
||||
|
@ -64,12 +65,5 @@
|
|||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": true
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
"extensions": {}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,6 @@
|
|||
},
|
||||
"ui": {
|
||||
"enable": true
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/chartmuseum/auth"
|
||||
guuid "github.com/gofrs/uuid"
|
||||
"github.com/google/go-github/v52/github"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -353,7 +354,7 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
|||
return
|
||||
}
|
||||
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmt
|
||||
allowAnonymous := ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists()
|
||||
|
||||
// try basic auth if authorization header is given
|
||||
|
@ -443,7 +444,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
name := vars["name"]
|
||||
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmt
|
||||
|
||||
header := request.Header.Get("Authorization")
|
||||
|
||||
|
@ -849,3 +850,25 @@ func GetAuthUserFromRequestSession(cookieStore sessions.Store, request *http.Req
|
|||
|
||||
return identity, true
|
||||
}
|
||||
|
||||
func GenerateAPIKey(uuidGenerator guuid.Generator, log log.Logger,
|
||||
) (string, string, error) {
|
||||
apiKeyBase, err := uuidGenerator.NewV4()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to generate uuid for api key base")
|
||||
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
apiKey := strings.ReplaceAll(apiKeyBase.String(), "-", "")
|
||||
|
||||
// will be used for identifying a specific api key
|
||||
apiKeyID, err := uuidGenerator.NewV4()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to generate uuid for api key id")
|
||||
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return apiKey, apiKeyID.String(), err
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
//go:build apikey
|
||||
// +build apikey
|
||||
//go:build mgmt
|
||||
// +build mgmt
|
||||
|
||||
package extensions_test
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
guuid "github.com/gofrs/uuid"
|
||||
"github.com/project-zot/mockoidc"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
|
@ -18,14 +21,16 @@ import (
|
|||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
"zotregistry.io/zot/pkg/test/mocks"
|
||||
)
|
||||
|
||||
var ErrUnexpectedError = errors.New("error: unexpected error")
|
||||
|
||||
type (
|
||||
apiKeyResponse struct {
|
||||
mTypes.APIKeyDetails
|
||||
|
@ -33,7 +38,30 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
var ErrUnexpectedError = errors.New("unexpected err")
|
||||
func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
|
||||
defaultVal := true
|
||||
|
||||
Convey("Test http options response", t, func() {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
conf.HTTP.Port = port
|
||||
conf.HTTP.Auth.APIKey = defaultVal
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctrlManager := test.NewControllerManager(ctlr)
|
||||
|
||||
ctrlManager.StartAndWait(port)
|
||||
defer ctrlManager.StopServer()
|
||||
|
||||
resp, _ := resty.R().Options(baseURL + constants.APIKeyPath)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,DELETE,OPTIONS")
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIKeys(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
|
@ -59,6 +87,8 @@ func TestAPIKeys(t *testing.T) {
|
|||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
defaultVal := true
|
||||
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
|
@ -74,23 +104,17 @@ func TestAPIKeys(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
APIKey: defaultVal,
|
||||
}
|
||||
|
||||
conf.HTTP.AccessControl = &config.AccessControlConfig{}
|
||||
|
||||
defaultVal := true
|
||||
apiKeyConfig := &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: apiKeyConfig,
|
||||
Mgmt: mgmtConfg,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
dir := t.TempDir()
|
||||
|
@ -103,7 +127,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
defer cm.StopServer()
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
payload := extensions.APIKeyPayload{
|
||||
payload := api.APIKeyPayload{
|
||||
Label: "test",
|
||||
Scopes: []string{"test"},
|
||||
}
|
||||
|
@ -115,7 +139,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err := resty.R().
|
||||
SetBody(reqBody).
|
||||
SetBasicAuth("test", "test").
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
@ -141,7 +165,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = resty.R().
|
||||
SetBody(reqBody).
|
||||
SetBasicAuth("test", "test").
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
@ -175,7 +199,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
@ -186,7 +210,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
@ -260,7 +284,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
@ -270,8 +294,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
client := resty.New()
|
||||
|
||||
// mgmt should work both unauthenticated and authenticated
|
||||
resp, err := client.R().
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := client.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -292,7 +315,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
@ -326,7 +349,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -334,7 +357,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
// invalid api keys
|
||||
resp, err = client.R().
|
||||
SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
@ -366,7 +389,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
@ -375,8 +398,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
||||
|
||||
// without creds should work
|
||||
resp, err = client.R().
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = client.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -395,7 +417,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetBody(reqBody).
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Post(baseURL + constants.FullAPIKeyPrefix)
|
||||
Post(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
@ -406,7 +428,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
// should work with session
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -414,7 +436,7 @@ func TestAPIKeys(t *testing.T) {
|
|||
// should work with api key
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(email, apiKeyResponse.APIKey).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -433,14 +455,14 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
SetQueryParam("id", apiKeyResponse.UUID).
|
||||
Delete(baseURL + constants.FullAPIKeyPrefix)
|
||||
Delete(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Delete(baseURL + constants.FullAPIKeyPrefix)
|
||||
Delete(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
@ -455,18 +477,31 @@ func TestAPIKeys(t *testing.T) {
|
|||
resp, err = client.R().
|
||||
SetBasicAuth("test", "test").
|
||||
SetQueryParam("id", apiKeyResponse.UUID).
|
||||
Delete(baseURL + constants.FullAPIKeyPrefix)
|
||||
Delete(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// unsupported method
|
||||
resp, err = client.R().
|
||||
Put(baseURL + constants.FullAPIKeyPrefix)
|
||||
Put(baseURL + constants.APIKeyPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
||||
})
|
||||
|
||||
Convey("Test error handling when API Key handler reads the request body", func() {
|
||||
request, _ := http.NewRequestWithContext(context.TODO(),
|
||||
http.MethodPost, "baseURL", errReader(0))
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
rthdlr := api.NewRouteHandler(ctlr)
|
||||
rthdlr.CreateAPIKey(response, request)
|
||||
|
||||
resp := response.Result()
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -489,6 +524,8 @@ func TestAPIKeysOpenDBError(t *testing.T) {
|
|||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
defaultVal := true
|
||||
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
|
@ -505,14 +542,8 @@ func TestAPIKeysOpenDBError(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultVal := true
|
||||
apiKeyConfig := &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: apiKeyConfig,
|
||||
APIKey: defaultVal,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
@ -529,3 +560,58 @@ func TestAPIKeysOpenDBError(t *testing.T) {
|
|||
}, ShouldPanic)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIKeysGeneratorErrors(t *testing.T) {
|
||||
Convey("Test API keys - unable to generate API keys and API Key IDs", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
|
||||
apiKey, apiKeyID, err := api.GenerateAPIKey(guuid.DefaultGenerator, log)
|
||||
So(err, ShouldBeNil)
|
||||
So(apiKey, ShouldNotEqual, "")
|
||||
So(apiKeyID, ShouldNotEqual, "")
|
||||
|
||||
generator := &mockUUIDGenerator{
|
||||
guuid.DefaultGenerator, 0, 0,
|
||||
}
|
||||
|
||||
apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(apiKey, ShouldEqual, "")
|
||||
So(apiKeyID, ShouldEqual, "")
|
||||
|
||||
generator = &mockUUIDGenerator{
|
||||
guuid.DefaultGenerator, 1, 0,
|
||||
}
|
||||
|
||||
apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(apiKey, ShouldEqual, "")
|
||||
So(apiKeyID, ShouldEqual, "")
|
||||
})
|
||||
}
|
||||
|
||||
type mockUUIDGenerator struct {
|
||||
guuid.Generator
|
||||
succeedAttempts int
|
||||
attemptCount int
|
||||
}
|
||||
|
||||
func (gen *mockUUIDGenerator) NewV4() (
|
||||
guuid.UUID, error,
|
||||
) {
|
||||
defer func() {
|
||||
gen.attemptCount += 1
|
||||
}()
|
||||
|
||||
if gen.attemptCount >= gen.succeedAttempts {
|
||||
return guuid.UUID{}, ErrUnexpectedError
|
||||
}
|
||||
|
||||
return guuid.DefaultGenerator.NewV4()
|
||||
}
|
||||
|
||||
type errReader int
|
||||
|
||||
func (errReader) Read(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("test error") //nolint:goerr113
|
||||
}
|
|
@ -50,6 +50,7 @@ type AuthConfig struct {
|
|||
LDAP *LDAPConfig
|
||||
Bearer *BearerConfig
|
||||
OpenID *OpenIDConfig
|
||||
APIKey bool
|
||||
}
|
||||
|
||||
type BearerConfig struct {
|
||||
|
@ -274,8 +275,7 @@ func (c *Config) IsOpenIDAuthEnabled() bool {
|
|||
}
|
||||
|
||||
func (c *Config) IsAPIKeyEnabled() bool {
|
||||
if c.Extensions != nil && c.Extensions.APIKey != nil &&
|
||||
*c.Extensions.APIKey.Enable {
|
||||
if c.HTTP.Auth != nil && c.HTTP.Auth.APIKey {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -308,6 +308,38 @@ func isOpenIDAuthProviderEnabled(config *Config, provider string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *Config) IsSearchEnabled() bool {
|
||||
return c.Extensions != nil && c.Extensions.Search != nil && *c.Extensions.Search.Enable
|
||||
}
|
||||
|
||||
func (c *Config) IsUIEnabled() bool {
|
||||
return c.Extensions != nil && c.Extensions.UI != nil && *c.Extensions.UI.Enable
|
||||
}
|
||||
|
||||
func (c *Config) AreUserPrefsEnabled() bool {
|
||||
return c.IsSearchEnabled() && c.IsUIEnabled()
|
||||
}
|
||||
|
||||
func (c *Config) IsMgmtEnabled() bool {
|
||||
return c.IsSearchEnabled() && c.IsUIEnabled()
|
||||
}
|
||||
|
||||
func (c *Config) IsImageTrustEnabled() bool {
|
||||
return c.Extensions != nil && c.Extensions.Trust != nil && *c.Extensions.Trust.Enable
|
||||
}
|
||||
|
||||
func (c *Config) IsCosignEnabled() bool {
|
||||
return c.IsImageTrustEnabled() && c.Extensions.Trust.Cosign
|
||||
}
|
||||
|
||||
func (c *Config) IsNotationEnabled() bool {
|
||||
return c.IsImageTrustEnabled() && c.Extensions.Trust.Notation
|
||||
}
|
||||
|
||||
func (c *Config) IsSyncEnabled() bool {
|
||||
return c.Extensions != nil && c.Extensions.Sync != nil && *c.Extensions.Sync.Enable
|
||||
}
|
||||
|
||||
func IsOpenIDSupported(provider string) bool {
|
||||
for _, supportedProvider := range openIDSupportedProviders {
|
||||
if supportedProvider == provider {
|
||||
|
|
|
@ -15,6 +15,7 @@ const (
|
|||
CallbackBasePath = "/auth/callback"
|
||||
LoginPath = "/auth/login"
|
||||
LogoutPath = "/auth/logout"
|
||||
APIKeyPath = "/auth/apikey" //nolint: gosec
|
||||
SessionClientHeaderName = "X-ZOT-API-CLIENT"
|
||||
SessionClientHeaderValue = "zot-ui"
|
||||
APIKeysPrefix = "zak_"
|
||||
|
|
|
@ -4,21 +4,31 @@ package constants
|
|||
const (
|
||||
ExtCatalogPrefix = "/_catalog"
|
||||
ExtOciDiscoverPrefix = "/_oci/ext/discover"
|
||||
// zot specific extensions.
|
||||
ExtPrefix = "/_zot/ext"
|
||||
|
||||
// zot specific extensions.
|
||||
BasePrefix = "/_zot"
|
||||
ExtPrefix = BasePrefix + "/ext"
|
||||
|
||||
// search extension.
|
||||
ExtSearch = "/search"
|
||||
ExtSearchPrefix = ExtPrefix + ExtSearch
|
||||
FullSearchPrefix = RoutePrefix + ExtSearchPrefix
|
||||
|
||||
ExtMgmt = "/mgmt"
|
||||
ExtMgmtPrefix = ExtPrefix + ExtMgmt
|
||||
FullMgmtPrefix = RoutePrefix + ExtMgmtPrefix
|
||||
// mgmt extension.
|
||||
Mgmt = "/mgmt"
|
||||
ExtMgmt = ExtPrefix + Mgmt
|
||||
FullMgmt = RoutePrefix + ExtMgmt
|
||||
|
||||
ExtUserPreferences = "/userprefs"
|
||||
ExtUserPreferencesPrefix = ExtPrefix + ExtUserPreferences
|
||||
FullUserPreferencesPrefix = RoutePrefix + ExtUserPreferencesPrefix
|
||||
ExtAPIKey = "/apikey"
|
||||
ExtAPIKeyPrefix = ExtPrefix + ExtAPIKey //nolint: gosec
|
||||
FullAPIKeyPrefix = RoutePrefix + ExtAPIKeyPrefix
|
||||
// signatures extension.
|
||||
Notation = "/notation"
|
||||
ExtNotation = ExtPrefix + Notation
|
||||
FullNotation = RoutePrefix + ExtNotation
|
||||
Cosign = "/cosign"
|
||||
ExtCosign = ExtPrefix + Cosign
|
||||
FullCosign = RoutePrefix + ExtCosign
|
||||
|
||||
// user preferences extension.
|
||||
UserPrefs = "/userprefs"
|
||||
ExtUserPrefs = ExtPrefix + UserPrefs
|
||||
FullUserPrefs = RoutePrefix + ExtUserPrefs
|
||||
)
|
||||
|
|
|
@ -258,9 +258,8 @@ func (c *Controller) InitImageStore() error {
|
|||
}
|
||||
|
||||
func (c *Controller) InitMetaDB(reloadCtx context.Context) error {
|
||||
// init metaDB if search is enabled or authn enabled (need to store user profiles) or apikey ext is enabled
|
||||
if (c.Config.Extensions != nil && c.Config.Extensions.Search != nil && *c.Config.Extensions.Search.Enable) ||
|
||||
c.Config.IsBasicAuthnEnabled() {
|
||||
// init metaDB if search is enabled or we need to store user profiles, api keys or signatures
|
||||
if c.Config.IsSearchEnabled() || c.Config.IsBasicAuthnEnabled() || c.Config.IsImageTrustEnabled() {
|
||||
driver, err := meta.New(c.Config.Storage.StorageConfig, c.Log) //nolint:contextcheck
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -368,11 +367,8 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
|
|||
c.SyncOnDemand = syncOnDemand
|
||||
}
|
||||
|
||||
if c.Config.Extensions != nil {
|
||||
if c.Config.Extensions.Mgmt != nil && *c.Config.Extensions.Mgmt.Enable {
|
||||
ext.EnablePeriodicSignaturesVerification(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
|
||||
}
|
||||
}
|
||||
// we can later move enabling the other scheduled tasks inside the call below
|
||||
ext.EnableScheduledTasks(c.Config, taskScheduler, c.MetaDB, c.Log) //nolint: contextcheck
|
||||
}
|
||||
|
||||
type SyncOnDemand interface {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build sync && scrub && metrics && search && lint && apikey && mgmt
|
||||
// +build sync,scrub,metrics,search,lint,apikey,mgmt
|
||||
//go:build sync && scrub && metrics && search && lint && userprefs && mgmt && imagetrust && ui
|
||||
// +build sync,scrub,metrics,search,lint,userprefs,mgmt,imagetrust,ui
|
||||
|
||||
package api_test
|
||||
|
||||
|
@ -2659,12 +2659,18 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
// UI is enabled because we also want to test access on the mgmt route
|
||||
uiConfig := &extconf.UIConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Mgmt: mgmtConfg,
|
||||
Search: searchConfig,
|
||||
UI: uiConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
@ -2769,7 +2775,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -2778,7 +2784,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(htpasswdUsername, passphrase).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -2795,7 +2801,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -2835,7 +2841,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -2844,7 +2850,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
|
||||
resp, err = client.R().
|
||||
SetBasicAuth(username, passphrase).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -2861,7 +2867,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
|
||||
resp, err = client.R().
|
||||
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -2888,7 +2894,7 @@ func TestOpenIDMiddleware(t *testing.T) {
|
|||
|
||||
// mgmt should work both unauthenticated and authenticated
|
||||
resp, err := client.R().
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
@ -3063,12 +3069,17 @@ func TestAuthnSessionErrors(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
uiConfig := &extconf.UIConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Mgmt: mgmtConfg,
|
||||
UI: uiConfig,
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
@ -8391,7 +8402,7 @@ func TestSearchRoutes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDistSpecExtensions(t *testing.T) {
|
||||
Convey("start zot server with search extension", t, func(c C) {
|
||||
Convey("start zot server with search, ui and trust extensions", t, func(c C) {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
@ -8400,13 +8411,16 @@ func TestDistSpecExtensions(t *testing.T) {
|
|||
|
||||
defaultVal := true
|
||||
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
|
||||
conf.Extensions.Trust.Enable = &defaultVal
|
||||
conf.Extensions.Trust.Cosign = defaultVal
|
||||
conf.Extensions.Trust.Notation = defaultVal
|
||||
|
||||
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -8427,16 +8441,23 @@ func TestDistSpecExtensions(t *testing.T) {
|
|||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
err = json.Unmarshal(resp.Body(), &extensionList)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(extensionList.Extensions)
|
||||
So(len(extensionList.Extensions), ShouldEqual, 1)
|
||||
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
|
||||
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
|
||||
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
|
||||
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
|
||||
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
|
||||
// Verify the endpoints below are enabled by search
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPreferencesPrefix)
|
||||
// Verify the endpoints below are enabled by trust
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullCosign)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullNotation)
|
||||
// Verify the endpint below are enabled by having both the UI and the Search enabled
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPrefs)
|
||||
})
|
||||
|
||||
Convey("start zot server with search and mgmt extensions", t, func(c C) {
|
||||
Convey("start zot server with only the search extension enabled", t, func(c C) {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
@ -8445,18 +8466,9 @@ func TestDistSpecExtensions(t *testing.T) {
|
|||
|
||||
defaultVal := true
|
||||
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
Mgmt: mgmtConfg,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
|
||||
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -8477,14 +8489,51 @@ func TestDistSpecExtensions(t *testing.T) {
|
|||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
err = json.Unmarshal(resp.Body(), &extensionList)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(extensionList.Extensions)
|
||||
So(len(extensionList.Extensions), ShouldEqual, 1)
|
||||
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 3)
|
||||
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 1)
|
||||
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
|
||||
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
|
||||
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
|
||||
// Verify the endpoints below are enabled by search
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPreferencesPrefix)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmtPrefix)
|
||||
// Verify the endpoints below are not enabled since trust is not enabled
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullCosign)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullNotation)
|
||||
// Verify the endpoints below are not enabled since the UI is not enabled
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullMgmt)
|
||||
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullUserPrefs)
|
||||
})
|
||||
|
||||
Convey("start zot server with no enabled extensions", t, func(c C) {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
conf.HTTP.Port = port
|
||||
|
||||
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // clean up
|
||||
|
||||
ctlr := makeController(conf, t.TempDir(), "")
|
||||
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
cm.StartAndWait(port)
|
||||
defer cm.StopServer()
|
||||
|
||||
var extensionList distext.ExtensionList
|
||||
|
||||
resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
err = json.Unmarshal(resp.Body(), &extensionList)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(extensionList.Extensions)
|
||||
// Verify all endpoints which are disabled (even signing urls depend on search being enabled)
|
||||
So(len(extensionList.Extensions), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("start minimal zot server", t, func(c C) {
|
||||
|
|
|
@ -19,9 +19,12 @@ import (
|
|||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
guuid "github.com/gofrs/uuid"
|
||||
"github.com/google/go-github/v52/github"
|
||||
"github.com/gorilla/mux"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
@ -40,6 +43,7 @@ import (
|
|||
syncConstants "zotregistry.io/zot/pkg/extensions/sync/constants"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/meta"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
zreg "zotregistry.io/zot/pkg/regexp"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageCommon "zotregistry.io/zot/pkg/storage/common"
|
||||
|
@ -80,6 +84,19 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
}
|
||||
}
|
||||
|
||||
if rh.c.Config.IsAPIKeyEnabled() {
|
||||
// enable api key management urls
|
||||
apiKeyRouter := rh.c.Router.PathPrefix(constants.APIKeyPath).Subrouter()
|
||||
apiKeyRouter.Use(authHandler)
|
||||
apiKeyRouter.Use(BaseAuthzHandler(rh.c))
|
||||
apiKeyRouter.Use(zcommon.ACHeadersMiddleware(rh.c.Config,
|
||||
http.MethodPost, http.MethodDelete, http.MethodOptions))
|
||||
apiKeyRouter.Use(zcommon.CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin))
|
||||
|
||||
apiKeyRouter.Methods(http.MethodPost, http.MethodOptions).HandlerFunc(rh.CreateAPIKey)
|
||||
apiKeyRouter.Methods(http.MethodDelete).HandlerFunc(rh.RevokeAPIKey)
|
||||
}
|
||||
|
||||
/* on every route which may be used by UI we set OPTIONS as allowed METHOD
|
||||
to enable preflight request from UI to backend */
|
||||
if rh.c.Config.IsBasicAuthnEnabled() {
|
||||
|
@ -157,61 +174,42 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
|
||||
// swagger
|
||||
debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
|
||||
// gql playground
|
||||
gqlPlayground.SetupGQLPlaygroundRoutes(prefixedRouter, rh.c.StoreController, rh.c.Log)
|
||||
|
||||
// Setup Extensions Routes
|
||||
// setup extension routes
|
||||
if rh.c.Config != nil {
|
||||
// This logic needs to be reviewed, it should depend on build options
|
||||
// not the general presence of the extensions in config
|
||||
if rh.c.Config.Extensions == nil {
|
||||
// minimal build
|
||||
prefixedRouter.HandleFunc("/metrics", rh.GetMetrics).Methods("GET")
|
||||
} else {
|
||||
// extended build
|
||||
prefixedExtensionsRouter := prefixedRouter.PathPrefix(constants.ExtPrefix).Subrouter()
|
||||
prefixedExtensionsRouter.Use(CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin))
|
||||
|
||||
ext.SetupMgmtRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.Log)
|
||||
ext.SetupSearchRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo,
|
||||
rh.c.Log)
|
||||
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.StoreController, rh.c.MetaDB,
|
||||
rh.c.CveInfo, rh.c.Log)
|
||||
ext.SetupAPIKeyRoutes(rh.c.Config, prefixedExtensionsRouter, rh.c.MetaDB, rh.c.CookieStore, rh.c.Log)
|
||||
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, authHandler, rh.c.Log)
|
||||
|
||||
gqlPlayground.SetupGQLPlaygroundRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.Log)
|
||||
|
||||
// last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer.
|
||||
ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log)
|
||||
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
addCORSHeaders(allowOrigin, response)
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
})
|
||||
}
|
||||
// Preconditions for enabling the actual extension routes are part of extensions themselves
|
||||
ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo,
|
||||
rh.c.Log)
|
||||
ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
|
||||
ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
|
||||
ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
|
||||
// last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer.
|
||||
ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.Log)
|
||||
}
|
||||
|
||||
func getCORSHeadersHandler(allowOrigin string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
addCORSHeaders(allowOrigin, response)
|
||||
zcommon.AddCORSHeaders(allowOrigin, response)
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func addCORSHeaders(allowOrigin string, response http.ResponseWriter) {
|
||||
if allowOrigin == "" {
|
||||
response.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
func getUIHeadersHandler(config *config.Config, allowedMethods ...string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
allowedMethodsValue := strings.Join(allowedMethods, ",")
|
||||
|
||||
|
@ -1980,6 +1978,123 @@ func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request *
|
|||
zcommon.WriteJSON(response, http.StatusOK, rs)
|
||||
}
|
||||
|
||||
type APIKeyPayload struct { //nolint:revive
|
||||
Label string `json:"label"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
// CreateAPIKey godoc
|
||||
// @Summary Create an API key for the current user
|
||||
// @Description Can create an api key for a logged in user, based on the provided label and scopes.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id body APIKeyPayload true "api token id (UUID)"
|
||||
// @Success 201 {string} string "created"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /auth/apikey [post].
|
||||
func (rh *RouteHandler) CreateAPIKey(resp http.ResponseWriter, req *http.Request) {
|
||||
var payload APIKeyPayload
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
rh.c.Log.Error().Msg("unable to read request body")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, apiKeyID, err := GenerateAPIKey(guuid.DefaultGenerator, rh.c.Log)
|
||||
if err != nil {
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hashedAPIKey := hashUUID(apiKey)
|
||||
|
||||
apiKeyDetails := &mTypes.APIKeyDetails{
|
||||
CreatedAt: time.Now(),
|
||||
LastUsed: time.Now(),
|
||||
CreatorUA: req.UserAgent(),
|
||||
GeneratedBy: "manual",
|
||||
Label: payload.Label,
|
||||
Scopes: payload.Scopes,
|
||||
UUID: apiKeyID,
|
||||
}
|
||||
|
||||
err = rh.c.MetaDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)
|
||||
if err != nil {
|
||||
rh.c.Log.Error().Err(err).Msg("error storing API key")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiKeyResponse := struct {
|
||||
mTypes.APIKeyDetails
|
||||
APIKey string `json:"apiKey"`
|
||||
}{
|
||||
APIKey: fmt.Sprintf("%s%s", constants.APIKeysPrefix, apiKey),
|
||||
APIKeyDetails: *apiKeyDetails,
|
||||
}
|
||||
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
data, err := json.Marshal(apiKeyResponse)
|
||||
if err != nil {
|
||||
rh.c.Log.Error().Err(err).Msg("unable to marshal api key response")
|
||||
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp.Header().Set("Content-Type", constants.DefaultMediaType)
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
_, _ = resp.Write(data)
|
||||
}
|
||||
|
||||
// RevokeAPIKey godoc
|
||||
// @Summary Revokes one current user API key
|
||||
// @Description Revokes one current user API key based on given key ID
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "api token id (UUID)"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Router /auth/apikey [delete].
|
||||
func (rh *RouteHandler) RevokeAPIKey(resp http.ResponseWriter, req *http.Request) {
|
||||
ids, ok := req.URL.Query()["id"]
|
||||
if !ok || len(ids) != 1 {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
keyID := ids[0]
|
||||
|
||||
err := rh.c.MetaDB.DeleteUserAPIKey(req.Context(), keyID)
|
||||
if err != nil {
|
||||
rh.c.Log.Error().Err(err).Str("keyID", keyID).Msg("error deleting API key")
|
||||
resp.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// GetBlobUploadSessionLocation returns actual blob location to start/resume uploading blobs.
|
||||
// e.g. /v2/<name>/blobs/uploads/<session-id>.
|
||||
func getBlobUploadSessionLocation(url *url.URL, sessionID string) string {
|
||||
|
@ -2009,9 +2124,7 @@ func getBlobUploadLocation(url *url.URL, name string, digest godigest.Digest) st
|
|||
}
|
||||
|
||||
func isSyncOnDemandEnabled(ctlr Controller) bool {
|
||||
if ctlr.Config.Extensions != nil &&
|
||||
ctlr.Config.Extensions.Sync != nil &&
|
||||
*ctlr.Config.Extensions.Sync.Enable &&
|
||||
if ctlr.Config.IsSyncEnabled() &&
|
||||
fmt.Sprintf("%v", ctlr.SyncOnDemand) != fmt.Sprintf("%v", nil) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build sync && scrub && metrics && search && lint && apikey && mgmt
|
||||
// +build sync,scrub,metrics,search,lint,apikey,mgmt
|
||||
//go:build sync && scrub && metrics && search && lint && mgmt
|
||||
// +build sync,scrub,metrics,search,lint,mgmt
|
||||
|
||||
package api_test
|
||||
|
||||
|
@ -7,7 +7,6 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -28,8 +27,6 @@ import (
|
|||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageTypes "zotregistry.io/zot/pkg/storage/types"
|
||||
|
@ -37,8 +34,6 @@ import (
|
|||
"zotregistry.io/zot/pkg/test/mocks"
|
||||
)
|
||||
|
||||
var ErrUnexpectedError = errors.New("error: unexpected error")
|
||||
|
||||
const sessionStr = "session"
|
||||
|
||||
func TestRoutes(t *testing.T) {
|
||||
|
@ -62,6 +57,8 @@ func TestRoutes(t *testing.T) {
|
|||
}()
|
||||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
defaultVal := true
|
||||
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
|
@ -77,14 +74,7 @@ func TestRoutes(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultVal := true
|
||||
apiKeyConfig := &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: apiKeyConfig,
|
||||
APIKey: defaultVal,
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
@ -1434,14 +1424,14 @@ func TestRoutes(t *testing.T) {
|
|||
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
extensions.CreateAPIKey(response, request, ctlr.MetaDB, ctlr.CookieStore, ctlr.Log)
|
||||
rthdlr.CreateAPIKey(response, request)
|
||||
|
||||
resp := response.Result()
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: username,
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx = context.TODO()
|
||||
|
@ -1451,14 +1441,14 @@ func TestRoutes(t *testing.T) {
|
|||
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
|
||||
response = httptest.NewRecorder()
|
||||
|
||||
extensions.CreateAPIKey(response, request, ctlr.MetaDB, ctlr.CookieStore, ctlr.Log)
|
||||
rthdlr.CreateAPIKey(response, request)
|
||||
|
||||
resp = response.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
payload := extensions.APIKeyPayload{
|
||||
payload := api.APIKeyPayload{
|
||||
Label: "test",
|
||||
Scopes: []string{"test"},
|
||||
}
|
||||
|
@ -1468,11 +1458,12 @@ func TestRoutes(t *testing.T) {
|
|||
request, _ = http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader(reqBody))
|
||||
response = httptest.NewRecorder()
|
||||
|
||||
extensions.CreateAPIKey(response, request, mocks.MetaDBMock{
|
||||
ctlr.MetaDB = mocks.MetaDBMock{
|
||||
AddUserAPIKeyFn: func(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error {
|
||||
return ErrUnexpectedError
|
||||
},
|
||||
}, ctlr.CookieStore, ctlr.Log)
|
||||
}
|
||||
rthdlr.CreateAPIKey(response, request)
|
||||
|
||||
resp = response.Result()
|
||||
defer resp.Body.Close()
|
||||
|
@ -1486,11 +1477,12 @@ func TestRoutes(t *testing.T) {
|
|||
q.Add("id", "apikeyid")
|
||||
request.URL.RawQuery = q.Encode()
|
||||
|
||||
extensions.RevokeAPIKey(response, request, mocks.MetaDBMock{
|
||||
ctlr.MetaDB = mocks.MetaDBMock{
|
||||
DeleteUserAPIKeyFn: func(ctx context.Context, id string) error {
|
||||
return ErrUnexpectedError
|
||||
},
|
||||
}, ctlr.CookieStore, ctlr.Log)
|
||||
}
|
||||
rthdlr.RevokeAPIKey(response, request)
|
||||
|
||||
resp = response.Result()
|
||||
defer resp.Body.Close()
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
//go:build sync && scrub && metrics && search && apikey
|
||||
// +build sync,scrub,metrics,search,apikey
|
||||
//go:build sync && scrub && metrics && search && userprefs && mgmt && imagetrust
|
||||
// +build sync,scrub,metrics,search,userprefs,mgmt,imagetrust
|
||||
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -640,7 +638,7 @@ func TestServeSearchEnabled(t *testing.T) {
|
|||
|
||||
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}`
|
||||
|
||||
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
|
@ -691,7 +689,7 @@ func TestServeSearchEnabledCVE(t *testing.T) {
|
|||
substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
|
||||
"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"}}}"
|
||||
|
||||
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
|
||||
defer func() {
|
||||
if !found {
|
||||
|
@ -704,7 +702,7 @@ func TestServeSearchEnabledCVE(t *testing.T) {
|
|||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
found, err = readLogFileAndSearchString(logPath, "updating the CVE database", readLogFileTimeout)
|
||||
found, err = ReadLogFileAndSearchString(logPath, "updating the CVE database", readLogFileTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
@ -741,7 +739,7 @@ func TestServeSearchEnabledNoCVE(t *testing.T) {
|
|||
defer os.Remove(logPath) // clean up
|
||||
|
||||
substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}` //nolint:lll // gofumpt conflicts with lll
|
||||
found, err := readLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
|
@ -815,20 +813,31 @@ func TestServeMgmtExtension(t *testing.T) {
|
|||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"Mgmt": {
|
||||
"ui": {
|
||||
"enable": true
|
||||
},
|
||||
"search": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":true}")
|
||||
found, err := ReadLogFileAndSearchString(logPath, "setting up mgmt routes", 10*time.Second)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Mgmt disabled", t, func(c C) {
|
||||
Convey("Mgmt disabled - UI unconfigured", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
|
@ -842,27 +851,66 @@ func TestServeMgmtExtension(t *testing.T) {
|
|||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"Mgmt": {
|
||||
"enable": "false"
|
||||
"search": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
defer os.Remove(logPath) // clean up
|
||||
found, err := ReadLogFileAndSearchString(logPath,
|
||||
"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Mgmt disabled - extensions missing", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":false}")
|
||||
found, err := ReadLogFileAndSearchString(logPath,
|
||||
"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeAPIKeyExtension(t *testing.T) {
|
||||
func TestServeImageTrustExtension(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("apikey implicitly enabled", t, func(c C) {
|
||||
Convey("Trust explicitly disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
|
@ -876,20 +924,29 @@ func TestServeAPIKeyExtension(t *testing.T) {
|
|||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"trust": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":true}")
|
||||
found, err := ReadLogFileAndSearchString(logPath,
|
||||
"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("apikey disabled", t, func(c C) {
|
||||
Convey("Trust explicitly enabled - but cosign and notation disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
|
@ -903,79 +960,75 @@ func TestServeAPIKeyExtension(t *testing.T) {
|
|||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"apikey": {
|
||||
"enable": "false"
|
||||
"trust": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
defer os.Remove(logPath) // clean up
|
||||
found, err := ReadLogFileAndSearchString(logPath,
|
||||
"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)
|
||||
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Trust explicitly enabled - cosign and notation enabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"trust": {
|
||||
"enable": true,
|
||||
"cosign": true,
|
||||
"notation": true
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":false}")
|
||||
found, err := ReadLogFileAndSearchString(logPath,
|
||||
"setting up image trust routes", 10*time.Second)
|
||||
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
|
||||
found, err = ReadLogFileAndSearchString(logPath,
|
||||
"setting up notation route", 10*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
|
||||
found, err = ReadLogFileAndSearchString(logPath,
|
||||
"setting up cosign route", 10*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func readLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { //nolint:unparam,lll
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancelFunc()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
default:
|
||||
content, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(string(content), stringToMatch) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run cli and return output.
|
||||
func runCLIWithConfig(tempDir string, config string) (string, error) {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
|
||||
logFile, err := os.CreateTemp(tempDir, "zot-log*.txt")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cfgfile, err := os.CreateTemp(tempDir, "zot-test*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config = fmt.Sprintf(config, tempDir, port, logFile.Name())
|
||||
|
||||
_, err = cfgfile.Write([]byte(config))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cfgfile.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
os.Args = []string{"cli_test", "serve", cfgfile.Name()}
|
||||
|
||||
go func() {
|
||||
err = cli.NewServerRootCmd().Execute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
WaitTillServerReady(baseURL)
|
||||
|
||||
return logFile.Name(), nil
|
||||
}
|
||||
|
|
|
@ -757,10 +757,10 @@ func TestOutputFormat(t *testing.T) {
|
|||
`"variant":""},"isSigned":false,"downloadCount":0,`+
|
||||
`"layers":[{"size":"","digest":"sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6",`+
|
||||
`"score":0}],"history":null,"vulnerabilities":{"maxSeverity":"","count":0},`+
|
||||
`"referrers":null,"artifactType":""}],"size":"123445",`+
|
||||
`"referrers":null,"artifactType":"","signatureInfo":null}],"size":"123445",`+
|
||||
`"downloadCount":0,"lastUpdated":"0001-01-01T00:00:00Z","description":"","isSigned":false,"licenses":"",`+
|
||||
`"labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",`+
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}`+"\n")
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}`+"\n")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
|
@ -788,10 +788,10 @@ func TestOutputFormat(t *testing.T) {
|
|||
`issigned: false downloadcount: 0 layers: - size: "" `+
|
||||
`digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+
|
||||
`size: "123445" downloadcount: 0 `+
|
||||
`signatureinfo: [] size: "123445" downloadcount: 0 `+
|
||||
`lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+
|
||||
`title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: "" `+
|
||||
`count: 0 referrers: []`,
|
||||
`count: 0 referrers: [] signatureinfo: []`,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -822,10 +822,10 @@ func TestOutputFormat(t *testing.T) {
|
|||
`issigned: false downloadcount: 0 layers: - size: "" `+
|
||||
`digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+
|
||||
`size: "123445" downloadcount: 0 `+
|
||||
`signatureinfo: [] size: "123445" downloadcount: 0 `+
|
||||
`lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+
|
||||
`title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: `+
|
||||
`"" count: 0 referrers: []`,
|
||||
`"" count: 0 referrers: [] signatureinfo: []`,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
@ -886,10 +886,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
|||
`"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` +
|
||||
`"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` +
|
||||
`"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"artifactType":""}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` +
|
||||
`"referrers":null,"artifactType":"","signatureInfo":null}],` +
|
||||
`"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` +
|
||||
`"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` +
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}` + "\n" +
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n" +
|
||||
`{"repoName":"repo7","tag":"test:2.0",` +
|
||||
`"digest":"sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06",` +
|
||||
`"mediaType":"application/vnd.oci.image.manifest.v1+json",` +
|
||||
|
@ -898,10 +899,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
|||
`"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` +
|
||||
`"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` +
|
||||
`"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"artifactType":""}],` +
|
||||
`"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` +
|
||||
`"referrers":null,"artifactType":"","signatureInfo":null}],` +
|
||||
`"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` +
|
||||
`"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` +
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null}` + "\n"
|
||||
`"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n"
|
||||
// Output is supposed to be in json lines format, keep all spaces as is for verification
|
||||
So(buff.String(), ShouldEqual, expectedStr)
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -928,10 +930,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
|||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: [] ` +
|
||||
`--- reponame: repo7 tag: test:2.0 ` +
|
||||
`digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` +
|
||||
`mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` +
|
||||
|
@ -940,10 +943,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
|||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: []`
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []`
|
||||
So(strings.TrimSpace(str), ShouldEqual, expectedStr)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
@ -969,10 +973,12 @@ func TestOutputFormatGQL(t *testing.T) {
|
|||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] signatureinfo: [] ` +
|
||||
`--- reponame: repo7 tag: test:2.0 ` +
|
||||
`digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` +
|
||||
`mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` +
|
||||
|
@ -981,10 +987,11 @@ func TestOutputFormatGQL(t *testing.T) {
|
|||
`lastupdated: 2023-01-01T12:00:00Z size: "528" platform: os: linux arch: amd64 variant: "" ` +
|
||||
`issigned: false downloadcount: 0 layers: - size: "15" ` +
|
||||
`digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" ` +
|
||||
`history: [] vulnerabilities: maxseverity: "" ` +
|
||||
`count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` +
|
||||
`size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` +
|
||||
`issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` +
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: []`
|
||||
`authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []`
|
||||
So(strings.TrimSpace(str), ShouldEqual, expectedStr)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
|
|
@ -313,13 +313,18 @@ func validateCacheConfig(cfg *config.Config) error {
|
|||
}
|
||||
|
||||
func validateExtensionsConfig(cfg *config.Config) error {
|
||||
if cfg.Extensions != nil && cfg.Extensions.Mgmt != nil {
|
||||
log.Warn().Msg("The mgmt extensions configuration option has been made redundant and will be ignored.")
|
||||
}
|
||||
|
||||
if cfg.Extensions != nil && cfg.Extensions.APIKey != nil {
|
||||
log.Warn().Msg("The apikey extension configuration will be ignored as API keys " +
|
||||
"are now configurable in the HTTP settings.")
|
||||
}
|
||||
|
||||
if cfg.Extensions != nil && cfg.Extensions.UI != nil && cfg.Extensions.UI.Enable != nil && *cfg.Extensions.UI.Enable {
|
||||
if cfg.Extensions.Mgmt == nil || !*cfg.Extensions.Mgmt.Enable {
|
||||
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without mgmt extension.")
|
||||
|
||||
return errors.ErrBadConfig
|
||||
}
|
||||
|
||||
// it would make sense to also check for mgmt and user prefs to be enabled,
|
||||
// but those are both enabled by having the search and ui extensions enabled
|
||||
if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
|
||||
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
|
||||
|
||||
|
@ -513,18 +518,18 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
config.Extensions.Scrub = &extconf.ScrubConfig{}
|
||||
}
|
||||
|
||||
_, ok = extMap["mgmt"]
|
||||
_, ok = extMap["trust"]
|
||||
if ok {
|
||||
// we found a config like `"extensions": {"mgmt:": {}}`
|
||||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Mgmt = &extconf.MgmtConfig{}
|
||||
// we found a config like `"extensions": {"trust:": {}}`
|
||||
// Note: In case trust is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Trust = &extconf.ImageTrustConfig{}
|
||||
}
|
||||
|
||||
_, ok = extMap["apikey"]
|
||||
_, ok = extMap["ui"]
|
||||
if ok {
|
||||
// we found a config like `"extensions": {"mgmt:": {}}`
|
||||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.APIKey = &extconf.APIKeyConfig{}
|
||||
// we found a config like `"extensions": {"ui:": {}}`
|
||||
// Note: In case UI is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.UI = &extconf.UIConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -586,18 +591,6 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Mgmt != nil {
|
||||
if config.Extensions.Mgmt.Enable == nil {
|
||||
config.Extensions.Mgmt.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.APIKey != nil {
|
||||
if config.Extensions.APIKey.Enable == nil {
|
||||
config.Extensions.APIKey.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Scrub != nil {
|
||||
if config.Extensions.Scrub.Enable == nil {
|
||||
config.Extensions.Scrub.Enable = &defaultVal
|
||||
|
@ -607,6 +600,18 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
config.Extensions.Scrub.Interval = 24 * time.Hour //nolint: gomnd
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.UI != nil {
|
||||
if config.Extensions.UI.Enable == nil {
|
||||
config.Extensions.UI.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Trust != nil {
|
||||
if config.Extensions.Trust.Enable == nil {
|
||||
config.Extensions.Trust.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !config.Storage.GC && viperInstance.Get("storage::gcdelay") == nil {
|
||||
|
@ -663,6 +668,12 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
config.Storage.SubPaths[name] = storageConfig
|
||||
}
|
||||
}
|
||||
|
||||
// if OpenID authentication is enabled,
|
||||
// API Keys are also enabled in order to provide data path authentication
|
||||
if config.HTTP.Auth != nil && config.HTTP.Auth.OpenID != nil {
|
||||
config.HTTP.Auth.APIKey = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateDistSpecVersion(config *config.Config) {
|
||||
|
|
|
@ -1083,7 +1083,7 @@ func TestVerify(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidateExtensionsConfig(t *testing.T) {
|
||||
Convey("Test missing extensions for UI to work", t, func(c C) {
|
||||
Convey("Legacy extensions should not error", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -1100,40 +1100,39 @@ func TestValidateExtensionsConfig(t *testing.T) {
|
|||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {
|
||||
"enable": "true"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test missing extensions for UI to work", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{
|
||||
"storage": {
|
||||
"rootDirectory": "%/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {
|
||||
"enable": "true"
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": "true"
|
||||
},
|
||||
"apikey": {
|
||||
"enable": "true"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test missing extensions for UI to work", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
content := []byte(`{
|
||||
"storage": {
|
||||
"rootDirectory": "%/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {
|
||||
"enable": "true"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
@ -1143,7 +1142,7 @@ func TestValidateExtensionsConfig(t *testing.T) {
|
|||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test missing mgmt extension for UI to work", t, func(c C) {
|
||||
Convey("Test enabling UI extension with all prerequisites", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -1172,7 +1171,165 @@ func TestValidateExtensionsConfig(t *testing.T) {
|
|||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test extension are implicitly enabled", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{
|
||||
"storage": {
|
||||
"rootDirectory": "%/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8080"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug"
|
||||
},
|
||||
"extensions": {
|
||||
"ui": {},
|
||||
"search": {},
|
||||
"metrics": {},
|
||||
"trust": {},
|
||||
"scrub": {}
|
||||
}
|
||||
}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.Extensions.UI, ShouldNotBeNil)
|
||||
So(*config.Extensions.UI.Enable, ShouldBeTrue)
|
||||
So(config.Extensions.Search, ShouldNotBeNil)
|
||||
So(*config.Extensions.Search.Enable, ShouldBeTrue)
|
||||
So(config.Extensions.Trust, ShouldNotBeNil)
|
||||
So(*config.Extensions.Trust.Enable, ShouldBeTrue)
|
||||
So(*config.Extensions.Metrics, ShouldNotBeNil)
|
||||
So(*config.Extensions.Metrics.Enable, ShouldBeTrue)
|
||||
So(config.Extensions.Scrub, ShouldNotBeNil)
|
||||
So(*config.Extensions.Scrub.Enable, ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiKeyConfig(t *testing.T) {
|
||||
Convey("Test API Keys are enabled if OpenID is enabled", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex",
|
||||
"clientid":"client_id","scopes":["openid"]}}}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.HTTP.Auth, ShouldNotBeNil)
|
||||
So(config.HTTP.Auth.APIKey, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Test API Keys are not enabled by default", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot"},
|
||||
"log":{"level":"debug"}}`)
|
||||
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.HTTP.Auth, ShouldNotBeNil)
|
||||
So(config.HTTP.Auth.APIKey, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Test API Keys are not enabled if OpenID is not enabled", t, func(c C) {
|
||||
config := config.New()
|
||||
tmpfile, err := os.CreateTemp("", "zot-test*.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"}}},
|
||||
"log":{"level":"debug"}}`)
|
||||
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
So(err, ShouldBeNil)
|
||||
err = cli.LoadConfiguration(config, tmpfile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(config.HTTP.Auth, ShouldNotBeNil)
|
||||
So(config.HTTP.Auth.APIKey, ShouldBeFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeAPIKey(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("apikey implicitly enabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s",
|
||||
"auth": {
|
||||
"apikey": true
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":true")
|
||||
})
|
||||
|
||||
Convey("apikey disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s",
|
||||
"auth": {
|
||||
"apikey": false
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"APIKey\":false")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1557,3 +1714,44 @@ func TestScrub(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
// run cli and return output.
|
||||
func runCLIWithConfig(tempDir string, config string) (string, error) {
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
|
||||
logFile, err := os.CreateTemp(tempDir, "zot-log*.txt")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cfgfile, err := os.CreateTemp(tempDir, "zot-test*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config = fmt.Sprintf(config, tempDir, port, logFile.Name())
|
||||
|
||||
_, err = cfgfile.Write([]byte(config))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cfgfile.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
os.Args = []string{"cli_test", "serve", cfgfile.Name()}
|
||||
|
||||
go func() {
|
||||
err = cli.NewServerRootCmd().Execute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
WaitTillServerReady(baseURL)
|
||||
|
||||
return logFile.Name(), nil
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
apiErr "zotregistry.io/zot/pkg/api/errors"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
)
|
||||
|
||||
func AllowedMethods(methods ...string) []string {
|
||||
|
@ -29,7 +30,7 @@ func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen
|
|||
}
|
||||
}
|
||||
|
||||
func ACHeadersHandler(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
|
||||
func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
|
||||
allowedMethodsValue := strings.Join(allowedMethods, ",")
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
@ -50,6 +51,54 @@ func ACHeadersHandler(config *config.Config, allowedMethods ...string) mux.Middl
|
|||
}
|
||||
}
|
||||
|
||||
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
AddCORSHeaders(allowOrigin, response)
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func AddCORSHeaders(allowOrigin string, response http.ResponseWriter) {
|
||||
if allowOrigin == "" {
|
||||
response.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthzOnlyAdminsMiddleware permits only admin user access if auth is enabled.
|
||||
func AuthzOnlyAdminsMiddleware(conf *config.Config) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if !conf.IsBasicAuthnEnabled() {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// get acCtx built in previous authn/authz middlewares
|
||||
acCtx, err := localCtx.GetAccessControlContext(request.Context())
|
||||
if err != nil { // should not happen as this has been previously checked for errors
|
||||
AuthzFail(response, request, conf.HTTP.Realm, conf.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// reject non-admin access if authentication is enabled
|
||||
if acCtx != nil && !acCtx.IsAdmin {
|
||||
AuthzFail(response, request, conf.HTTP.Realm, conf.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func AuthzFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ type ImageSummary struct {
|
|||
Vendor string `json:"vendor"`
|
||||
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
|
||||
Referrers []Referrer `json:"referrers"`
|
||||
SignatureInfo []SignatureSummary `json:"signatureInfo"`
|
||||
}
|
||||
|
||||
type ManifestSummary struct {
|
||||
|
@ -67,6 +68,13 @@ type ManifestSummary struct {
|
|||
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
|
||||
Referrers []Referrer `json:"referrers"`
|
||||
ArtifactType string `json:"artifactType"`
|
||||
SignatureInfo []SignatureSummary `json:"signatureInfo"`
|
||||
}
|
||||
|
||||
type SignatureSummary struct {
|
||||
Tool string `json:"tool"`
|
||||
IsTrusted bool `json:"isTrusted"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
type Platform struct {
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
debugCst "zotregistry.io/zot/pkg/debug/constants"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
|
@ -21,7 +20,7 @@ import (
|
|||
var playgroundHTML embed.FS
|
||||
|
||||
// SetupGQLPlaygroundRoutes ...
|
||||
func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router,
|
||||
func SetupGQLPlaygroundRoutes(router *mux.Router,
|
||||
storeController storage.StoreController, l log.Logger,
|
||||
) {
|
||||
log := log.Logger{Logger: l.With().Caller().Timestamp().Logger()}
|
||||
|
|
|
@ -6,13 +6,12 @@ package debug
|
|||
import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
// SetupGQLPlaygroundRoutes ...
|
||||
func SetupGQLPlaygroundRoutes(conf *config.Config, router *mux.Router,
|
||||
func SetupGQLPlaygroundRoutes(router *mux.Router,
|
||||
storeController storage.StoreController, log log.Logger,
|
||||
) {
|
||||
log.Warn().Msg("skipping enabling graphql playground extension because given zot binary " +
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -1,48 +1,77 @@
|
|||
# Verifying signatures
|
||||
# Image Trust
|
||||
|
||||
The `imagetrust` extension provides a mechanism to verify image signatures using certificates and public keys
|
||||
|
||||
## How to configure zot for verifying signatures
|
||||
|
||||
In order to configure zot for verifying signatures, the user should provide:
|
||||
In order to configure zot for verifying signatures, the user should first enable this feature:
|
||||
|
||||
1. public keys (which correspond to the private keys used to sign images with `cosign`)
|
||||
```json
|
||||
"extensions": {
|
||||
"trust": {
|
||||
"enable": true,
|
||||
"cosign": true,
|
||||
"notation": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
In order for verification to run, the user needs to enable at least one of the cosign or notation options above.
|
||||
|
||||
2. certificates (used to sign images with `notation`)
|
||||
## Uploading public keys or certificates
|
||||
|
||||
These files could be uploaded using one of these requests:
|
||||
Next the user needs to upload the keys or certificates used for the verification.
|
||||
|
||||
1. upload a public key
|
||||
| Supported queries | Input | Output | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| Upload a certificate | certificate | None | Add certificate for verifying notation signatures|
|
||||
| Upload a public key | public key | None | Add public key for verifying cosign signatures |
|
||||
|
||||
***Example of request***
|
||||
```
|
||||
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=cosign"
|
||||
```
|
||||
### Uploading a Cosign public key
|
||||
|
||||
2. upload a certificate
|
||||
The Cosign public keys uploaded correspond to the private keys used to sign images with `cosign`.
|
||||
|
||||
***Example of request***
|
||||
```
|
||||
curl --data-binary @filet.crt -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=notation&truststoreType=ca&truststoreName=upload-cert"
|
||||
```
|
||||
***Example of request***
|
||||
|
||||
Besides the requested files, the user should also specify the `tool` which should be :
|
||||
|
||||
- `cosign` for uploading public keys
|
||||
- `notation` for uploading certificates
|
||||
```bash
|
||||
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/cosign
|
||||
```
|
||||
|
||||
Also, if the uploaded file is a certificate then the user should also specify the type of the truststore through `truststoreType` param and also its name through `truststoreName` param.
|
||||
As a result of this request, the uploaded file will be stored in `_cosign` directory
|
||||
under the rootDir specified in the zot config.
|
||||
|
||||
### Uploading a Notation certificate
|
||||
|
||||
Notation certificates are used to sign images with the `notation` tool.
|
||||
The user needs to specify the type of the truststore through the `truststoreType`
|
||||
query parameter and its name through the `truststoreName` parameter.
|
||||
`truststoreType` defaults to `ca`, while `truststoreName` is a mandatory parameter.
|
||||
|
||||
***Example of request***
|
||||
|
||||
```bash
|
||||
curl --data-binary @certificate.crt -X POST "http://localhost:8080/v2/_zot/ext/notation?truststoreType=ca&truststoreName=upload-cert"
|
||||
```
|
||||
|
||||
As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/{truststoreName}`
|
||||
directory under the rootDir specified in the zot config.
|
||||
The `truststores` field found in `_notation/trustpolicy.json` file will be updated automatically as well.
|
||||
|
||||
## Verification and results
|
||||
|
||||
Based on the uploaded files, signatures verification will be performed for all the signed images.
|
||||
The information determined about the signatures will be:
|
||||
|
||||
Based on the uploaded files, signatures verification will be performed for all the signed images. Then the information known about the signatures will be:
|
||||
|
||||
- the tool used to generate the signature (`cosign` or `notation`)
|
||||
- info about the trustworthiness of the signature (if there is a certificate or a public key which can successfully verify the signature)
|
||||
- the author of the signature which will be:
|
||||
|
||||
- the public key -> for signatures generated using `cosign`
|
||||
- the subject of the certificate -> for signatures generated using `notation`
|
||||
|
||||
**Example of GraphQL output**
|
||||
- the public key -> for signatures generated using `cosign`
|
||||
- the subject of the certificate -> for signatures generated using `notation`
|
||||
|
||||
The information above will be included in the ManifestSummary objects returned by the `search` extension.
|
||||
|
||||
***Example of GraphQL output***
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -90,12 +119,13 @@ Besides the requested files, the user should also specify the `tool` which shoul
|
|||
## Notes
|
||||
|
||||
- The files (public keys and certificates) uploaded using the exposed routes will be stored in some specific directories called `_cosign` and `_notation` under `$rootDir`.
|
||||
|
||||
|
||||
- `_cosign` directory will contain the uploaded public keys
|
||||
|
||||
```
|
||||
_cosign
|
||||
├── $publicKey1
|
||||
└── $publicKey2
|
||||
└── $publicKey2
|
||||
```
|
||||
|
||||
- `_notation` directory will have this structure:
|
||||
|
@ -103,15 +133,16 @@ Besides the requested files, the user should also specify the `tool` which shoul
|
|||
```
|
||||
_notation
|
||||
├── trustpolicy.json
|
||||
└── truststore
|
||||
└── x509
|
||||
└── $truststoreType
|
||||
└── $truststoreName
|
||||
└── $certificate
|
||||
└── truststore
|
||||
└── x509
|
||||
└── $truststoreType
|
||||
└── $truststoreName
|
||||
└── $certificate
|
||||
```
|
||||
|
||||
where `trustpolicy.json` file has this default content which can not be modified by the user and which is updated each time a new certificate is added to a new truststore:
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"trustPolicies": [
|
||||
|
@ -127,6 +158,5 @@ Besides the requested files, the user should also specify the `tool` which shoul
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -10,12 +10,6 @@ Response depends on the user privileges:
|
|||
| Supported queries | Input | Output | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [Get current configuration](#get-current-configuration) | None | config json | Get current zot configuration |
|
||||
| [Upload a certificate](#post-certificate) | certificate | None | Add certificate for verifying notation signatures|
|
||||
| [Upload a public key](#post-public-key) | public key | None | Add public key for verifying cosign signatures |
|
||||
|
||||
## General usage
|
||||
The mgmt endpoint accepts as a query parameter what `resource` is targeted by the request and then all other required parameters for the specified resource. The default value of this
|
||||
query parameter is `config`.
|
||||
|
||||
## Get current configuration
|
||||
|
||||
|
@ -46,35 +40,3 @@ curl http://localhost:8080/v2/_zot/ext/mgmt | jq
|
|||
If ldap or htpasswd are enabled mgmt will return `{"htpasswd": {}}` indicating that clients can authenticate with basic auth credentials.
|
||||
|
||||
If any key is present under `'auth'` key, in the mgmt response, it means that particular authentication method is enabled.
|
||||
|
||||
## Configure zot for verifying signatures
|
||||
If the `resource` is `signatures` then the mgmt endpoint accepts as a query parameter the `tool` that corresponds to the uploaded file and then all other required parameters for the specified tool.
|
||||
|
||||
### Upload a certificate
|
||||
|
||||
**Sample request**
|
||||
|
||||
| Tool | Parameter | Parameter Type | Parameter Description |
|
||||
| --- | --- | --- | --- |
|
||||
| notation | truststoreType | string | The type of the truststore. This parameter is optional and its default value is `ca` |
|
||||
| | truststoreName | string | The name of the truststore |
|
||||
|
||||
```bash
|
||||
curl --data-binary @certificate.crt -X POST http://localhost:8080/v2/_zot/ext/mgmt?resource=signature&tool=notation&truststoreType=ca&truststoreName=newtruststore
|
||||
```
|
||||
As a result of this request, the uploaded file will be stored in `_notation/truststore/x509/{truststoreType}/{truststoreName}` directory under $rootDir. And `truststores` field from `_notation/trustpolicy.json` file will be updated.
|
||||
|
||||
### Upload a public key
|
||||
|
||||
**Sample request**
|
||||
|
||||
| Tool | Parameter | Parameter Type | Parameter Description |
|
||||
| --- | --- | --- | --- |
|
||||
| cosign |
|
||||
|
||||
|
||||
```bash
|
||||
curl --data-binary @publicKey.pub -X POST http://localhost:8080/v2/_zot/ext/mgmt?resource=signature&tool=cosign
|
||||
```
|
||||
|
||||
As a result of this request, the uploaded file will be stored in `_cosign` directory under $rootDir.
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
Component | Endpoint | Description
|
||||
--- | --- | ---
|
||||
[`search`](search/search.md) | `/v2/_zot/ext/search` | efficient and enhanced registry search capabilities using graphQL backend
|
||||
[`mgmt`](mgmt.md) | `/v2/_zot/ext/mgmt` | config management
|
||||
[`userprefs`](userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences
|
||||
[`apikey`](README_apikey.md) | `/v2/_zot/ext/apikey` | user api keys management
|
||||
[`mgmt`](README_mgmt.md) | `/v2/_zot/ext/mgmt` | config management
|
||||
[`userprefs`](README_userprefs.md) | `/v2/_zot/ext/userprefs` | change user preferences
|
||||
[`imagetrust`](README_imagetrust.md) | `/v2/_zot/ext/cosign` | cosign public key management
|
||||
[`imagetrust`](README_imagetrust.md) | `/v2/_zot/ext/notation` | notation certificate management
|
||||
|
||||
|
||||
# References
|
||||
|
|
|
@ -20,6 +20,13 @@ type ExtensionConfig struct {
|
|||
UI *UIConfig
|
||||
Mgmt *MgmtConfig
|
||||
APIKey *APIKeyConfig
|
||||
Trust *ImageTrustConfig
|
||||
}
|
||||
|
||||
type ImageTrustConfig struct {
|
||||
BaseConfig `mapstructure:",squash"`
|
||||
Cosign bool
|
||||
Notation bool
|
||||
}
|
||||
|
||||
type APIKeyConfig struct {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
}
|
183
pkg/extensions/extension_image_trust.go
Normal file
183
pkg/extensions/extension_image_trust.go
Normal 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)
|
||||
}
|
29
pkg/extensions/extension_image_trust_disabled.go
Normal file
29
pkg/extensions/extension_image_trust_disabled.go
Normal 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")
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
//go:build !mgmt
|
||||
//go:build !imagetrust
|
||||
|
||||
package extensions_test
|
||||
|
||||
|
@ -14,8 +14,8 @@ import (
|
|||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
||||
func TestMgmtExtension(t *testing.T) {
|
||||
Convey("periodic signature verification is skipped when binary doesn't include mgmt", t, func() {
|
||||
func TestImageTrustExtension(t *testing.T) {
|
||||
Convey("periodic signature verification is skipped when binary doesn't include imagetrust", t, func() {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
|
||||
|
@ -30,11 +30,10 @@ func TestMgmtExtension(t *testing.T) {
|
|||
conf.Storage.RootDirectory = globalDir
|
||||
conf.Storage.Commit = true
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
|
||||
conf.Extensions.Trust.Enable = &defaultValue
|
||||
conf.Extensions.Trust.Cosign = defaultValue
|
||||
conf.Extensions.Trust.Notation = defaultValue
|
||||
conf.Log.Level = "warn"
|
||||
conf.Log.Output = logFile.Name()
|
||||
|
958
pkg/extensions/extension_image_trust_test.go
Normal file
958
pkg/extensions/extension_image_trust_test.go
Normal 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)
|
||||
})
|
||||
}
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir string) {
|
||||
|
@ -26,7 +25,7 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
|
|||
}
|
||||
}
|
||||
|
||||
func SetupMetricsRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
func SetupMetricsRoutes(config *config.Config, router *mux.Router,
|
||||
authFunc mux.MiddlewareFunc, log log.Logger,
|
||||
) {
|
||||
log.Info().Msg("setting up metrics routes")
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
// EnableMetricsExtension ...
|
||||
|
@ -19,7 +18,7 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
|
|||
|
||||
// SetupMetricsRoutes ...
|
||||
func SetupMetricsRoutes(conf *config.Config, router *mux.Router,
|
||||
storeController storage.StoreController, authFunc mux.MiddlewareFunc, log log.Logger,
|
||||
authFunc mux.MiddlewareFunc, log log.Logger,
|
||||
) {
|
||||
log.Warn().Msg("skipping setting up metrics routes because given zot binary doesn't include this feature," +
|
||||
"please build a binary that does so")
|
||||
|
|
|
@ -4,27 +4,15 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
zcommon "zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/meta/signatures"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
"zotregistry.io/zot/pkg/scheduler"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigResource = "config"
|
||||
SignaturesResource = "signatures"
|
||||
)
|
||||
|
||||
type HTPasswd struct {
|
||||
|
@ -90,246 +78,51 @@ func (auth Auth) MarshalJSON() ([]byte, error) {
|
|||
return json.Marshal((localAuth)(auth))
|
||||
}
|
||||
|
||||
type mgmt struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
}
|
||||
func SetupMgmtRoutes(conf *config.Config, router *mux.Router, log log.Logger) {
|
||||
if !conf.IsMgmtEnabled() {
|
||||
log.Info().Msg("skip enabling the mgmt route as the config prerequisites are not met")
|
||||
|
||||
func (mgmt *mgmt) handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var resource string
|
||||
|
||||
if zcommon.QueryHasParams(r.URL.Query(), []string{"resource"}) {
|
||||
resource = r.URL.Query().Get("resource")
|
||||
} else {
|
||||
resource = ConfigResource // default value of "resource" query param
|
||||
}
|
||||
|
||||
switch resource {
|
||||
case ConfigResource:
|
||||
if r.Method == http.MethodGet {
|
||||
mgmt.HandleGetConfig(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return
|
||||
case SignaturesResource:
|
||||
if r.Method == http.MethodPost {
|
||||
HandleCertificatesAndPublicKeysUploads(w, r) //nolint: contextcheck
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger) {
|
||||
if config.Extensions.Mgmt != nil && *config.Extensions.Mgmt.Enable {
|
||||
log.Info().Msg("setting up mgmt routes")
|
||||
|
||||
mgmt := mgmt{config: config, log: log}
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
||||
|
||||
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
|
||||
mgmtRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
|
||||
mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
mgmtRouter.Methods(allowedMethods...).Handler(mgmt.handler())
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("setting up mgmt routes")
|
||||
|
||||
mgmt := Mgmt{Conf: conf, Log: log}
|
||||
|
||||
// The endpoint for reading configuration should be available to all users
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet)
|
||||
|
||||
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
|
||||
mgmtRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
|
||||
mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
mgmtRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
|
||||
mgmtRouter.Methods(allowedMethods...).HandlerFunc(mgmt.HandleGetConfig)
|
||||
|
||||
log.Info().Msg("finished setting up mgmt routes")
|
||||
}
|
||||
|
||||
type Mgmt struct {
|
||||
Conf *config.Config
|
||||
Log log.Logger
|
||||
}
|
||||
|
||||
// mgmtHandler godoc
|
||||
// @Summary Get current server configuration
|
||||
// @Description Get current server configuration
|
||||
// @Router /v2/_zot/ext/mgmt [get]
|
||||
// @Router /v2/_zot/ext/mgmt [get]
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param resource query string false "specify resource" Enums(config)
|
||||
// @Success 200 {object} extensions.StrippedConfig
|
||||
// @Failure 500 {string} string "internal server error".
|
||||
func (mgmt *mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
sanitizedConfig := mgmt.config.Sanitize()
|
||||
// @Param resource query string false "specify resource" Enums(config)
|
||||
// @Success 200 {object} extensions.StrippedConfig
|
||||
// @Failure 500 {string} string "internal server error".
|
||||
func (mgmt *Mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
sanitizedConfig := mgmt.Conf.Sanitize()
|
||||
|
||||
buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
|
||||
if err != nil {
|
||||
mgmt.log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
|
||||
mgmt.Log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, _ = w.Write(buf)
|
||||
}
|
||||
|
||||
// mgmtHandler godoc
|
||||
// @Summary Upload certificates and public keys for verifying signatures
|
||||
// @Description Upload certificates and public keys for verifying signatures
|
||||
// @Router /v2/_zot/ext/mgmt [post]
|
||||
// @Accept octet-stream
|
||||
// @Produce json
|
||||
// @Param resource query string true "specify resource" Enums(signatures)
|
||||
// @Param tool query string true "specify signing tool" Enums(cosign, notation)
|
||||
// @Param truststoreType query string false "truststore type"
|
||||
// @Param truststoreName query string false "truststore name"
|
||||
// @Param requestBody body string true "Public key or Certificate content"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @Failure 400 {string} string "bad request".
|
||||
// @Failure 500 {string} string "internal server error".
|
||||
func HandleCertificatesAndPublicKeysUploads(response http.ResponseWriter, request *http.Request) {
|
||||
if !zcommon.QueryHasParams(request.URL.Query(), []string{"tool"}) {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tool := request.URL.Query().Get("tool")
|
||||
|
||||
switch tool {
|
||||
case signatures.CosignSignature:
|
||||
err := signatures.UploadPublicKey(body)
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
case signatures.NotationSignature:
|
||||
var truststoreType string
|
||||
|
||||
if !zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreName"}) {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreType"}) {
|
||||
truststoreType = request.URL.Query().Get("truststoreType")
|
||||
} else {
|
||||
truststoreType = "ca" // default value of "truststoreType" query param
|
||||
}
|
||||
|
||||
truststoreName := request.URL.Query().Get("truststoreName")
|
||||
|
||||
if truststoreType == "" || truststoreName == "" {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = signatures.UploadCertificate(body, truststoreType, truststoreName)
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func EnablePeriodicSignaturesVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
|
||||
metaDB mTypes.MetaDB, log log.Logger,
|
||||
) {
|
||||
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
|
||||
ctx := context.Background()
|
||||
|
||||
repos, err := metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool {
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
generator := &taskGeneratorSigValidity{
|
||||
repos: repos,
|
||||
metaDB: metaDB,
|
||||
repoIndex: -1,
|
||||
log: log,
|
||||
}
|
||||
|
||||
numberOfHours := 2
|
||||
interval := time.Duration(numberOfHours) * time.Minute
|
||||
taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
|
||||
}
|
||||
}
|
||||
|
||||
type taskGeneratorSigValidity struct {
|
||||
repos []mTypes.RepoMetadata
|
||||
metaDB mTypes.MetaDB
|
||||
repoIndex int
|
||||
done bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (gen *taskGeneratorSigValidity) Next() (scheduler.Task, error) {
|
||||
gen.repoIndex++
|
||||
|
||||
if gen.repoIndex >= len(gen.repos) {
|
||||
gen.done = true
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return NewValidityTask(gen.metaDB, gen.repos[gen.repoIndex], gen.log), nil
|
||||
}
|
||||
|
||||
func (gen *taskGeneratorSigValidity) IsDone() bool {
|
||||
return gen.done
|
||||
}
|
||||
|
||||
func (gen *taskGeneratorSigValidity) Reset() {
|
||||
gen.done = false
|
||||
gen.repoIndex = -1
|
||||
ctx := context.Background()
|
||||
|
||||
repos, err := gen.metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool { return true })
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gen.repos = repos
|
||||
}
|
||||
|
||||
type validityTask struct {
|
||||
metaDB mTypes.MetaDB
|
||||
repo mTypes.RepoMetadata
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewValidityTask(metaDB mTypes.MetaDB, repo mTypes.RepoMetadata, log log.Logger) *validityTask {
|
||||
return &validityTask{metaDB, repo, log}
|
||||
}
|
||||
|
||||
func (validityT *validityTask) DoWork() error {
|
||||
validityT.log.Info().Msg("updating signatures validity")
|
||||
|
||||
for signedManifest, sigs := range validityT.repo.Signatures {
|
||||
if len(sigs[signatures.CosignSignature]) != 0 || len(sigs[signatures.NotationSignature]) != 0 {
|
||||
err := validityT.metaDB.UpdateSignaturesValidity(validityT.repo.Name, digest.Digest(signedManifest))
|
||||
if err != nil {
|
||||
validityT.log.Info().Msg("error while verifying signatures")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validityT.log.Info().Msg("verifying signatures successfully completed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@ import (
|
|||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
"zotregistry.io/zot/pkg/scheduler"
|
||||
)
|
||||
|
||||
func IsBuiltWithMGMTExtension() bool {
|
||||
|
@ -20,10 +18,3 @@ func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger)
|
|||
log.Warn().Msg("skipping setting up mgmt routes because given zot binary doesn't include this feature," +
|
||||
"please build a binary that does so")
|
||||
}
|
||||
|
||||
func EnablePeriodicSignaturesVerification(config *config.Config, taskScheduler *scheduler.Scheduler,
|
||||
metaDB mTypes.MetaDB, log log.Logger,
|
||||
) {
|
||||
log.Warn().Msg("skipping adding to the scheduler a generator for updating signatures validity because " +
|
||||
"given binary doesn't include this feature, please build a binary that does so")
|
||||
}
|
||||
|
|
|
@ -156,20 +156,27 @@ func (trivyT *trivyTask) DoWork() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
|
||||
) {
|
||||
if !conf.IsSearchEnabled() {
|
||||
log.Info().Msg("skip enabling the search route as the config prerequisites are not met")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("setting up search routes")
|
||||
|
||||
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
|
||||
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
|
||||
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
||||
|
||||
extRouter := router.PathPrefix(constants.ExtSearch).Subrouter()
|
||||
extRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
|
||||
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
extRouter.Methods(allowedMethods...).
|
||||
Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig)))
|
||||
}
|
||||
extRouter := router.PathPrefix(constants.ExtSearchPrefix).Subrouter()
|
||||
extRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
|
||||
extRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
|
||||
extRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
extRouter.Methods(allowedMethods...).
|
||||
Handler(gqlHandler.NewDefaultServer(gql_generated.NewExecutableSchema(resConfig)))
|
||||
|
||||
log.Info().Msg("finished setting up search routes")
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
zcommon "zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
// content is our static web server content.
|
||||
|
@ -57,19 +57,38 @@ func addUISecurityHeaders(h http.Handler) http.HandlerFunc { //nolint:varnamelen
|
|||
}
|
||||
}
|
||||
|
||||
func SetupUIRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
func SetupUIRoutes(conf *config.Config, router *mux.Router,
|
||||
log log.Logger,
|
||||
) {
|
||||
if config.Extensions.UI != nil {
|
||||
fsub, _ := fs.Sub(content, "build")
|
||||
uih := uiHandler{log: log}
|
||||
if !conf.IsUIEnabled() {
|
||||
log.Info().Msg("skip enabling the ui route as the config prerequisites are not met")
|
||||
|
||||
router.PathPrefix("/login").Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/home").Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/explore").Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/image").Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/").Handler(addUISecurityHeaders(http.FileServer(http.FS(fsub))))
|
||||
|
||||
log.Info().Msg("setting up ui routes")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("setting up ui routes")
|
||||
|
||||
fsub, _ := fs.Sub(content, "build")
|
||||
uih := uiHandler{log: log}
|
||||
|
||||
// See https://go-review.googlesource.com/c/go/+/482635/2/src/net/http/fs.go
|
||||
// See https://github.com/golang/go/issues/59469
|
||||
// In go 1.20.4 they decided to allow any method in the FileServer handler.
|
||||
// In order to be consistent with the status codes returned when the UI is disabled
|
||||
// we need to be explicit about the methods we allow on UI routes.
|
||||
// If we don't add this, all unmatched http methods on any urls would match the UI routes.
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodGet)
|
||||
|
||||
router.PathPrefix("/login").Methods(allowedMethods...).
|
||||
Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/home").Methods(allowedMethods...).
|
||||
Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/explore").Methods(allowedMethods...).
|
||||
Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/image").Methods(allowedMethods...).
|
||||
Handler(addUISecurityHeaders(uih))
|
||||
router.PathPrefix("/").Methods(allowedMethods...).
|
||||
Handler(addUISecurityHeaders(http.FileServer(http.FS(fsub))))
|
||||
|
||||
log.Info().Msg("finished setting up ui routes")
|
||||
}
|
||||
|
|
|
@ -8,10 +8,9 @@ import (
|
|||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
func SetupUIRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
func SetupUIRoutes(conf *config.Config, router *mux.Router,
|
||||
log log.Logger,
|
||||
) {
|
||||
log.Warn().Msg("skipping setting up ui routes because given zot binary doesn't include this feature," +
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
zcommon "zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -27,37 +26,43 @@ func IsBuiltWithUserPrefsExtension() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
|
||||
func SetupUserPreferencesRoutes(conf *config.Config, router *mux.Router,
|
||||
metaDB mTypes.MetaDB, log log.Logger,
|
||||
) {
|
||||
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
|
||||
log.Info().Msg("setting up user preferences routes")
|
||||
if !conf.AreUserPrefsEnabled() {
|
||||
log.Info().Msg("skip enabling the user preferences route as the config prerequisites are not met")
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodPut)
|
||||
|
||||
userprefsRouter := router.PathPrefix(constants.ExtUserPreferences).Subrouter()
|
||||
userprefsRouter.Use(zcommon.ACHeadersHandler(config, allowedMethods...))
|
||||
userprefsRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
|
||||
userprefsRouter.HandleFunc("", HandleUserPrefs(metaDB, log)).Methods(allowedMethods...)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msg("setting up user preferences routes")
|
||||
|
||||
allowedMethods := zcommon.AllowedMethods(http.MethodPut)
|
||||
|
||||
userPrefsRouter := router.PathPrefix(constants.ExtUserPrefs).Subrouter()
|
||||
userPrefsRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
|
||||
userPrefsRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
||||
userPrefsRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
|
||||
userPrefsRouter.Methods(allowedMethods...).Handler(HandleUserPrefs(metaDB, log))
|
||||
|
||||
log.Info().Msg("finished setting up user preferences routes")
|
||||
}
|
||||
|
||||
// ListTags godoc
|
||||
// Repo preferences godoc
|
||||
// @Summary Add bookmarks/stars info
|
||||
// @Description Add bookmarks/stars info
|
||||
// @Router /v2/_zot/ext/userprefs [put]
|
||||
// @Router /v2/_zot/ext/userprefs [put]
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param action query string true "specify action" Enums(toggleBookmark, toggleStar)
|
||||
// @Param repo query string true "repository name"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 403 {string} string "forbidden"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Failure 400 {string} string "bad request".
|
||||
func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(rsp http.ResponseWriter, req *http.Request) {
|
||||
// @Param action query string true "specify action" Enums(toggleBookmark, toggleStar)
|
||||
// @Param repo query string true "repository name"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 403 {string} string "forbidden"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Failure 400 {string} string "bad request".
|
||||
func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) http.Handler {
|
||||
return http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) {
|
||||
if !zcommon.QueryHasParams(req.URL.Query(), []string{"action"}) {
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
|
@ -80,7 +85,7 @@ func HandleUserPrefs(metaDB mTypes.MetaDB, log log.Logger) func(w http.ResponseW
|
|||
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func PutStar(rsp http.ResponseWriter, req *http.Request, metaDB mTypes.MetaDB, log log.Logger) {
|
||||
|
|
|
@ -9,15 +9,14 @@ import (
|
|||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
func IsBuiltWithUserPrefsExtension() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
|
||||
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger,
|
||||
func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router,
|
||||
metaDB mTypes.MetaDB, log log.Logger,
|
||||
) {
|
||||
log.Warn().Msg("userprefs extension is disabled because given zot binary doesn't" +
|
||||
"include this feature please build a binary that does so")
|
||||
|
|
|
@ -36,11 +36,13 @@ func TestAllowedMethodsHeaderUserPrefs(t *testing.T) {
|
|||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
conf.HTTP.Port = port
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
@ -51,7 +53,7 @@ func TestAllowedMethodsHeaderUserPrefs(t *testing.T) {
|
|||
ctrlManager.StartAndWait(port)
|
||||
defer ctrlManager.StopServer()
|
||||
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullUserPreferencesPrefix)
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullUserPrefs)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "PUT,OPTIONS")
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build sync || metrics || mgmt || apikey
|
||||
// +build sync metrics mgmt apikey
|
||||
//go:build sync && metrics && mgmt && userprefs && search
|
||||
// +build sync,metrics,mgmt,userprefs,search
|
||||
|
||||
package extensions_test
|
||||
|
||||
|
@ -9,7 +9,6 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -22,10 +21,6 @@ import (
|
|||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
|
||||
"zotregistry.io/zot/pkg/extensions/monitoring"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
"zotregistry.io/zot/pkg/storage/local"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
||||
|
@ -125,6 +120,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mgmtReadyTimeout := 5 * time.Second
|
||||
|
||||
defaultValue := true
|
||||
|
||||
|
@ -142,16 +138,16 @@ func TestMgmtExtension(t *testing.T) {
|
|||
|
||||
mockOIDCConfig := mockOIDCServer.Config()
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with htpasswd", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -168,19 +164,31 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(err, ShouldBeNil)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
Convey("unsupported http method call", func() {
|
||||
// without credentials
|
||||
resp, err := resty.R().Patch(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
||||
})
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Patch(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
||||
|
||||
// without credentials
|
||||
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -193,7 +201,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
|
||||
|
||||
// with credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -206,12 +214,12 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
|
||||
|
||||
// with wrong credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "wrong").Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = resty.R().SetBasicAuth("test", "wrong").Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with ldap", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with ldap", t, func() {
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
BindDN: "binddn",
|
||||
BaseDN: "basedn",
|
||||
|
@ -219,11 +227,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -240,12 +248,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -258,7 +279,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd + ldap", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with htpasswd + ldap", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
|
@ -268,11 +289,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -289,12 +310,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -307,7 +341,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
|
||||
|
||||
// with credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -320,7 +354,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd + ldap + bearer", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with htpasswd + ldap + bearer", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
|
@ -335,11 +369,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -352,12 +386,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -372,7 +419,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
|
||||
|
||||
// with credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -387,7 +434,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with ldap + bearer", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with ldap + bearer", t, func() {
|
||||
conf.HTTP.Auth.HTPasswd.Path = ""
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
BindDN: "binddn",
|
||||
|
@ -401,11 +448,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -422,12 +469,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -442,7 +502,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with bearer", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with bearer", t, func() {
|
||||
conf.HTTP.Auth.HTPasswd.Path = ""
|
||||
conf.HTTP.Auth.LDAP = nil
|
||||
conf.HTTP.Auth.Bearer = &config.BearerConfig{
|
||||
|
@ -451,11 +511,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -468,12 +528,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -487,7 +560,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with openID", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with openID", t, func() {
|
||||
conf.HTTP.Auth.HTPasswd.Path = ""
|
||||
conf.HTTP.Auth.LDAP = nil
|
||||
conf.HTTP.Auth.Bearer = nil
|
||||
|
@ -504,11 +577,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -521,12 +594,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -541,7 +627,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.OpenID.Providers, ShouldNotBeEmpty)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with empty openID provider list", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled with empty openID provider list", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
|
@ -555,11 +641,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -572,12 +658,25 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -591,7 +690,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.OpenID, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled without any auth", t, func() {
|
||||
Convey("Verify mgmt auth info route enabled without any auth", t, func() {
|
||||
globalDir := t.TempDir()
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
|
@ -605,11 +704,11 @@ func TestMgmtExtension(t *testing.T) {
|
|||
conf.Commit = "v1.0.0"
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
@ -622,7 +721,7 @@ func TestMgmtExtension(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -634,158 +733,22 @@ func TestMgmtExtension(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled for uploading certificates and public keys", t, func() {
|
||||
globalDir := t.TempDir()
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
conf.HTTP.Port = port
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
defaultValue := true
|
||||
|
||||
conf.Commit = "v1.0.0"
|
||||
|
||||
imageStore := local.NewImageStore(globalDir, false, 0, false, false,
|
||||
log.NewLogger("debug", logFile.Name()), monitoring.NewMetricsServer(false,
|
||||
log.NewLogger("debug", logFile.Name())), nil, nil)
|
||||
|
||||
storeController := storage.StoreController{
|
||||
DefaultStore: imageStore,
|
||||
}
|
||||
|
||||
config, layers, manifest, err := test.GetRandomImageComponents(10) //nolint:staticcheck
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = test.WriteImageToFileSystem(
|
||||
test.Image{
|
||||
Manifest: manifest,
|
||||
Layers: layers,
|
||||
Config: config,
|
||||
}, "repo", "0.0.1", storeController,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sigConfig, sigLayers, sigManifest, err := test.GetRandomImageComponents(10) //nolint:staticcheck
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ref, _ := test.GetCosignSignatureTagForManifest(manifest)
|
||||
err = test.WriteImageToFileSystem(
|
||||
test.Image{
|
||||
Manifest: sigManifest,
|
||||
Layers: sigLayers,
|
||||
Config: sigConfig,
|
||||
}, "repo", ref, storeController,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
|
||||
conf.Log.Output = logFile.Name()
|
||||
defer os.Remove(logFile.Name()) // cleanup
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
rootDir := t.TempDir()
|
||||
|
||||
test.NotationPathLock.Lock()
|
||||
defer test.NotationPathLock.Unlock()
|
||||
|
||||
test.LoadNotationPath(rootDir)
|
||||
|
||||
// generate a keypair
|
||||
err = test.GenerateNotationCerts(rootDir, "test")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
certificateContent, err := os.ReadFile(path.Join(rootDir, "notation/localkeys", "test.crt"))
|
||||
So(err, ShouldBeNil)
|
||||
So(certificateContent, ShouldNotBeNil)
|
||||
|
||||
client := resty.New()
|
||||
resp, err := client.R().SetHeader("Content-type", "application/octet-stream").
|
||||
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "test").
|
||||
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
|
||||
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").
|
||||
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
|
||||
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "").
|
||||
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
|
||||
SetQueryParam("resource", "signatures").SetQueryParam("tool", "notation").SetQueryParam("truststoreName", "test").
|
||||
SetQueryParam("truststoreType", "signatureAuthority").
|
||||
SetBody([]byte("wrong content")).Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
|
||||
SetQueryParam("resource", "signatures").SetQueryParam("tool", "invalidTool").
|
||||
SetBody(certificateContent).Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().SetHeader("Content-type", "application/octet-stream").
|
||||
SetQueryParam("resource", "signatures").SetQueryParam("tool", "cosign").
|
||||
SetBody([]byte("wrong content")).Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
||||
|
||||
resp, err = client.R().SetQueryParam("resource", "signatures").SetQueryParam("tool", "cosign").
|
||||
Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().SetQueryParam("resource", "signatures").Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().SetQueryParam("resource", "config").Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
resp, err = client.R().SetQueryParam("resource", "invalid").Post(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
||||
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(), "setting up mgmt routes", time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"setting up mgmt routes", mgmtReadyTimeout)
|
||||
defer func() {
|
||||
if !found {
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
t.Log(string(data))
|
||||
}
|
||||
}()
|
||||
So(found, ShouldBeTrue)
|
||||
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "updating signatures validity", 10*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(), "verifying signatures successfully completed",
|
||||
time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
|
||||
"finished setting up mgmt routes", mgmtReadyTimeout)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -816,11 +779,11 @@ func TestMgmtWithBearer(t *testing.T) {
|
|||
defaultValue := true
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
Enable: &defaultValue,
|
||||
},
|
||||
}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultValue
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultValue
|
||||
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
|
@ -909,7 +872,7 @@ func TestMgmtWithBearer(t *testing.T) {
|
|||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
// test mgmt route
|
||||
resp, err = resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -923,7 +886,7 @@ func TestMgmtWithBearer(t *testing.T) {
|
|||
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
|
||||
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmtPrefix)
|
||||
resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
|
@ -946,11 +909,13 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
|
|||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
conf.HTTP.Port = port
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Mgmt: &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
@ -961,38 +926,9 @@ func TestAllowedMethodsHeaderMgmt(t *testing.T) {
|
|||
ctrlManager.StartAndWait(port)
|
||||
defer ctrlManager.StopServer()
|
||||
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullMgmtPrefix)
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullMgmt)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,OPTIONS")
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
|
||||
defaultVal := true
|
||||
|
||||
Convey("Test http options response", t, func() {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
conf.HTTP.Port = port
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
APIKey: &extconf.APIKeyConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
},
|
||||
}
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctrlManager := test.NewControllerManager(ctlr)
|
||||
|
||||
ctrlManager.StartAndWait(port)
|
||||
defer ctrlManager.StopServer()
|
||||
|
||||
resp, _ := resty.R().Options(baseURL + constants.FullAPIKeyPrefix)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "POST,DELETE,OPTIONS")
|
||||
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ import (
|
|||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
"zotregistry.io/zot/pkg/scheduler"
|
||||
)
|
||||
|
||||
func GetExtensions(config *config.Config) distext.ExtensionList {
|
||||
|
@ -13,18 +16,24 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
|
|||
endpoints := []string{}
|
||||
extensions := []distext.Extension{}
|
||||
|
||||
if config.Extensions != nil && config.Extensions.Search != nil {
|
||||
if IsBuiltWithSearchExtension() {
|
||||
endpoints = append(endpoints, constants.FullSearchPrefix)
|
||||
}
|
||||
|
||||
if IsBuiltWithUserPrefsExtension() {
|
||||
endpoints = append(endpoints, constants.FullUserPreferencesPrefix)
|
||||
}
|
||||
if config.IsNotationEnabled() && IsBuiltWithImageTrustExtension() {
|
||||
endpoints = append(endpoints, constants.FullNotation)
|
||||
}
|
||||
|
||||
if IsBuiltWithMGMTExtension() && config.Extensions != nil && config.Extensions.Mgmt != nil {
|
||||
endpoints = append(endpoints, constants.FullMgmtPrefix)
|
||||
if config.IsCosignEnabled() && IsBuiltWithImageTrustExtension() {
|
||||
endpoints = append(endpoints, constants.FullCosign)
|
||||
}
|
||||
|
||||
if config.IsSearchEnabled() && IsBuiltWithSearchExtension() {
|
||||
endpoints = append(endpoints, constants.FullSearchPrefix)
|
||||
}
|
||||
|
||||
if config.AreUserPrefsEnabled() && IsBuiltWithUserPrefsExtension() {
|
||||
endpoints = append(endpoints, constants.FullUserPrefs)
|
||||
}
|
||||
|
||||
if config.IsMgmtEnabled() && IsBuiltWithMGMTExtension() {
|
||||
endpoints = append(endpoints, constants.FullMgmt)
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
|
@ -40,3 +49,9 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
|
|||
|
||||
return extensionList
|
||||
}
|
||||
|
||||
func EnableScheduledTasks(conf *config.Config, taskScheduler *scheduler.Scheduler,
|
||||
metaDB mTypes.MetaDB, log log.Logger,
|
||||
) {
|
||||
EnableImageTrustVerification(conf, taskScheduler, metaDB, log)
|
||||
}
|
||||
|
|
|
@ -28,18 +28,12 @@ func TestGetExensionsDisabled(t *testing.T) {
|
|||
|
||||
defaultVal := true
|
||||
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
Mgmt: mgmtConfg,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
|
|
|
@ -83,9 +83,12 @@ func TestUserData(t *testing.T) {
|
|||
Actions: []string{"read", "create", "update"},
|
||||
},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
|
@ -134,7 +137,7 @@ func TestUserData(t *testing.T) {
|
|||
}
|
||||
}`
|
||||
|
||||
userprefsBaseURL := baseURL + constants.FullUserPreferencesPrefix
|
||||
userprefsBaseURL := baseURL + constants.FullUserPrefs
|
||||
|
||||
Convey("Flip starred repo authorized", func(c C) {
|
||||
clientHTTP := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
|
||||
|
@ -499,9 +502,12 @@ func TestChangingRepoState(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
gqlStarredRepos := `
|
||||
{
|
||||
|
@ -563,7 +569,7 @@ func TestChangingRepoState(t *testing.T) {
|
|||
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
|
||||
anonynousClient := resty.R()
|
||||
|
||||
userprefsBaseURL := baseURL + constants.FullUserPreferencesPrefix
|
||||
userprefsBaseURL := baseURL + constants.FullUserPrefs
|
||||
|
||||
Convey("PutStars", t, func() {
|
||||
resp, err := simpleUserClient.Put(userprefsBaseURL + PutRepoStarURL(accesibleRepo))
|
||||
|
@ -647,9 +653,12 @@ func TestGlobalSearchWithUserPrefFiltering(t *testing.T) {
|
|||
}
|
||||
|
||||
defaultVal := true
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
|
@ -657,7 +666,7 @@ func TestGlobalSearchWithUserPrefFiltering(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
preferencesBaseURL := baseURL + constants.FullUserPreferencesPrefix
|
||||
preferencesBaseURL := baseURL + constants.FullUserPrefs
|
||||
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
|
||||
|
||||
// ------ Add simple repo
|
||||
|
@ -840,9 +849,12 @@ func TestExpandedRepoInfoWithUserPrefs(t *testing.T) {
|
|||
}
|
||||
|
||||
defaultVal := true
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Search = &extconf.SearchConfig{}
|
||||
conf.Extensions.Search.Enable = &defaultVal
|
||||
conf.Extensions.Search.CVE = nil
|
||||
conf.Extensions.UI = &extconf.UIConfig{}
|
||||
conf.Extensions.UI.Enable = &defaultVal
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
|
@ -850,7 +862,7 @@ func TestExpandedRepoInfoWithUserPrefs(t *testing.T) {
|
|||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
preferencesBaseURL := baseURL + constants.FullUserPreferencesPrefix
|
||||
preferencesBaseURL := baseURL + constants.FullUserPrefs
|
||||
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
|
||||
|
||||
// ------ Add sbrepo and star/bookmark it
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -136,7 +137,7 @@ func UploadPublicKey(publicKeyContent []byte) error {
|
|||
func validatePublicKey(publicKeyContent []byte) (bool, error) {
|
||||
_, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyContent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidPublicKeyContent, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
|
|
@ -302,7 +302,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
|
|||
// data may be in DER format
|
||||
derCerts, err := x509.ParseCertificates(certificateContent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
|
||||
}
|
||||
|
||||
certs = append(certs, derCerts...)
|
||||
|
@ -311,7 +311,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
|
|||
for block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
block, rest = pem.Decode(rest)
|
||||
|
@ -319,7 +319,8 @@ func validateCertificate(certificateContent []byte) (bool, error) {
|
|||
}
|
||||
|
||||
if len(certs) == 0 {
|
||||
return false, zerr.ErrInvalidCertificateContent
|
||||
return false, fmt.Errorf("%w: no valid certificates found in payload",
|
||||
zerr.ErrInvalidCertificateContent)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package signatures
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
|
@ -8,6 +9,9 @@ import (
|
|||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
"zotregistry.io/zot/pkg/scheduler"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -58,3 +62,84 @@ func VerifySignature(
|
|||
return "", time.Time{}, false, zerr.ErrInvalidSignatureType
|
||||
}
|
||||
}
|
||||
|
||||
func NewTaskGenerator(metaDB mTypes.MetaDB, log log.Logger) scheduler.TaskGenerator {
|
||||
return &sigValidityTaskGenerator{
|
||||
repos: []mTypes.RepoMetadata{},
|
||||
metaDB: metaDB,
|
||||
repoIndex: -1,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type sigValidityTaskGenerator struct {
|
||||
repos []mTypes.RepoMetadata
|
||||
metaDB mTypes.MetaDB
|
||||
repoIndex int
|
||||
done bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (gen *sigValidityTaskGenerator) Next() (scheduler.Task, error) {
|
||||
if len(gen.repos) == 0 {
|
||||
ctx := context.Background()
|
||||
|
||||
repos, err := gen.metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool {
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gen.repos = repos
|
||||
}
|
||||
|
||||
gen.repoIndex++
|
||||
|
||||
if gen.repoIndex >= len(gen.repos) {
|
||||
gen.done = true
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return NewValidityTask(gen.metaDB, gen.repos[gen.repoIndex], gen.log), nil
|
||||
}
|
||||
|
||||
func (gen *sigValidityTaskGenerator) IsDone() bool {
|
||||
return gen.done
|
||||
}
|
||||
|
||||
func (gen *sigValidityTaskGenerator) Reset() {
|
||||
gen.done = false
|
||||
gen.repoIndex = -1
|
||||
gen.repos = []mTypes.RepoMetadata{}
|
||||
}
|
||||
|
||||
type validityTask struct {
|
||||
metaDB mTypes.MetaDB
|
||||
repo mTypes.RepoMetadata
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewValidityTask(metaDB mTypes.MetaDB, repo mTypes.RepoMetadata, log log.Logger) *validityTask {
|
||||
return &validityTask{metaDB, repo, log}
|
||||
}
|
||||
|
||||
func (validityT *validityTask) DoWork() error {
|
||||
validityT.log.Info().Msg("updating signatures validity")
|
||||
|
||||
for signedManifest, sigs := range validityT.repo.Signatures {
|
||||
if len(sigs[CosignSignature]) != 0 || len(sigs[NotationSignature]) != 0 {
|
||||
err := validityT.metaDB.UpdateSignaturesValidity(validityT.repo.Name, godigest.Digest(signedManifest))
|
||||
if err != nil {
|
||||
validityT.log.Info().Msg("error while verifying signatures")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validityT.log.Info().Msg("verifying signatures successfully completed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import (
|
|||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/resty.v1"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
@ -126,6 +127,17 @@ func MakeHtpasswdFile() string {
|
|||
return MakeHtpasswdFileFromString(content)
|
||||
}
|
||||
|
||||
func GetCredString(username, password string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
|
||||
|
||||
return usernameAndHash
|
||||
}
|
||||
|
||||
func MakeHtpasswdFileFromString(fileContent string) string {
|
||||
htpasswdFile, err := os.CreateTemp("", "htpasswd-")
|
||||
if err != nil {
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/opencontainers/image-spec/specs-go"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
|
@ -610,7 +609,7 @@ func TestUploadImage(t *testing.T) {
|
|||
|
||||
user1 := "test"
|
||||
password1 := "test"
|
||||
testString1 := getCredString(user1, password1)
|
||||
testString1 := test.GetCredString(user1, password1)
|
||||
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
||||
defer os.Remove(htpasswdPath)
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
|
@ -768,17 +767,6 @@ func TestUploadImage(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func getCredString(username, password string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
|
||||
|
||||
return usernameAndHash
|
||||
}
|
||||
|
||||
func TestInjectUploadImage(t *testing.T) {
|
||||
Convey("Inject failures for unreachable lines", t, func() {
|
||||
port := test.GetFreePort()
|
||||
|
@ -909,7 +897,7 @@ func TestInjectUploadImageWithBasicAuth(t *testing.T) {
|
|||
|
||||
user := "user"
|
||||
password := "password"
|
||||
testString := getCredString(user, password)
|
||||
testString := test.GetCredString(user, password)
|
||||
htpasswdPath := test.MakeHtpasswdFileFromString(testString)
|
||||
defer os.Remove(htpasswdPath)
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
|
|
237
swagger/docs.go
237
swagger/docs.go
|
@ -20,6 +20,126 @@ const docTemplate = `{
|
|||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/auth/apikey": {
|
||||
"post": {
|
||||
"description": "Can create an api key for a logged in user, based on the provided label and scopes.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Create an API key for the current user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "api token id (UUID)",
|
||||
"name": "id",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.APIKeyPayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Revokes one current user API key based on given key ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Revokes one current user API key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "api token id (UUID)",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"description": "Logout by removing current session",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Logout by removing current session",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oras/artifacts/v1/{name}/manifests/{digest}/referrers": {
|
||||
"get": {
|
||||
"description": "Get references for an image given a digest and artifact type",
|
||||
|
@ -141,6 +261,49 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v2/_zot/ext/cosign": {
|
||||
"post": {
|
||||
"description": "Upload cosign public keys for verifying signatures",
|
||||
"consumes": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Upload cosign public keys for verifying signatures",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Public key content",
|
||||
"name": "requestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/_zot/ext/mgmt": {
|
||||
"get": {
|
||||
"description": "Get current server configuration",
|
||||
|
@ -176,38 +339,19 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/v2/_zot/ext/notation": {
|
||||
"post": {
|
||||
"description": "Upload certificates and public keys for verifying signatures",
|
||||
"description": "Upload notation certificates for verifying signatures",
|
||||
"consumes": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Upload certificates and public keys for verifying signatures",
|
||||
"summary": "Upload notation certificates for verifying signatures",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"signatures"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "specify resource",
|
||||
"name": "resource",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"cosign",
|
||||
"notation"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "specify signing tool",
|
||||
"name": "tool",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "truststore type",
|
||||
|
@ -221,7 +365,7 @@ const docTemplate = `{
|
|||
"in": "query"
|
||||
},
|
||||
{
|
||||
"description": "Public key or Certificate content",
|
||||
"description": "Certificate content",
|
||||
"name": "requestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
|
@ -992,6 +1136,20 @@ const docTemplate = `{
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"api.APIKeyPayload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.ExtensionList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1013,6 +1171,10 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"artifactType": {
|
||||
"description": "ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact.",
|
||||
"type": "string"
|
||||
},
|
||||
"manifests": {
|
||||
"description": "Manifests references platform specific manifests.",
|
||||
"type": "array",
|
||||
|
@ -1027,6 +1189,14 @@ const docTemplate = `{
|
|||
"schemaVersion": {
|
||||
"description": "SchemaVersion is the image manifest schema that this image follows",
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"description": "Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/github_com_opencontainers_image-spec_specs-go_v1.Descriptor"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1118,6 +1288,9 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"openid": {
|
||||
"$ref": "#/definitions/extensions.OpenIDConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1160,6 +1333,20 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"extensions.OpenIDConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/extensions.OpenIDProviderConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions.OpenIDProviderConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"extensions.StrippedConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -11,6 +11,126 @@
|
|||
"version": "v1.1.0-dev"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/apikey": {
|
||||
"post": {
|
||||
"description": "Can create an api key for a logged in user, based on the provided label and scopes.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Create an API key for the current user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "api token id (UUID)",
|
||||
"name": "id",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.APIKeyPayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Revokes one current user API key based on given key ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Revokes one current user API key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "api token id (UUID)",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"description": "Logout by removing current session",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Logout by removing current session",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oras/artifacts/v1/{name}/manifests/{digest}/referrers": {
|
||||
"get": {
|
||||
"description": "Get references for an image given a digest and artifact type",
|
||||
|
@ -132,6 +252,49 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/v2/_zot/ext/cosign": {
|
||||
"post": {
|
||||
"description": "Upload cosign public keys for verifying signatures",
|
||||
"consumes": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Upload cosign public keys for verifying signatures",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Public key content",
|
||||
"name": "requestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/_zot/ext/mgmt": {
|
||||
"get": {
|
||||
"description": "Get current server configuration",
|
||||
|
@ -167,38 +330,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/v2/_zot/ext/notation": {
|
||||
"post": {
|
||||
"description": "Upload certificates and public keys for verifying signatures",
|
||||
"description": "Upload notation certificates for verifying signatures",
|
||||
"consumes": [
|
||||
"application/octet-stream"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Upload certificates and public keys for verifying signatures",
|
||||
"summary": "Upload notation certificates for verifying signatures",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"signatures"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "specify resource",
|
||||
"name": "resource",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"cosign",
|
||||
"notation"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "specify signing tool",
|
||||
"name": "tool",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "truststore type",
|
||||
|
@ -212,7 +356,7 @@
|
|||
"in": "query"
|
||||
},
|
||||
{
|
||||
"description": "Public key or Certificate content",
|
||||
"description": "Certificate content",
|
||||
"name": "requestBody",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
|
@ -983,6 +1127,20 @@
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"api.APIKeyPayload": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.ExtensionList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1004,6 +1162,10 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"artifactType": {
|
||||
"description": "ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact.",
|
||||
"type": "string"
|
||||
},
|
||||
"manifests": {
|
||||
"description": "Manifests references platform specific manifests.",
|
||||
"type": "array",
|
||||
|
@ -1018,6 +1180,14 @@
|
|||
"schemaVersion": {
|
||||
"description": "SchemaVersion is the image manifest schema that this image follows",
|
||||
"type": "integer"
|
||||
},
|
||||
"subject": {
|
||||
"description": "Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/github_com_opencontainers_image-spec_specs-go_v1.Descriptor"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1109,6 +1279,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"openid": {
|
||||
"$ref": "#/definitions/extensions.OpenIDConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1151,6 +1324,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"extensions.OpenIDConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/extensions.OpenIDProviderConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions.OpenIDProviderConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"extensions.StrippedConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
definitions:
|
||||
api.APIKeyPayload:
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
scopes:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
api.ExtensionList:
|
||||
properties:
|
||||
extensions:
|
||||
|
@ -13,6 +22,10 @@ definitions:
|
|||
type: string
|
||||
description: Annotations contains arbitrary metadata for the image index.
|
||||
type: object
|
||||
artifactType:
|
||||
description: ArtifactType specifies the IANA media type of artifact when the
|
||||
manifest is used for an artifact.
|
||||
type: string
|
||||
manifests:
|
||||
description: Manifests references platform specific manifests.
|
||||
items:
|
||||
|
@ -25,6 +38,12 @@ definitions:
|
|||
schemaVersion:
|
||||
description: SchemaVersion is the image manifest schema that this image follows
|
||||
type: integer
|
||||
subject:
|
||||
allOf:
|
||||
- $ref: '#/definitions/github_com_opencontainers_image-spec_specs-go_v1.Descriptor'
|
||||
description: Subject is an optional link from the image manifest to another
|
||||
manifest forming an association between the image manifest and the other
|
||||
manifest.
|
||||
type: object
|
||||
api.ImageManifest:
|
||||
properties:
|
||||
|
@ -89,6 +108,8 @@ definitions:
|
|||
address:
|
||||
type: string
|
||||
type: object
|
||||
openid:
|
||||
$ref: '#/definitions/extensions.OpenIDConfig'
|
||||
type: object
|
||||
extensions.BearerConfig:
|
||||
properties:
|
||||
|
@ -115,6 +136,15 @@ definitions:
|
|||
path:
|
||||
type: string
|
||||
type: object
|
||||
extensions.OpenIDConfig:
|
||||
properties:
|
||||
providers:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/extensions.OpenIDProviderConfig'
|
||||
type: object
|
||||
type: object
|
||||
extensions.OpenIDProviderConfig:
|
||||
type: object
|
||||
extensions.StrippedConfig:
|
||||
properties:
|
||||
binaryType:
|
||||
|
@ -206,6 +236,86 @@ info:
|
|||
title: Open Container Initiative Distribution Specification
|
||||
version: v1.1.0-dev
|
||||
paths:
|
||||
/auth/apikey:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Revokes one current user API key based on given key ID
|
||||
parameters:
|
||||
- description: api token id (UUID)
|
||||
in: query
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Revokes one current user API key
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Can create an api key for a logged in user, based on the provided
|
||||
label and scopes.
|
||||
parameters:
|
||||
- description: api token id (UUID)
|
||||
in: body
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api.APIKeyPayload'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: created
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Create an API key for the current user
|
||||
/auth/logout:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Logout by removing current session
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok".
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error".
|
||||
schema:
|
||||
type: string
|
||||
summary: Logout by removing current session
|
||||
/oras/artifacts/v1/{name}/manifests/{digest}/referrers:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -286,6 +396,34 @@ paths:
|
|||
schema:
|
||||
$ref: '#/definitions/api.ExtensionList'
|
||||
summary: List Registry level extensions
|
||||
/v2/_zot/ext/cosign:
|
||||
post:
|
||||
consumes:
|
||||
- application/octet-stream
|
||||
description: Upload cosign public keys for verifying signatures
|
||||
parameters:
|
||||
- description: Public key content
|
||||
in: body
|
||||
name: requestBody
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request".
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error".
|
||||
schema:
|
||||
type: string
|
||||
summary: Upload cosign public keys for verifying signatures
|
||||
/v2/_zot/ext/mgmt:
|
||||
get:
|
||||
consumes:
|
||||
|
@ -310,26 +448,12 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
summary: Get current server configuration
|
||||
/v2/_zot/ext/notation:
|
||||
post:
|
||||
consumes:
|
||||
- application/octet-stream
|
||||
description: Upload certificates and public keys for verifying signatures
|
||||
description: Upload notation certificates for verifying signatures
|
||||
parameters:
|
||||
- description: specify resource
|
||||
enum:
|
||||
- signatures
|
||||
in: query
|
||||
name: resource
|
||||
required: true
|
||||
type: string
|
||||
- description: specify signing tool
|
||||
enum:
|
||||
- cosign
|
||||
- notation
|
||||
in: query
|
||||
name: tool
|
||||
required: true
|
||||
type: string
|
||||
- description: truststore type
|
||||
in: query
|
||||
name: truststoreType
|
||||
|
@ -338,7 +462,7 @@ paths:
|
|||
in: query
|
||||
name: truststoreName
|
||||
type: string
|
||||
- description: Public key or Certificate content
|
||||
- description: Certificate content
|
||||
in: body
|
||||
name: requestBody
|
||||
required: true
|
||||
|
@ -359,7 +483,7 @@ paths:
|
|||
description: internal server error".
|
||||
schema:
|
||||
type: string
|
||||
summary: Upload certificates and public keys for verifying signatures
|
||||
summary: Upload notation certificates for verifying signatures
|
||||
/v2/_zot/ext/userprefs:
|
||||
put:
|
||||
consumes:
|
||||
|
|
|
@ -25,6 +25,9 @@ function setup_file() {
|
|||
"extensions": {
|
||||
"search": {
|
||||
"enable": true
|
||||
},
|
||||
"ui": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
|
|
Loading…
Reference in a new issue