- Read library ID from Target.Folder.ID everywhere - Guard scan queue enqueue against nil Folder - Simplify admin API key auth error plumbing
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
)
|
|
|
|
type adminAPIKeyContextKey string
|
|
|
|
const adminAPIKeyKey adminAPIKeyContextKey = "jellycompat_admin_api_key"
|
|
|
|
type apiKeyValidator interface {
|
|
GetByKey(ctx context.Context, key string) (*models.APIKey, error)
|
|
UpdateLastUsed(ctx context.Context, id int64) error
|
|
}
|
|
|
|
type apiKeyUserLoader interface {
|
|
GetByID(ctx context.Context, id int) (*models.User, error)
|
|
}
|
|
|
|
type AdminAPIKeyAuthenticator struct {
|
|
keys apiKeyValidator
|
|
users apiKeyUserLoader
|
|
}
|
|
|
|
type adminAPIKeyAuthResult struct {
|
|
ctx context.Context
|
|
status int
|
|
code string
|
|
message string
|
|
ok bool
|
|
}
|
|
|
|
func NewAdminAPIKeyAuthenticator(keys apiKeyValidator, users apiKeyUserLoader) *AdminAPIKeyAuthenticator {
|
|
if keys == nil || users == nil {
|
|
return nil
|
|
}
|
|
return &AdminAPIKeyAuthenticator{keys: keys, users: users}
|
|
}
|
|
|
|
func AdminAPIKeyFromContext(ctx context.Context) bool {
|
|
ok, _ := ctx.Value(adminAPIKeyKey).(bool)
|
|
return ok
|
|
}
|
|
|
|
func (a *AdminAPIKeyAuthenticator) RequireAdminAPIKey(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
result := a.authenticate(r)
|
|
if !result.ok {
|
|
writeError(w, result.status, result.code, result.message)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r.WithContext(result.ctx))
|
|
})
|
|
}
|
|
|
|
func RequireSessionOrAdminAPIKey(sessionAuth *Authenticator, keyAuth *AdminAPIKeyAuthenticator) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
token, ok := ExtractToken(r)
|
|
if ok && strings.HasPrefix(token, "sa_") {
|
|
result := keyAuth.authenticate(r)
|
|
if !result.ok {
|
|
writeError(w, result.status, result.code, result.message)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r.WithContext(result.ctx))
|
|
return
|
|
}
|
|
sessionAuth.RequireSession(next).ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (a *AdminAPIKeyAuthenticator) authenticate(r *http.Request) adminAPIKeyAuthResult {
|
|
unauthorized := adminAPIKeyAuthResult{
|
|
ctx: r.Context(),
|
|
status: http.StatusUnauthorized,
|
|
code: "Unauthorized",
|
|
message: "Invalid API key",
|
|
}
|
|
if a == nil || a.keys == nil || a.users == nil {
|
|
return unauthorized
|
|
}
|
|
token, ok := ExtractToken(r)
|
|
if !ok || !strings.HasPrefix(token, "sa_") {
|
|
return unauthorized
|
|
}
|
|
apiKey, err := a.keys.GetByKey(r.Context(), token)
|
|
if err != nil || apiKey == nil {
|
|
return unauthorized
|
|
}
|
|
user, err := a.users.GetByID(r.Context(), apiKey.UserID)
|
|
if err != nil || user == nil || !user.Enabled {
|
|
return unauthorized
|
|
}
|
|
if user.Role != "admin" {
|
|
return adminAPIKeyAuthResult{
|
|
ctx: r.Context(),
|
|
status: http.StatusForbidden,
|
|
code: "Forbidden",
|
|
message: "Admin access required",
|
|
}
|
|
}
|
|
go func(id int64) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := a.keys.UpdateLastUsed(ctx, id); err != nil {
|
|
slog.Debug("jellycompat api key last-used update failed", "id", id, "error", err)
|
|
}
|
|
}(apiKey.ID)
|
|
return adminAPIKeyAuthResult{
|
|
ctx: context.WithValue(r.Context(), adminAPIKeyKey, true),
|
|
status: http.StatusOK,
|
|
ok: true,
|
|
}
|
|
}
|