0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-13 22:50:38 -05:00
zot/pkg/extensions/extension_api_key.go
peusebiu 17d1338af1
feat: integrate openID auth logic and user profile management (#1381)
This change introduces OpenID authn by using providers such as Github,
Gitlab, Google and Dex.
User sessions are now used for web clients to identify
and persist an authenticated users session, thus not requiring every request to
use credentials.
Another change is apikey feature, users can create/revoke their api keys and use them
to authenticate when using cli clients such as skopeo.

eg:
login:
/auth/login?provider=github
/auth/login?provider=gitlab
and so on

logout:
/auth/logout

redirectURL:
/auth/callback/github
/auth/callback/gitlab
and so on

If network policy doesn't allow inbound connections, this callback wont work!

for more info read documentation added in this commit.

Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
Co-authored-by: Alex Stan <alexandrustan96@yahoo.ro>
2023-07-07 09:27:10 -07:00

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"
"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()
}