mirror of
https://github.com/project-zot/zot.git
synced 2025-01-20 22:52:51 -05:00
198 lines
5.2 KiB
Go
198 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"
|
||
|
"zotregistry.io/zot/pkg/meta/repodb"
|
||
|
)
|
||
|
|
||
|
func SetupAPIKeyRoutes(config *config.Config, router *mux.Router, repoDB repodb.RepoDB,
|
||
|
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(repoDB, cookieStore, log))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type APIKeyPayload struct { //nolint:revive
|
||
|
Label string `json:"label"`
|
||
|
Scopes []string `json:"scopes"`
|
||
|
}
|
||
|
|
||
|
func HandleAPIKeyRequest(repoDB repodb.RepoDB, 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, repoDB, cookieStore, log) //nolint:contextcheck
|
||
|
|
||
|
return
|
||
|
case http.MethodDelete:
|
||
|
RevokeAPIKey(resp, req, repoDB, 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, repoDB repodb.RepoDB,
|
||
|
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 := &repodb.APIKeyDetails{
|
||
|
CreatedAt: time.Now(),
|
||
|
LastUsed: time.Now(),
|
||
|
CreatorUA: req.UserAgent(),
|
||
|
GeneratedBy: "manual",
|
||
|
Label: payload.Label,
|
||
|
Scopes: payload.Scopes,
|
||
|
UUID: apiKeyID.String(),
|
||
|
}
|
||
|
|
||
|
err = repoDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)
|
||
|
if err != nil {
|
||
|
log.Error().Err(err).Msg("error storing API key")
|
||
|
resp.WriteHeader(http.StatusInternalServerError)
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
apiKeyResponse := struct {
|
||
|
repodb.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, repoDB repodb.RepoDB,
|
||
|
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 := repoDB.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()
|
||
|
}
|