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