forgejo/tests/integration/api_quota_management_test.go
Gergely Nagy e1fe3bbdc0
feat(quota): Humble beginnings of a quota engine
This is an implementation of a quota engine, and the API routes to
manage its settings. This does *not* contain any enforcement code: this
is just the bedrock, the engine itself.

The goal of the engine is to be flexible and future proof: to be nimble
enough to build on it further, without having to rewrite large parts of
it.

It might feel a little more complicated than necessary, because the goal
was to be able to support scenarios only very few Forgejo instances
need, scenarios the vast majority of mostly smaller instances simply do
not care about. The goal is to support both big and small, and for that,
we need a solid, flexible foundation.

There are thee big parts to the engine: counting quota use, setting
limits, and evaluating whether the usage is within the limits. Sounds
simple on paper, less so in practice!

Quota counting
==============

Quota is counted based on repo ownership, whenever possible, because
repo owners are in ultimate control over the resources they use: they
can delete repos, attachments, everything, even if they don't *own*
those themselves. They can clean up, and will always have the permission
and access required to do so. Would we count quota based on the owning
user, that could lead to situations where a user is unable to free up
space, because they uploaded a big attachment to a repo that has been
taken private since. It's both more fair, and much safer to count quota
against repo owners.

This means that if user A uploads an attachment to an issue opened
against organization O, that will count towards the quota of
organization O, rather than user A.

One's quota usage stats can be queried using the `/user/quota` API
endpoint. To figure out what's eating into it, the
`/user/repos?order_by=size`, `/user/quota/attachments`,
`/user/quota/artifacts`, and `/user/quota/packages` endpoints should be
consulted. There's also `/user/quota/check?subject=<...>` to check
whether the signed-in user is within a particular quota limit.

Quotas are counted based on sizes stored in the database.

Setting quota limits
====================

There are different "subjects" one can limit usage for. At this time,
only size-based limits are implemented, which are:

- `size:all`: As the name would imply, the total size of everything
  Forgejo tracks.
- `size:repos:all`: The total size of all repositories (not including
  LFS).
- `size:repos:public`: The total size of all public repositories (not
  including LFS).
- `size:repos:private`: The total size of all private repositories (not
  including LFS).
- `size:git:all`: The total size of all git data (including all
  repositories, and LFS).
- `size:git:lfs`: The size of all git LFS data (either in private or
  public repos).
- `size:assets:all`: The size of all assets tracked by Forgejo.
- `size:assets:attachments:all`: The size of all kinds of attachments
  tracked by Forgejo.
- `size:assets:attachments:issues`: Size of all attachments attached to
  issues, including issue comments.
- `size:assets:attachments:releases`: Size of all attachments attached
  to releases. This does *not* include automatically generated archives.
- `size:assets:artifacts`: Size of all Action artifacts.
- `size:assets:packages:all`: Size of all Packages.
- `size:wiki`: Wiki size

Wiki size is currently not tracked, and the engine will always deem it
within quota.

These subjects are built into Rules, which set a limit on *all* subjects
within a rule. Thus, we can create a rule that says: "1Gb limit on all
release assets, all packages, and git LFS, combined". For a rule to
stand, the total sum of all subjects must be below the rule's limit.

Rules are in turn collected into groups. A group is just a name, and a
list of rules. For a group to stand, all of its rules must stand. Thus,
if we have a group with two rules, one that sets a combined 1Gb limit on
release assets, all packages, and git LFS, and another rule that sets a
256Mb limit on packages, if the user has 512Mb of packages, the group
will not stand, because the second rule deems it over quota. Similarly,
if the user has only 128Mb of packages, but 900Mb of release assets, the
group will not stand, because the combined size of packages and release
assets is over the 1Gb limit of the first rule.

Groups themselves are collected into Group Lists. A group list stands
when *any* of the groups within stand. This allows an administrator to
set conservative defaults, but then place select users into additional
groups that increase some aspect of their limits.

To top it off, it is possible to set the default quota groups a user
belongs to in `app.ini`. If there's no explicit assignment, the engine
will use the default groups. This makes it possible to avoid having to
assign each and every user a list of quota groups, and only those need
to be explicitly assigned who need a different set of groups than the
defaults.

If a user has any quota groups assigned to them, the default list will
not be considered for them.

The management APIs
===================

This commit contains the engine itself, its unit tests, and the quota
management APIs. It does not contain any enforcement.

The APIs are documented in-code, and in the swagger docs, and the
integration tests can serve as an example on how to use them.

Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
2024-08-02 11:10:34 +02:00

846 lines
29 KiB
Go

// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
quota_model "code.gitea.io/gitea/models/quota"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIQuotaDisabled(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Quota.Enabled, false)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
session := loginUser(t, user.Name)
req := NewRequest(t, "GET", "/api/v1/user/quota")
session.MakeRequest(t, req, http.StatusNotFound)
}
func apiCreateUser(t *testing.T, username string) func() {
t.Helper()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
session := loginUser(t, admin.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
mustChangePassword := false
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users", api.CreateUserOption{
Email: "api+" + username + "@example.com",
Username: username,
Password: "password",
MustChangePassword: &mustChangePassword,
}).AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusCreated)
return func() {
req := NewRequest(t, "DELETE", "/api/v1/admin/users/"+username+"?purge=true").AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusNoContent)
}
}
func TestAPIQuotaCreateGroupWithRules(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
// Create two rules in advance
unlimited := int64(-1)
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
Name: "unlimited",
Limit: &unlimited,
Subjects: []string{"size:all"},
})()
zero := int64(0)
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
Name: "deny-git-lfs",
Limit: &zero,
Subjects: []string{"size:git:lfs"},
})()
// Log in as admin
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
// Create a new group, with rules specified
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
Name: "group-with-rules",
Rules: []api.CreateQuotaRuleOptions{
// First: an existing group, unlimited, name only
{
Name: "unlimited",
},
// Second: an existing group, deny-git-lfs, with different params
{
Name: "deny-git-lfs",
Limit: &unlimited,
},
// Third: an entirely new group
{
Name: "new-rule",
Subjects: []string{"size:assets:all"},
},
},
}).AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
defer func() {
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/group-with-rules").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/new-rule").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
}()
// Verify that we created a group with rules included
var q api.QuotaGroup
DecodeJSON(t, resp, &q)
assert.Equal(t, "group-with-rules", q.Name)
assert.Len(t, q.Rules, 3)
// Verify that the previously existing rules are unchanged
rule, err := quota_model.GetRuleByName(db.DefaultContext, "unlimited")
require.NoError(t, err)
assert.NotNil(t, rule)
assert.EqualValues(t, -1, rule.Limit)
assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}, rule.Subjects)
rule, err = quota_model.GetRuleByName(db.DefaultContext, "deny-git-lfs")
require.NoError(t, err)
assert.NotNil(t, rule)
assert.EqualValues(t, 0, rule.Limit)
assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeGitLFS}, rule.Subjects)
// Verify that the new rule was also created
rule, err = quota_model.GetRuleByName(db.DefaultContext, "new-rule")
require.NoError(t, err)
assert.NotNil(t, rule)
assert.EqualValues(t, 0, rule.Limit)
assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAssetsAll}, rule.Subjects)
t.Run("invalid rule spec", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
Name: "group-with-invalid-rule-spec",
Rules: []api.CreateQuotaRuleOptions{
{
Name: "rule-with-wrong-spec",
Subjects: []string{"valid:false"},
},
},
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
}
func TestAPIQuotaEmptyState(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
username := "quota-empty-user"
defer apiCreateUser(t, username)()
session := loginUser(t, username)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
t.Run("#/admin/users/quota-empty-user/quota", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
req := NewRequest(t, "GET", "/api/v1/admin/users/quota-empty-user/quota").AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusOK)
var q api.QuotaInfo
DecodeJSON(t, resp, &q)
assert.EqualValues(t, api.QuotaUsed{}, q.Used)
assert.Empty(t, q.Groups)
})
t.Run("#/user/quota", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/user/quota").AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusOK)
var q api.QuotaInfo
DecodeJSON(t, resp, &q)
assert.EqualValues(t, api.QuotaUsed{}, q.Used)
assert.Empty(t, q.Groups)
t.Run("#/user/quota/artifacts", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/user/quota/artifacts").AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusOK)
var q api.QuotaUsedArtifactList
DecodeJSON(t, resp, &q)
assert.Empty(t, q)
})
t.Run("#/user/quota/attachments", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/user/quota/attachments").AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusOK)
var q api.QuotaUsedAttachmentList
DecodeJSON(t, resp, &q)
assert.Empty(t, q)
})
t.Run("#/user/quota/packages", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/user/quota/packages").AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusOK)
var q api.QuotaUsedPackageList
DecodeJSON(t, resp, &q)
assert.Empty(t, q)
})
})
}
func createQuotaRule(t *testing.T, opts api.CreateQuotaRuleOptions) func() {
t.Helper()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", opts).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusCreated)
return func() {
req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/rules/%s", opts.Name).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
}
}
func createQuotaGroup(t *testing.T, name string) func() {
t.Helper()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
Name: name,
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusCreated)
return func() {
req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/%s", name).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
}
}
func TestAPIQuotaAdminRoutesRules(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
zero := int64(0)
oneKb := int64(1024)
t.Run("adminCreateQuotaRule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", api.CreateQuotaRuleOptions{
Name: "deny-all",
Limit: &zero,
Subjects: []string{"size:all"},
}).AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
defer func() {
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
}()
var q api.QuotaRuleInfo
DecodeJSON(t, resp, &q)
assert.Equal(t, "deny-all", q.Name)
assert.EqualValues(t, 0, q.Limit)
assert.EqualValues(t, []string{"size:all"}, q.Subjects)
rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all")
require.NoError(t, err)
assert.EqualValues(t, 0, rule.Limit)
t.Run("unhappy path", func(t *testing.T) {
t.Run("missing options", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", nil).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
t.Run("invalid subjects", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", api.CreateQuotaRuleOptions{
Name: "invalid-subjects",
Limit: &zero,
Subjects: []string{"valid:false"},
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
t.Run("trying to add an existing rule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
rule := api.CreateQuotaRuleOptions{
Name: "double-rule",
Limit: &zero,
}
defer createQuotaRule(t, rule)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", rule).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusConflict)
})
})
})
t.Run("adminDeleteQuotaRule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
createQuotaRule(t, api.CreateQuotaRuleOptions{
Name: "deny-all",
Limit: &zero,
Subjects: []string{"size:all"},
})
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all")
require.NoError(t, err)
assert.Nil(t, rule)
t.Run("unhappy path", func(t *testing.T) {
t.Run("nonexistent rule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminEditQuotaRule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
Name: "deny-all",
Limit: &zero,
Subjects: []string{"size:all"},
})()
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", api.EditQuotaRuleOptions{
Limit: &oneKb,
}).AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusOK)
var q api.QuotaRuleInfo
DecodeJSON(t, resp, &q)
assert.EqualValues(t, 1024, q.Limit)
rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all")
require.NoError(t, err)
assert.EqualValues(t, 1024, rule.Limit)
t.Run("no options", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", nil).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusOK)
})
t.Run("unhappy path", func(t *testing.T) {
t.Run("nonexistent rule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/does-not-exist", api.EditQuotaRuleOptions{
Limit: &oneKb,
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("invalid subjects", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", api.EditQuotaRuleOptions{
Subjects: &[]string{"valid:false"},
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
})
})
t.Run("adminListQuotaRules", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
Name: "deny-all",
Limit: &zero,
Subjects: []string{"size:all"},
})()
req := NewRequest(t, "GET", "/api/v1/admin/quota/rules").AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusOK)
var rules []api.QuotaRuleInfo
DecodeJSON(t, resp, &rules)
assert.Len(t, rules, 1)
assert.Equal(t, "deny-all", rules[0].Name)
assert.EqualValues(t, 0, rules[0].Limit)
})
}
func TestAPIQuotaAdminRoutesGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
zero := int64(0)
ruleDenyAll := api.CreateQuotaRuleOptions{
Name: "deny-all",
Limit: &zero,
Subjects: []string{"size:all"},
}
username := "quota-test-user"
defer apiCreateUser(t, username)()
t.Run("adminCreateQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
Name: "default",
}).AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
defer func() {
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
}()
var q api.QuotaGroup
DecodeJSON(t, resp, &q)
assert.Equal(t, "default", q.Name)
assert.Empty(t, q.Rules)
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
require.NoError(t, err)
assert.Equal(t, "default", group.Name)
assert.Empty(t, group.Rules)
t.Run("unhappy path", func(t *testing.T) {
t.Run("missing options", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", nil).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
t.Run("trying to add an existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "duplicate")()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
Name: "duplicate",
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusConflict)
})
})
})
t.Run("adminDeleteQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
createQuotaGroup(t, "default")
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
require.NoError(t, err)
assert.Nil(t, group)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminAddRuleToQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
defer createQuotaRule(t, ruleDenyAll)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
require.NoError(t, err)
assert.Len(t, group.Rules, 1)
assert.Equal(t, "deny-all", group.Rules[0].Name)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/does-not-exist/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("non-existing rule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminRemoveRuleFromQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
defer createQuotaRule(t, ruleDenyAll)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
require.NoError(t, err)
assert.Equal(t, "default", group.Name)
assert.Empty(t, group.Rules)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("non-existing rule", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("rule not in group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
Name: "rule-not-in-group",
Limit: &zero,
})()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/rule-not-in-group").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminGetQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
defer createQuotaRule(t, ruleDenyAll)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusOK)
var q api.QuotaGroup
DecodeJSON(t, resp, &q)
assert.Equal(t, "default", q.Name)
assert.Len(t, q.Rules, 1)
assert.Equal(t, "deny-all", q.Rules[0].Name)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/admin/quota/groups/does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminListQuotaGroups", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
defer createQuotaRule(t, ruleDenyAll)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", "/api/v1/admin/quota/groups").AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusOK)
var q api.QuotaGroupList
DecodeJSON(t, resp, &q)
assert.Len(t, q, 1)
assert.Equal(t, "default", q[0].Name)
assert.Len(t, q[0].Rules, 1)
assert.Equal(t, "deny-all", q[0].Rules[0].Name)
})
t.Run("adminAddUserToQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID)
require.NoError(t, err)
assert.Len(t, groups, 1)
assert.Equal(t, "default", groups[0].Name)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/does-not-exist/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("non-existing user", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/this-user-does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("user already added", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusConflict)
})
})
})
t.Run("adminRemoveUserFromQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID)
require.NoError(t, err)
assert.Empty(t, groups)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("non-existing user", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/users/does-not-exist").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("user not in group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminListUsersInQuotaGroup", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", "/api/v1/admin/quota/groups/default/users").AddTokenAuth(adminToken)
resp := adminSession.MakeRequest(t, req, http.StatusOK)
var q []api.User
DecodeJSON(t, resp, &q)
assert.Len(t, q, 1)
assert.Equal(t, username, q[0].UserName)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/admin/quota/groups/does-not-exist/users").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
})
})
t.Run("adminSetUserQuotaGroups", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer createQuotaGroup(t, "default")()
defer createQuotaGroup(t, "test-1")()
defer createQuotaGroup(t, "test-2")()
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/admin/users/%s/quota/groups", username), api.SetUserQuotaGroupsOptions{
Groups: &[]string{"default", "test-1", "test-2"},
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID)
require.NoError(t, err)
assert.Len(t, groups, 3)
t.Run("unhappy path", func(t *testing.T) {
t.Run("non-existing user", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/does-not-exist/quota/groups", api.SetUserQuotaGroupsOptions{
Groups: &[]string{"default", "test-1", "test-2"},
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("non-existing group", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/admin/users/%s/quota/groups", username), api.SetUserQuotaGroupsOptions{
Groups: &[]string{"default", "test-1", "test-2", "this-group-does-not-exist"},
}).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
})
})
})
}
func TestAPIQuotaUserRoutes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
adminSession := loginUser(t, admin.Name)
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
// Create a test user
username := "quota-test-user-routes"
defer apiCreateUser(t, username)()
session := loginUser(t, username)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
// Set up rules & groups for the user
defer createQuotaGroup(t, "user-routes-deny")()
defer createQuotaGroup(t, "user-routes-1kb")()
zero := int64(0)
ruleDenyAll := api.CreateQuotaRuleOptions{
Name: "user-routes-deny-all",
Limit: &zero,
Subjects: []string{"size:all"},
}
defer createQuotaRule(t, ruleDenyAll)()
oneKb := int64(1024)
rule1KbStuff := api.CreateQuotaRuleOptions{
Name: "user-routes-1kb",
Limit: &oneKb,
Subjects: []string{"size:assets:attachments:releases", "size:assets:packages:all", "size:git:lfs"},
}
defer createQuotaRule(t, rule1KbStuff)()
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/user-routes-deny/rules/user-routes-deny-all").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "PUT", "/api/v1/admin/quota/groups/user-routes-1kb/rules/user-routes-1kb").AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/user-routes-deny/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
req = NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/user-routes-1kb/users/%s", username).AddTokenAuth(adminToken)
adminSession.MakeRequest(t, req, http.StatusNoContent)
t.Run("userGetQuota", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/user/quota").AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusOK)
var q api.QuotaInfo
DecodeJSON(t, resp, &q)
assert.Len(t, q.Groups, 2)
assert.Len(t, q.Groups[0].Rules, 1)
assert.Len(t, q.Groups[1].Rules, 1)
})
}