All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m43s
189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"warpbox/lib/userstore"
|
|
)
|
|
|
|
const userSessionCookie = "warpbox_user_session"
|
|
|
|
type requestActor struct {
|
|
User userstore.User
|
|
FromAPIKey bool
|
|
KeyID string
|
|
}
|
|
|
|
func requestBearerToken(ctx *gin.Context) string {
|
|
auth := strings.TrimSpace(ctx.GetHeader("Authorization"))
|
|
if !strings.HasPrefix(strings.ToLower(auth), "bearer ") {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(auth[7:])
|
|
}
|
|
|
|
func (app *App) sessionSecret() string {
|
|
return app.config.AdminUsername + "|" + app.config.AdminPassword + "|warpbox"
|
|
}
|
|
|
|
func (app *App) signSessionToken(userID string, expiresAt time.Time) string {
|
|
payload := userID + "|" + expiresAt.UTC().Format(time.RFC3339)
|
|
mac := hmac.New(sha256.New, []byte(app.sessionSecret()))
|
|
mac.Write([]byte(payload))
|
|
sig := hex.EncodeToString(mac.Sum(nil))
|
|
return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + sig
|
|
}
|
|
|
|
func (app *App) parseSessionToken(token string) (string, bool) {
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 2 {
|
|
return "", false
|
|
}
|
|
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
payload := string(payloadBytes)
|
|
mac := hmac.New(sha256.New, []byte(app.sessionSecret()))
|
|
mac.Write([]byte(payload))
|
|
expectedSig := hex.EncodeToString(mac.Sum(nil))
|
|
if !hmac.Equal([]byte(expectedSig), []byte(parts[1])) {
|
|
return "", false
|
|
}
|
|
items := strings.Split(payload, "|")
|
|
if len(items) != 2 {
|
|
return "", false
|
|
}
|
|
expiresAt, err := time.Parse(time.RFC3339, items[1])
|
|
if err != nil || time.Now().UTC().After(expiresAt) {
|
|
return "", false
|
|
}
|
|
return items[0], true
|
|
}
|
|
|
|
func (app *App) resolveActor(ctx *gin.Context) (*requestActor, bool) {
|
|
if app.userStore == nil {
|
|
return nil, false
|
|
}
|
|
if rawKey := requestBearerToken(ctx); rawKey != "" {
|
|
user, key, ok := app.userStore.FindByAPIKey(rawKey)
|
|
if ok {
|
|
app.userStore.TouchAPIKey(user.ID, key.ID)
|
|
return &requestActor{User: user, FromAPIKey: true, KeyID: key.ID}, true
|
|
}
|
|
return nil, false
|
|
}
|
|
if token, err := ctx.Cookie(userSessionCookie); err == nil {
|
|
if userID, ok := app.parseSessionToken(token); ok {
|
|
if user, found := app.userStore.FindByID(userID); found {
|
|
app.userStore.TouchUser(user.ID)
|
|
return &requestActor{User: user}, true
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (app *App) denyActor(ctx *gin.Context, status int, message string) bool {
|
|
ctx.JSON(status, gin.H{"error": message})
|
|
return false
|
|
}
|
|
|
|
func (app *App) authorizeUpload(ctx *gin.Context) (*requestActor, bool) {
|
|
actor, ok := app.resolveActor(ctx)
|
|
if !ok {
|
|
if requestBearerToken(ctx) != "" {
|
|
return nil, app.denyActor(ctx, http.StatusUnauthorized, "Invalid API key")
|
|
}
|
|
return nil, true
|
|
}
|
|
if actor.User.Status != userstore.StatusActive {
|
|
return nil, app.denyActor(ctx, http.StatusForbidden, "User account is disabled")
|
|
}
|
|
if !actor.User.Permissions.CanUseAPI {
|
|
return nil, app.denyActor(ctx, http.StatusForbidden, "API access is not allowed for this user")
|
|
}
|
|
if !actor.User.Permissions.CanCreateBox {
|
|
return nil, app.denyActor(ctx, http.StatusForbidden, "Creating boxes is not allowed for this user")
|
|
}
|
|
if !actor.User.Permissions.CanUploadFile {
|
|
return nil, app.denyActor(ctx, http.StatusForbidden, "Uploading files is not allowed for this user")
|
|
}
|
|
return actor, true
|
|
}
|
|
|
|
func (app *App) handleUserLogin(ctx *gin.Context) {
|
|
var payload struct {
|
|
APIKey string `json:"api_key"`
|
|
}
|
|
if err := ctx.ShouldBindJSON(&payload); err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid login payload"})
|
|
return
|
|
}
|
|
if app.userStore == nil {
|
|
ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"})
|
|
return
|
|
}
|
|
user, key, ok := app.userStore.FindByAPIKey(payload.APIKey)
|
|
if !ok {
|
|
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
|
|
return
|
|
}
|
|
if user.Status != userstore.StatusActive {
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "User account is disabled"})
|
|
return
|
|
}
|
|
if !user.Permissions.CanUseWeb {
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "Web access is not allowed for this user"})
|
|
return
|
|
}
|
|
app.userStore.TouchAPIKey(user.ID, key.ID)
|
|
expiresAt := time.Now().UTC().Add(time.Duration(app.config.SessionTTLSeconds) * time.Second)
|
|
ctx.SetCookie(userSessionCookie, app.signSessionToken(user.ID, expiresAt), int(app.config.SessionTTLSeconds), "/", "", false, true)
|
|
ctx.JSON(http.StatusOK, gin.H{"ok": true, "user": gin.H{"id": user.ID, "email": user.Email, "username": user.Username}})
|
|
}
|
|
|
|
func (app *App) handleUserLogout(ctx *gin.Context) {
|
|
ctx.SetCookie(userSessionCookie, "", -1, "/", "", false, true)
|
|
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (app *App) handleUserMe(ctx *gin.Context) {
|
|
actor, ok := app.resolveActor(ctx)
|
|
if !ok || actor == nil {
|
|
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"user": toAdminUserView(actor.User)})
|
|
}
|
|
|
|
func (app *App) handleSelfCreateAPIKey(ctx *gin.Context) {
|
|
actor, ok := app.resolveActor(ctx)
|
|
if !ok || actor == nil {
|
|
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
|
|
return
|
|
}
|
|
if actor.User.Status != userstore.StatusActive {
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "User account is disabled"})
|
|
return
|
|
}
|
|
var payload struct {
|
|
Name string `json:"name"`
|
|
}
|
|
_ = ctx.ShouldBindJSON(&payload)
|
|
key, raw, err := app.userStore.CreateAPIKey(actor.User.ID, payload.Name)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, gin.H{"ok": true, "api_key": raw, "key": toAdminAPIKeyView(key)})
|
|
}
|