feat(users): add account limits and API keys
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m43s

This commit is contained in:
2026-05-04 02:27:36 +03:00
parent dc379ea6a6
commit d7cbba1bf2
14 changed files with 1688 additions and 271 deletions

View File

@@ -2,10 +2,82 @@ package server
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"warpbox/lib/userstore"
)
const bytesPerMegabyte = 1024 * 1024
type adminUserView struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Status string `json:"status"`
Permissions userstore.Permissions `json:"permissions"`
Limits userstore.Limits `json:"limits"`
APIKeys []adminAPIKeyView `json:"api_keys"`
APIKeyCount int `json:"api_key_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
LastSeenAt string `json:"last_seen_at"`
}
type adminAPIKeyView struct {
ID string `json:"id"`
Name string `json:"name"`
Prefix string `json:"prefix"`
CreatedAt string `json:"created_at"`
LastUsedAt string `json:"last_used_at"`
RevokedAt string `json:"revoked_at"`
}
func formatMaybeTime(value *time.Time) string {
if value == nil || value.IsZero() {
return ""
}
return value.UTC().Format(time.RFC3339)
}
func toAdminAPIKeyView(key userstore.APIKey) adminAPIKeyView {
return adminAPIKeyView{
ID: key.ID,
Name: key.Name,
Prefix: key.Prefix,
CreatedAt: key.CreatedAt.UTC().Format(time.RFC3339),
LastUsedAt: formatMaybeTime(key.LastUsedAt),
RevokedAt: formatMaybeTime(key.RevokedAt),
}
}
func toAdminAPIKeyViews(keys []userstore.APIKey) []adminAPIKeyView {
views := make([]adminAPIKeyView, 0, len(keys))
for _, key := range keys {
views = append(views, toAdminAPIKeyView(key))
}
return views
}
func toAdminUserView(user userstore.User) adminUserView {
return adminUserView{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Status: user.Status,
Permissions: user.Permissions,
Limits: user.Limits,
APIKeys: toAdminAPIKeyViews(user.APIKeys),
APIKeyCount: len(user.APIKeys),
CreatedAt: user.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: user.UpdatedAt.UTC().Format(time.RFC3339),
LastSeenAt: formatMaybeTime(user.LastSeenAt),
}
}
func (app *App) handleAdminUsers(ctx *gin.Context) {
if !app.adminLoginEnabled() {
ctx.Redirect(http.StatusSeeOther, "/")
@@ -18,3 +90,154 @@ func (app *App) handleAdminUsers(ctx *gin.Context) {
"ActivePage": "users",
})
}
func (app *App) handleAdminUsersList(ctx *gin.Context) {
if app.userStore == nil {
ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"})
return
}
users := app.userStore.List()
items := make([]adminUserView, 0, len(users))
for _, user := range users {
items = append(items, toAdminUserView(user))
}
ctx.JSON(http.StatusOK, gin.H{"users": items})
}
func parseInt64OrZero(value string) int64 {
value = strings.TrimSpace(value)
if value == "" {
return 0
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed < 0 {
return 0
}
return parsed
}
func parseMegabytesToBytesOrZero(value string) int64 {
megabytes := parseInt64OrZero(value)
if megabytes <= 0 {
return 0
}
return megabytes * bytesPerMegabyte
}
func (app *App) handleAdminUsersSave(ctx *gin.Context) {
if app.userStore == nil {
ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"})
return
}
var payload struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Status string `json:"status"`
MaxFileMB string `json:"max_file_size_mb"`
MaxBoxMB string `json:"max_box_size_mb"`
MaxFileSize string `json:"max_file_size_bytes"`
MaxBoxSize string `json:"max_box_size_bytes"`
Permissions struct {
CanUseWeb bool `json:"can_use_web"`
CanUseAPI bool `json:"can_use_api"`
CanCreateBox bool `json:"can_create_box"`
CanUploadFile bool `json:"can_upload_file"`
} `json:"permissions"`
}
if err := ctx.ShouldBindJSON(&payload); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user payload"})
return
}
permissions := userstore.Permissions{
CanUseWeb: payload.Permissions.CanUseWeb,
CanUseAPI: payload.Permissions.CanUseAPI,
CanCreateBox: payload.Permissions.CanCreateBox,
CanUploadFile: payload.Permissions.CanUploadFile,
}
limits := userstore.Limits{
MaxFileSizeBytes: parseMegabytesToBytesOrZero(payload.MaxFileMB),
MaxBoxSizeBytes: parseMegabytesToBytesOrZero(payload.MaxBoxMB),
}
if limits.MaxFileSizeBytes == 0 && strings.TrimSpace(payload.MaxFileSize) != "" {
limits.MaxFileSizeBytes = parseInt64OrZero(payload.MaxFileSize)
}
if limits.MaxBoxSizeBytes == 0 && strings.TrimSpace(payload.MaxBoxSize) != "" {
limits.MaxBoxSizeBytes = parseInt64OrZero(payload.MaxBoxSize)
}
var (
user userstore.User
err error
)
if strings.TrimSpace(payload.ID) == "" {
user, err = app.userStore.Create(payload.Username, payload.Email, permissions, limits, payload.Status)
} else {
user, err = app.userStore.Update(payload.ID, payload.Username, payload.Email, permissions, limits, payload.Status)
}
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"ok": true, "user": toAdminUserView(user)})
}
func (app *App) handleAdminUsersDelete(ctx *gin.Context) {
if app.userStore == nil {
ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"})
return
}
var payload struct {
ID string `json:"id"`
}
if err := ctx.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.ID) == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "User id is required"})
return
}
if err := app.userStore.Delete(payload.ID); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"ok": true})
}
func (app *App) handleAdminUserAPIKeyCreate(ctx *gin.Context) {
if app.userStore == nil {
ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"})
return
}
var payload struct {
UserID string `json:"user_id"`
Name string `json:"name"`
}
if err := ctx.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.UserID) == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "User id is required"})
return
}
key, raw, err := app.userStore.CreateAPIKey(payload.UserID, 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)})
}
func (app *App) handleAdminUserAPIKeyRevoke(ctx *gin.Context) {
if app.userStore == nil {
ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"})
return
}
var payload struct {
UserID string `json:"user_id"`
KeyID string `json:"key_id"`
}
if err := ctx.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.UserID) == "" || strings.TrimSpace(payload.KeyID) == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "User id and key id are required"})
return
}
if err := app.userStore.RevokeAPIKey(payload.UserID, payload.KeyID); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

View File

@@ -17,6 +17,7 @@ import (
"warpbox/lib/config"
"warpbox/lib/routing"
"warpbox/lib/security"
"warpbox/lib/userstore"
)
type App struct {
@@ -26,6 +27,7 @@ type App struct {
alertStore *alerts.Store
securityGuard *security.Guard
appVersion string
userStore *userstore.Store
}
func Run(addr string) error {
@@ -61,6 +63,11 @@ func Run(addr string) error {
securityGuard: security.NewGuard(),
appVersion: currentAppVersion(),
}
userStore, err := userstore.NewStore(filepath.Join(cfg.DBDir, "users.json"))
if err != nil {
return err
}
app.userStore = userStore
if err := app.reloadSecurityConfig(); err != nil {
return err
}
@@ -99,6 +106,11 @@ func Run(addr string) error {
AdminBoxes: app.handleAdminBoxes,
AdminBoxesAction: app.handleAdminBoxesAction,
AdminUsers: app.handleAdminUsers,
AdminUsersList: app.handleAdminUsersList,
AdminUsersSave: app.handleAdminUsersSave,
AdminUsersDelete: app.handleAdminUsersDelete,
AdminUserKeyCreate: app.handleAdminUserAPIKeyCreate,
AdminUserKeyRevoke: app.handleAdminUserAPIKeyRevoke,
AdminActivity: app.handleAdminActivity,
AdminSecurity: app.handleAdminSecurity,
AdminAlertsAction: app.handleAdminAlertsAction,
@@ -109,6 +121,10 @@ func Run(addr string) error {
AdminSettingsImport: app.handleAdminSettingsImport,
AdminSettingsReset: app.handleAdminSettingsReset,
AdminAuth: app.adminAuthMiddleware,
UserLogin: app.handleUserLogin,
UserLogout: app.handleUserLogout,
UserMe: app.handleUserMe,
UserCreateAPIKey: app.handleSelfCreateAPIKey,
})
compressed := router.Group("/", gzip.Gzip(gzip.DefaultCompression))

View File

@@ -17,7 +17,11 @@ func (app *App) handleCreateBox(ctx *gin.Context) {
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
return
}
app.limitRequestBody(ctx)
actor, ok := app.authorizeUpload(ctx)
if !ok {
return
}
app.limitRequestBodyForActor(ctx, actor)
boxID, err := boxstore.NewBoxID()
if err != nil {
@@ -35,7 +39,7 @@ func (app *App) handleCreateBox(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box payload"})
return
}
if err := app.validateCreateBoxRequest(&request); err != nil {
if err := app.validateCreateBoxRequestForActor(&request, actor); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -60,7 +64,11 @@ func (app *App) handleManifestFileUpload(ctx *gin.Context) {
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
return
}
app.limitRequestBody(ctx)
actor, ok := app.authorizeUpload(ctx)
if !ok {
return
}
app.limitRequestBodyForActor(ctx, actor)
boxID := ctx.Param("id")
fileID := ctx.Param("file_id")
@@ -75,7 +83,7 @@ func (app *App) handleManifestFileUpload(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"})
return
}
if err := app.validateManifestFileUpload(boxID, fileID, file.Size); err != nil {
if err := app.validateManifestFileUploadForActor(boxID, fileID, file.Size, actor); err != nil {
boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -135,7 +143,11 @@ func (app *App) handleDirectBoxUpload(ctx *gin.Context) {
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
return
}
app.limitRequestBody(ctx)
actor, ok := app.authorizeUpload(ctx)
if !ok {
return
}
app.limitRequestBodyForActor(ctx, actor)
boxID := ctx.Param("id")
if !boxstore.ValidBoxID(boxID) {
@@ -148,7 +160,7 @@ func (app *App) handleDirectBoxUpload(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"})
return
}
if err := app.validateIncomingFile(boxID, file.Size); err != nil {
if err := app.validateIncomingFileForActor(boxID, file.Size, actor); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -169,7 +181,11 @@ func (app *App) handleLegacyUpload(ctx *gin.Context) {
if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
return
}
app.limitRequestBody(ctx)
actor, ok := app.authorizeUpload(ctx)
if !ok {
return
}
app.limitRequestBodyForActor(ctx, actor)
form, err := ctx.MultipartForm()
if err != nil {
@@ -184,13 +200,13 @@ func (app *App) handleLegacyUpload(ctx *gin.Context) {
}
totalSize := int64(0)
for _, file := range files {
if err := app.validateFileSize(file.Size); err != nil {
if err := app.validateFileSizeForActor(file.Size, actor); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
totalSize += file.Size
}
if err := app.validateBoxSize(totalSize); err != nil {
if err := app.validateBoxSizeForActor(totalSize, actor); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
@@ -226,7 +242,7 @@ func (app *App) handleLegacyUpload(ctx *gin.Context) {
for _, file := range files {
request.Files = append(request.Files, models.CreateBoxFileRequest{Name: file.Filename, Size: file.Size})
}
if err := app.validateCreateBoxRequest(&request); err != nil {
if err := app.validateCreateBoxRequestForActor(&request, actor); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

188
lib/server/user_auth.go Normal file
View File

@@ -0,0 +1,188 @@
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)})
}

View File

@@ -29,6 +29,10 @@ func (app *App) requireGuestUploads(ctx *gin.Context) bool {
}
func (app *App) validateCreateBoxRequest(request *models.CreateBoxRequest) error {
return app.validateCreateBoxRequestForActor(request, nil)
}
func (app *App) validateCreateBoxRequestForActor(request *models.CreateBoxRequest, actor *requestActor) error {
if request == nil {
return nil
}
@@ -45,19 +49,23 @@ func (app *App) validateCreateBoxRequest(request *models.CreateBoxRequest) error
totalSize := int64(0)
for _, file := range request.Files {
if err := app.validateFileSize(file.Size); err != nil {
if err := app.validateFileSizeForActor(file.Size, actor); err != nil {
return err
}
totalSize += file.Size
}
return app.validateBoxSize(totalSize)
return app.validateBoxSizeForActor(totalSize, actor)
}
func (app *App) validateIncomingFile(boxID string, size int64) error {
if err := app.validateFileSize(size); err != nil {
return app.validateIncomingFileForActor(boxID, size, nil)
}
func (app *App) validateIncomingFileForActor(boxID string, size int64, actor *requestActor) error {
if err := app.validateFileSizeForActor(size, actor); err != nil {
return err
}
if app.config.GlobalMaxBoxSizeBytes <= 0 {
if app.effectiveMaxBoxBytes(actor) <= 0 {
return nil
}
@@ -69,23 +77,27 @@ func (app *App) validateIncomingFile(boxID string, size int64) error {
for _, file := range files {
totalSize += file.Size
}
return app.validateBoxSize(totalSize)
return app.validateBoxSizeForActor(totalSize, actor)
}
func (app *App) validateManifestFileUpload(boxID string, fileID string, size int64) error {
if err := app.validateFileSize(size); err != nil {
return app.validateManifestFileUploadForActor(boxID, fileID, size, nil)
}
func (app *App) validateManifestFileUploadForActor(boxID string, fileID string, size int64, actor *requestActor) error {
if err := app.validateFileSizeForActor(size, actor); err != nil {
return err
}
manifest, err := boxstore.ReadManifest(boxID)
if err != nil {
return app.validateIncomingFile(boxID, size)
return app.validateIncomingFileForActor(boxID, size, actor)
}
if boxstore.IsExpired(manifest) {
_ = boxstore.DeleteBox(boxID)
return fmt.Errorf("Box expired")
}
if app.config.GlobalMaxBoxSizeBytes <= 0 {
if app.effectiveMaxBoxBytes(actor) <= 0 {
return nil
}
totalSize := int64(0)
@@ -101,24 +113,54 @@ func (app *App) validateManifestFileUpload(boxID string, fileID string, size int
if !found {
totalSize += size
}
return app.validateBoxSize(totalSize)
return app.validateBoxSizeForActor(totalSize, actor)
}
func (app *App) validateFileSize(size int64) error {
return app.validateFileSizeForActor(size, nil)
}
func (app *App) effectiveMaxFileBytes(actor *requestActor) int64 {
if actor == nil {
return app.config.GlobalMaxFileSizeBytes
}
return actor.User.Limits.MaxFileSizeBytes
}
func (app *App) effectiveMaxBoxBytes(actor *requestActor) int64 {
if actor == nil {
return app.config.GlobalMaxBoxSizeBytes
}
return actor.User.Limits.MaxBoxSizeBytes
}
func (app *App) validateFileSizeForActor(size int64, actor *requestActor) error {
if size < 0 {
return fmt.Errorf("File size cannot be negative")
}
if app.config.GlobalMaxFileSizeBytes > 0 && size > app.config.GlobalMaxFileSizeBytes {
limit := app.effectiveMaxFileBytes(actor)
if limit > 0 && size > limit {
if actor != nil {
return fmt.Errorf("File exceeds this account's max file size")
}
return fmt.Errorf("File exceeds the global max file size")
}
return nil
}
func (app *App) validateBoxSize(size int64) error {
return app.validateBoxSizeForActor(size, nil)
}
func (app *App) validateBoxSizeForActor(size int64, actor *requestActor) error {
if size < 0 {
return fmt.Errorf("Box size cannot be negative")
}
if app.config.GlobalMaxBoxSizeBytes > 0 && size > app.config.GlobalMaxBoxSizeBytes {
limit := app.effectiveMaxBoxBytes(actor)
if limit > 0 && size > limit {
if actor != nil {
return fmt.Errorf("Box exceeds this account's max box size")
}
return fmt.Errorf("Box exceeds the global max box size")
}
return nil
@@ -137,7 +179,11 @@ func (app *App) rejectExpiredManifestBox(boxID string) error {
}
func (app *App) limitRequestBody(ctx *gin.Context) {
limit := app.maxRequestBodyBytes()
app.limitRequestBodyForActor(ctx, nil)
}
func (app *App) limitRequestBodyForActor(ctx *gin.Context, actor *requestActor) {
limit := app.maxRequestBodyBytesForActor(actor)
if limit <= 0 {
return
}
@@ -145,9 +191,14 @@ func (app *App) limitRequestBody(ctx *gin.Context) {
}
func (app *App) maxRequestBodyBytes() int64 {
limit := app.config.GlobalMaxBoxSizeBytes
if limit <= 0 || app.config.GlobalMaxFileSizeBytes > limit {
limit = app.config.GlobalMaxFileSizeBytes
return app.maxRequestBodyBytesForActor(nil)
}
func (app *App) maxRequestBodyBytesForActor(actor *requestActor) int64 {
limit := app.effectiveMaxBoxBytes(actor)
fileLimit := app.effectiveMaxFileBytes(actor)
if limit <= 0 || fileLimit > limit {
limit = fileLimit
}
if limit <= 0 {
return 0