feat: add upload policies, daily limits, and storage quotas
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
- Add environment variables to configure anonymous uploads, daily upload caps, and default user storage limits. - Update config loader to parse and validate the new settings. - Implement backend logic to track daily usage and active storage per user. - Update README and `.env.example` to document the new settings and admin panels.
This commit is contained in:
@@ -139,6 +139,276 @@ func TestAdminUploadBypassesMaxUploadSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymousUploadDisabled(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
policy := testPolicy(t, app)
|
||||
policy.AnonymousUploadsEnabled = false
|
||||
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
|
||||
request := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
|
||||
request.Header.Set("Accept", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
app.Upload(response, request)
|
||||
if response.Code != http.StatusForbidden {
|
||||
t.Fatalf("status = %d, want 403, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymousUploadLimits(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
policy := testPolicy(t, app)
|
||||
policy.AnonymousMaxUploadMB = 1
|
||||
policy.AnonymousDailyUploadMB = 0.001
|
||||
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
|
||||
large := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", 2*1024*1024))
|
||||
large.Header.Set("Accept", "application/json")
|
||||
large.RemoteAddr = "192.0.2.10:1234"
|
||||
largeResponse := httptest.NewRecorder()
|
||||
app.Upload(largeResponse, large)
|
||||
if largeResponse.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("large status = %d, body = %s", largeResponse.Code, largeResponse.Body.String())
|
||||
}
|
||||
|
||||
daily := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", strings.Repeat("x", 2048))
|
||||
daily.Header.Set("Accept", "application/json")
|
||||
daily.RemoteAddr = "192.0.2.10:1234"
|
||||
dailyResponse := httptest.NewRecorder()
|
||||
app.Upload(dailyResponse, daily)
|
||||
if dailyResponse.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("daily status = %d, body = %s", dailyResponse.Code, dailyResponse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedInUploadQuotaAndOverride(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
user, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
||||
}
|
||||
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, user.ID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateInvite returned error: %v", err)
|
||||
}
|
||||
normal, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("AcceptInvite returned error: %v", err)
|
||||
}
|
||||
_, token, err := app.authService.Login(normal.Email, "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Login returned error: %v", err)
|
||||
}
|
||||
policy := testPolicy(t, app)
|
||||
policy.DefaultUserStorageMB = 0.001
|
||||
policy.UserDailyUploadMB = 8
|
||||
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
|
||||
request := multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
||||
response := httptest.NewRecorder()
|
||||
app.Upload(response, request)
|
||||
if response.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("quota status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
|
||||
override := 10.0
|
||||
if err := app.authService.SetUserStorageQuota(normal.ID, &override); err != nil {
|
||||
t.Fatalf("SetUserStorageQuota returned error: %v", err)
|
||||
}
|
||||
request = multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
||||
response = httptest.NewRecorder()
|
||||
app.Upload(response, request)
|
||||
if response.Code != http.StatusCreated {
|
||||
t.Fatalf("override status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedInDailyCap(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
||||
}
|
||||
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateInvite returned error: %v", err)
|
||||
}
|
||||
user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("AcceptInvite returned error: %v", err)
|
||||
}
|
||||
_, token, err := app.authService.Login(user.Email, "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Login returned error: %v", err)
|
||||
}
|
||||
policy := testPolicy(t, app)
|
||||
policy.UserDailyUploadMB = 0.001
|
||||
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
request := multipartUploadRequest(t, "/api/v1/upload", "file", "daily.txt", strings.Repeat("x", 2048))
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
||||
response := httptest.NewRecorder()
|
||||
app.Upload(response, request)
|
||||
if response.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("daily status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminSettingsPostChangesUploadEnforcement(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
||||
}
|
||||
_, token, err := app.authService.Login("admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Login returned error: %v", err)
|
||||
}
|
||||
|
||||
settingsForm := strings.NewReader("anonymous_max_upload_mb=512&anonymous_daily_upload_mb=2048&user_daily_upload_mb=8192&default_user_storage_mb=51200&usage_retention_days=30")
|
||||
settingsRequest := httptest.NewRequest(http.MethodPost, "/admin/settings", settingsForm)
|
||||
settingsRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
settingsRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
||||
settingsResponse := httptest.NewRecorder()
|
||||
app.AdminSettingsPost(settingsResponse, settingsRequest)
|
||||
if settingsResponse.Code != http.StatusSeeOther {
|
||||
t.Fatalf("AdminSettingsPost status = %d, body = %s", settingsResponse.Code, settingsResponse.Body.String())
|
||||
}
|
||||
|
||||
uploadRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
|
||||
uploadRequest.Header.Set("Accept", "application/json")
|
||||
uploadResponse := httptest.NewRecorder()
|
||||
app.Upload(uploadResponse, uploadRequest)
|
||||
if uploadResponse.Code != http.StatusForbidden {
|
||||
t.Fatalf("upload status = %d, want 403, body = %s", uploadResponse.Code, uploadResponse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminUserQuotaPostChangesEnforcement(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
||||
}
|
||||
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateInvite returned error: %v", err)
|
||||
}
|
||||
user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("AcceptInvite returned error: %v", err)
|
||||
}
|
||||
_, adminToken, err := app.authService.Login(admin.Email, "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("admin Login returned error: %v", err)
|
||||
}
|
||||
|
||||
quotaRequest := httptest.NewRequest(http.MethodPost, "/admin/users/"+user.ID+"/quota", strings.NewReader("storage_quota_mb=0.001"))
|
||||
quotaRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
quotaRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
||||
quotaRequest.SetPathValue("userID", user.ID)
|
||||
quotaResponse := httptest.NewRecorder()
|
||||
app.AdminUpdateUserQuota(quotaResponse, quotaRequest)
|
||||
if quotaResponse.Code != http.StatusSeeOther {
|
||||
t.Fatalf("AdminUpdateUserQuota status = %d, body = %s", quotaResponse.Code, quotaResponse.Body.String())
|
||||
}
|
||||
|
||||
_, userToken, err := app.authService.Login(user.Email, "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("user Login returned error: %v", err)
|
||||
}
|
||||
uploadRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
|
||||
uploadRequest.Header.Set("Accept", "application/json")
|
||||
uploadRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: userToken})
|
||||
uploadResponse := httptest.NewRecorder()
|
||||
app.Upload(uploadResponse, uploadRequest)
|
||||
if uploadResponse.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("upload status = %d, want 413, body = %s", uploadResponse.Code, uploadResponse.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeReflectsUploadPolicySettings(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
policy := testPolicy(t, app)
|
||||
policy.AnonymousMaxUploadMB = 123
|
||||
policy.AnonymousDailyUploadMB = 456
|
||||
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
response := httptest.NewRecorder()
|
||||
app.Home(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("Home status = %d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
if !strings.Contains(body, "Max file size: 123 MB") || !strings.Contains(body, "456 MB") {
|
||||
t.Fatalf("home did not reflect policy settings: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIDocsHeaderReflectsLoggedInUser(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
||||
}
|
||||
_, token, err := app.authService.Login("admin@example.test", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Login returned error: %v", err)
|
||||
}
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api", nil)
|
||||
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
||||
response := httptest.NewRecorder()
|
||||
app.APIDocs(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("APIDocs status = %d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
header := body[:strings.Index(body, "<main")]
|
||||
if !strings.Contains(header, "My Account") || strings.Contains(header, ">Login<") || strings.Contains(header, "Health") {
|
||||
t.Fatalf("api header did not reflect logged-in state: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIDocsHeaderReflectsLoggedOutUser(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api", nil)
|
||||
response := httptest.NewRecorder()
|
||||
app.APIDocs(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("APIDocs status = %d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
header := body[:strings.Index(body, "<main")]
|
||||
if !strings.Contains(header, ">Login<") || !strings.Contains(header, ">API<") || strings.Contains(header, "Health") || strings.Contains(header, "My Account") {
|
||||
t.Fatalf("api header did not reflect logged-out state: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func createOwnedBoxThroughApp(t *testing.T, app *App, userID string) services.UploadResult {
|
||||
t.Helper()
|
||||
user, err := app.authService.UserByID(userID)
|
||||
@@ -163,3 +433,12 @@ func createOwnedBoxThroughApp(t *testing.T, app *App, userID string) services.Up
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func testPolicy(t *testing.T, app *App) services.UploadPolicySettings {
|
||||
t.Helper()
|
||||
policy, err := app.settingsService.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy returned error: %v", err)
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user