hub/internal/apikey/manager.go

164 lines
4.6 KiB
Go

package apikey
import (
"context"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/util"
"github.com/jackc/pgx/v4"
"github.com/satori/uuid"
"golang.org/x/crypto/bcrypt"
)
const (
// Database queries
addAPIKeyDBQ = `select add_api_key($1::jsonb)`
deleteAPIKeyDBQ = `select delete_api_key($1::uuid, $2::uuid)`
getAPIKeyDBQ = `select get_api_key($1::uuid, $2::uuid)`
getAPIKeyUserIDDBQ = `select user_id, secret from api_key where api_key_id = $1`
getUserAPIKeysDBQ = `select get_user_api_keys($1::uuid)`
updateAPIKeyDBQ = `select update_api_key($1::jsonb)`
)
// Manager provides an API to manage api keys.
type Manager struct {
db hub.DB
}
// NewManager creates a new Manager instance.
func NewManager(db hub.DB) *Manager {
return &Manager{
db: db,
}
}
// Add adds the provided api key to the database.
func (m *Manager) Add(ctx context.Context, ak *hub.APIKey) (*hub.APIKey, error) {
ak.UserID = ctx.Value(hub.UserIDKey).(string)
// Validate input
if ak.Name == "" {
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "name not provided")
}
// Generate API key secret
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return nil, err
}
apiKeySecret := base64.StdEncoding.EncodeToString(randomBytes)
apiKeySecretHashed := fmt.Sprintf("%x", sha512.Sum512([]byte(apiKeySecret)))
// Add api key to the database
var apiKeyID string
ak.Secret = apiKeySecretHashed
akJSON, _ := json.Marshal(ak)
if err := m.db.QueryRow(ctx, addAPIKeyDBQ, akJSON).Scan(&apiKeyID); err != nil {
return nil, err
}
return &hub.APIKey{
APIKeyID: apiKeyID,
Secret: apiKeySecret,
}, nil
}
// Check checks if the api key provided is valid.
func (m *Manager) Check(ctx context.Context, apiKeyID, apiKeySecret string) (*hub.CheckAPIKeyOutput, error) {
// Validate input
if apiKeyID == "" || apiKeySecret == "" {
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "api key id or secret not provided")
}
// Get key's user id and secret from database
var userID, apiKeySecretHashed string
err := m.db.QueryRow(ctx, getAPIKeyUserIDDBQ, apiKeyID).Scan(&userID, &apiKeySecretHashed)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return &hub.CheckAPIKeyOutput{Valid: false}, nil
}
return nil, err
}
// Check if the secret provided is valid
switch {
case strings.HasPrefix(apiKeySecretHashed, "$2a$"):
// Bcrypt hash, will be deprecated soon
err = bcrypt.CompareHashAndPassword([]byte(apiKeySecretHashed), []byte(apiKeySecret))
if err != nil {
return &hub.CheckAPIKeyOutput{Valid: false}, nil
}
default:
// SHA512 hash
if fmt.Sprintf("%x", sha512.Sum512([]byte(apiKeySecret))) != apiKeySecretHashed {
return &hub.CheckAPIKeyOutput{Valid: false}, nil
}
}
return &hub.CheckAPIKeyOutput{
Valid: true,
UserID: userID,
}, nil
}
// Delete deletes the provided api key from the database.
func (m *Manager) Delete(ctx context.Context, apiKeyID string) error {
userID := ctx.Value(hub.UserIDKey).(string)
// Validate input
if _, err := uuid.FromString(apiKeyID); err != nil {
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid api key id")
}
// Delete api key from database
_, err := m.db.Exec(ctx, deleteAPIKeyDBQ, userID, apiKeyID)
return err
}
// GetJSON returns the requested api key as a json object.
func (m *Manager) GetJSON(ctx context.Context, apiKeyID string) ([]byte, error) {
userID := ctx.Value(hub.UserIDKey).(string)
// Validate input
if _, err := uuid.FromString(apiKeyID); err != nil {
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid api key id")
}
// Get api key from database
return util.DBQueryJSON(ctx, m.db, getAPIKeyDBQ, userID, apiKeyID)
}
// GetOwnedByUserJSON returns the api keys belonging to the requesting user as
// a json array.
func (m *Manager) GetOwnedByUserJSON(ctx context.Context) ([]byte, error) {
userID := ctx.Value(hub.UserIDKey).(string)
// Get api keys from database
return util.DBQueryJSON(ctx, m.db, getUserAPIKeysDBQ, userID)
}
// Update updates the provided api key in the database.
func (m *Manager) Update(ctx context.Context, ak *hub.APIKey) error {
ak.UserID = ctx.Value(hub.UserIDKey).(string)
// Validate input
if _, err := uuid.FromString(ak.APIKeyID); err != nil {
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid api key id")
}
if ak.Name == "" {
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "name not provided")
}
// Update api key in database
akJSON, _ := json.Marshal(ak)
_, err := m.db.Exec(ctx, updateAPIKeyDBQ, akJSON)
return err
}