mirror of
https://github.com/project-zot/zot.git
synced 2025-01-06 22:40:28 -05:00
28de980319
Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
197 lines
5.2 KiB
Go
197 lines
5.2 KiB
Go
//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(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()
|
|
}
|