2023-03-09 13:43:26 -05:00
|
|
|
//go:build mgmt
|
|
|
|
// +build mgmt
|
|
|
|
|
|
|
|
package extensions
|
|
|
|
|
|
|
|
import (
|
2023-07-06 06:57:59 -05:00
|
|
|
"context"
|
2023-03-09 13:43:26 -05:00
|
|
|
"encoding/json"
|
2023-07-06 06:57:59 -05:00
|
|
|
"io"
|
2023-03-09 13:43:26 -05:00
|
|
|
"net/http"
|
2023-07-06 06:57:59 -05:00
|
|
|
"time"
|
2023-03-09 13:43:26 -05:00
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
2023-07-06 06:57:59 -05:00
|
|
|
"github.com/opencontainers/go-digest"
|
2023-03-09 13:43:26 -05:00
|
|
|
|
|
|
|
"zotregistry.io/zot/pkg/api/config"
|
|
|
|
"zotregistry.io/zot/pkg/api/constants"
|
2023-05-25 13:44:54 -05:00
|
|
|
zcommon "zotregistry.io/zot/pkg/common"
|
2023-03-09 13:43:26 -05:00
|
|
|
"zotregistry.io/zot/pkg/log"
|
2023-07-06 06:57:59 -05:00
|
|
|
"zotregistry.io/zot/pkg/meta/repodb"
|
|
|
|
"zotregistry.io/zot/pkg/meta/signatures"
|
|
|
|
"zotregistry.io/zot/pkg/scheduler"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
ConfigResource = "config"
|
|
|
|
SignaturesResource = "signatures"
|
2023-03-09 13:43:26 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
type HTPasswd struct {
|
|
|
|
Path string `json:"path,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type BearerConfig struct {
|
|
|
|
Realm string `json:"realm,omitempty"`
|
|
|
|
Service string `json:"service,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Auth struct {
|
|
|
|
HTPasswd *HTPasswd `json:"htpasswd,omitempty" mapstructure:"htpasswd"`
|
|
|
|
Bearer *BearerConfig `json:"bearer,omitempty" mapstructure:"bearer"`
|
|
|
|
LDAP *struct {
|
|
|
|
Address string `json:"address,omitempty" mapstructure:"address"`
|
|
|
|
} `json:"ldap,omitempty" mapstructure:"ldap"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type StrippedConfig struct {
|
|
|
|
DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"`
|
|
|
|
BinaryType string `json:"binaryType" mapstructure:"binaryType"`
|
|
|
|
HTTP struct {
|
|
|
|
Auth *Auth `json:"auth,omitempty" mapstructure:"auth"`
|
|
|
|
} `json:"http" mapstructure:"http"`
|
|
|
|
}
|
|
|
|
|
2023-05-25 06:46:52 -05:00
|
|
|
func IsBuiltWithMGMTExtension() bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-03-09 13:43:26 -05:00
|
|
|
func (auth Auth) MarshalJSON() ([]byte, error) {
|
|
|
|
type localAuth Auth
|
|
|
|
|
|
|
|
if auth.Bearer == nil && auth.LDAP == nil &&
|
|
|
|
auth.HTPasswd.Path == "" {
|
|
|
|
auth.HTPasswd = nil
|
|
|
|
|
|
|
|
return json.Marshal((localAuth)(auth))
|
|
|
|
}
|
|
|
|
|
|
|
|
if auth.HTPasswd.Path == "" && auth.LDAP == nil {
|
|
|
|
auth.HTPasswd = nil
|
|
|
|
} else {
|
|
|
|
auth.HTPasswd.Path = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
auth.LDAP = nil
|
|
|
|
|
|
|
|
return json.Marshal((localAuth)(auth))
|
|
|
|
}
|
|
|
|
|
|
|
|
type mgmt struct {
|
|
|
|
config *config.Config
|
|
|
|
log log.Logger
|
|
|
|
}
|
|
|
|
|
2023-05-12 11:43:14 -05:00
|
|
|
func (mgmt *mgmt) handler() http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-07-06 06:57:59 -05:00
|
|
|
var resource string
|
|
|
|
|
|
|
|
if 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
|
2023-05-12 11:43:14 -05:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2023-03-09 13:43:26 -05:00
|
|
|
|
|
|
|
func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger) {
|
|
|
|
if config.Extensions.Mgmt != nil && *config.Extensions.Mgmt.Enable {
|
|
|
|
log.Info().Msg("setting up mgmt routes")
|
|
|
|
|
|
|
|
mgmt := mgmt{config: config, log: log}
|
|
|
|
|
2023-07-06 06:57:59 -05:00
|
|
|
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)
|
2023-05-25 13:44:54 -05:00
|
|
|
|
|
|
|
mgmtRouter := router.PathPrefix(constants.ExtMgmt).Subrouter()
|
|
|
|
mgmtRouter.Use(zcommon.ACHeadersHandler(allowedMethods...))
|
|
|
|
mgmtRouter.Use(zcommon.AddExtensionSecurityHeaders())
|
|
|
|
mgmtRouter.Methods(allowedMethods...).Handler(mgmt.handler())
|
2023-03-09 13:43:26 -05:00
|
|
|
}
|
|
|
|
}
|
2023-07-06 06:57:59 -05:00
|
|
|
|
|
|
|
// mgmtHandler godoc
|
|
|
|
// @Summary Get current server configuration
|
|
|
|
// @Description Get current server configuration
|
|
|
|
// @Router /v2/_zot/ext/mgmt [get]
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param resource query string false "specify resource" Enums(config)
|
|
|
|
// @Success 200 {object} extensions.StrippedConfig
|
|
|
|
// @Failure 500 {string} string "internal server error".
|
|
|
|
func (mgmt *mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|
|
|
sanitizedConfig := mgmt.config.Sanitize()
|
|
|
|
|
|
|
|
buf, err := zcommon.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
|
|
|
|
if err != nil {
|
|
|
|
mgmt.log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, _ = w.Write(buf)
|
|
|
|
}
|
|
|
|
|
|
|
|
// mgmtHandler godoc
|
|
|
|
// @Summary Upload certificates and public keys for verifying signatures
|
|
|
|
// @Description Upload certificates and public keys for verifying signatures
|
|
|
|
// @Router /v2/_zot/ext/mgmt [post]
|
|
|
|
// @Accept octet-stream
|
|
|
|
// @Produce json
|
|
|
|
// @Param resource query string true "specify resource" Enums(signatures)
|
|
|
|
// @Param tool query string true "specify signing tool" Enums(cosign, notation)
|
|
|
|
// @Param truststoreType query string false "truststore type"
|
|
|
|
// @Param truststoreName query string false "truststore name"
|
|
|
|
// @Param requestBody body string true "Public key or Certificate content"
|
|
|
|
// @Success 200 {string} string "ok"
|
|
|
|
// @Failure 400 {string} string "bad request".
|
|
|
|
// @Failure 500 {string} string "internal server error".
|
|
|
|
func HandleCertificatesAndPublicKeysUploads(response http.ResponseWriter, request *http.Request) {
|
|
|
|
if !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 !queryHasParams(request.URL.Query(), []string{"truststoreName"}) {
|
|
|
|
response.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if 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,
|
|
|
|
repoDB repodb.RepoDB, log log.Logger,
|
|
|
|
) {
|
|
|
|
if config.Extensions.Search != nil && *config.Extensions.Search.Enable {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
repos, err := repoDB.GetMultipleRepoMeta(ctx, func(repoMeta repodb.RepoMetadata) bool {
|
|
|
|
return true
|
|
|
|
}, repodb.PageInput{})
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
generator := &taskGeneratorSigValidity{
|
|
|
|
repos: repos,
|
|
|
|
repoDB: repoDB,
|
|
|
|
repoIndex: -1,
|
|
|
|
log: log,
|
|
|
|
}
|
|
|
|
|
|
|
|
numberOfHours := 2
|
|
|
|
interval := time.Duration(numberOfHours) * time.Minute
|
|
|
|
taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type taskGeneratorSigValidity struct {
|
|
|
|
repos []repodb.RepoMetadata
|
|
|
|
repoDB repodb.RepoDB
|
|
|
|
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.repoDB, 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.repoDB.GetMultipleRepoMeta(ctx, func(repoMeta repodb.RepoMetadata) bool {
|
|
|
|
return true
|
|
|
|
}, repodb.PageInput{})
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
gen.repos = repos
|
|
|
|
}
|
|
|
|
|
|
|
|
type validityTask struct {
|
|
|
|
repoDB repodb.RepoDB
|
|
|
|
repo repodb.RepoMetadata
|
|
|
|
log log.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewValidityTask(repoDB repodb.RepoDB, repo repodb.RepoMetadata, log log.Logger) *validityTask {
|
|
|
|
return &validityTask{repoDB, 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.repoDB.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
|
|
|
|
}
|