internal/control/quota.go
299 lines · 10412 bytes
1package control
2
3import (
4 "fmt"
5 "io"
6 "strconv"
7 "sync"
8
9 "gitbay.org/gitbay/internal/config"
10 "gitbay.org/gitbay/internal/gitutil"
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15// Quotas cap what a user or an org owns directly, and how many orgs an
16// account creates. The limit is the owner's override when set, else the
17// configured default; 0 is unlimited.
18
19// RepoLimit is the owner's repository cap, 0 for none.
20func RepoLimit(st *store.Store, cfg configLimits, kind string, id int64) int64 {
21 if l, err := st.OwnerLimits(kind, id); err == nil && l.Repos != nil {
22 return *l.Repos
23 }
24 if kind == "org" {
25 return int64(cfg.MaxReposPerOrg)
26 }
27 return int64(cfg.MaxReposPerUser)
28}
29
30// ByteLimit is the owner's storage cap in bytes, 0 for none.
31func ByteLimit(st *store.Store, cfg configLimits, kind string, id int64) int64 {
32 if l, err := st.OwnerLimits(kind, id); err == nil && l.Bytes != nil {
33 return *l.Bytes
34 }
35 if kind == "org" {
36 return cfg.MaxBytesPerOrg
37 }
38 return cfg.MaxBytesPerUser
39}
40
41// OrgLimit is the account's cap on organizations it creates, 0 for none.
42func OrgLimit(st *store.Store, cfg configLimits, userID int64) int64 {
43 if l, err := st.OwnerLimits("user", userID); err == nil && l.Orgs != nil {
44 return *l.Orgs
45 }
46 return int64(cfg.MaxOrgsPerUser)
47}
48
49// OwnedBytes is the disk taken by the repositories an owner holds directly.
50func OwnedBytes(st *store.Store, root, kind string, id int64) int64 {
51 repos, err := st.ListReposForOwner(kind, id)
52 if err != nil {
53 return 0
54 }
55 var total int64
56 for _, r := range repos {
57 total += gitutil.DirSize(RepoDir(root, r.OwnerName, r.Name))
58 }
59 return total
60}
61
62// configLimits is the slice of config the quota functions read, so the
63// sshd package can pass its Limits without importing control's Ctx.
64type configLimits struct {
65 MaxReposPerUser int
66 MaxBytesPerUser int64
67 MaxOrgsPerUser int
68 MaxReposPerOrg int
69 MaxBytesPerOrg int64
70}
71
72// QuotaConfig is what sshd passes: the limits section of the config.
73func QuotaConfig(cfg config.Config) configLimits {
74 l := cfg.Limits
75 return configLimits{l.MaxReposPerUser, l.MaxBytesPerUser, l.MaxOrgsPerUser, l.MaxReposPerOrg, l.MaxBytesPerOrg}
76}
77
78func limitsOf(c *Ctx) configLimits { return QuotaConfig(c.Cfg) }
79
80// checkRepoQuota refuses one more repository for the owner past its cap.
81// repoCreateMu serialises the quota check with the insert that follows
82// it, so two concurrent creates cannot both pass the count (#108). One
83// process serves the instance, so a process-wide lock is the whole story.
84var repoCreateMu sync.Mutex
85
86func checkRepoQuota(c *Ctx, kind string, id int64) int {
87 limit := RepoLimit(c.Store, limitsOf(c), kind, id)
88 if limit == 0 {
89 return -1
90 }
91 n, err := c.Store.OwnedRepoCount(kind, id)
92 if err != nil {
93 return c.fail(protocol.ExitFailure, "%v", err)
94 }
95 if n >= limit {
96 if kind == "org" {
97 return c.fail(protocol.ExitDenied, "the organization owns %d of the %d repositories it may hold; delete or transfer one, or ask an admin to raise the limit", n, limit)
98 }
99 return c.fail(protocol.ExitDenied, "you own %d of the %d repositories your account may hold; delete or transfer one, or ask an admin to raise the limit", n, limit)
100 }
101 return -1
102}
103
104// checkStorageQuota refuses a server-side write into repo once its
105// owner's storage quota is used up, the check sshd makes before a push.
106func checkStorageQuota(c *Ctx, repo store.Repo) int {
107 return checkBytesLeft(c, repo.OwnerKind, repo.OwnerID, repo.OwnerName, 0)
108}
109
110// checkBytesLeft refuses when the owner's storage plus adding exceeds
111// its cap (with adding 0, once the cap is used up).
112func checkBytesLeft(c *Ctx, kind string, id int64, name string, adding int64) int {
113 limit := ByteLimit(c.Store, limitsOf(c), kind, id)
114 if limit <= 0 {
115 return -1
116 }
117 used := OwnedBytes(c.Store, c.Cfg.Server.Root, kind, id)
118 if adding == 0 && used >= limit {
119 return c.fail(protocol.ExitDenied,
120 "%s's storage quota is used up (%d of %d bytes); delete something, or ask an admin to raise the limit",
121 name, used, limit)
122 }
123 if adding > 0 && used+adding > limit {
124 return c.fail(protocol.ExitDenied,
125 "%s's storage quota cannot take %d more bytes (%d of %d used); delete something, or ask an admin to raise the limit",
126 name, adding, used, limit)
127 }
128 return -1
129}
130
131// orgCreateMu serialises the org cap check with the insert, as
132// repoCreateMu does for repositories.
133var orgCreateMu sync.Mutex
134
135func checkOrgQuota(c *Ctx) int {
136 limit := OrgLimit(c.Store, limitsOf(c), c.User.ID)
137 if limit == 0 {
138 return -1
139 }
140 n, err := c.Store.CreatedOrgCount(c.User.ID)
141 if err != nil {
142 return c.fail(protocol.ExitFailure, "%v", err)
143 }
144 if n >= limit {
145 return c.fail(protocol.ExitDenied, "you have created %d of the %d organizations your account may create; delete one, or ask an admin to raise the limit", n, limit)
146 }
147 return -1
148}
149
150func init() {
151 register(Command{Path: []string{"admin", "user", "limits"},
152 Summary: "show or set an account's repository, storage and organization caps (instance admins)",
153 Usage: "admin user limits <username> [--repos <n>|default] [--bytes <n>|default] [--orgs <n>|default]",
154 Flags: []Flag{
155 {"--repos", "<n>|default", "the account's repository cap", ""},
156 {"--bytes", "<n>|default", "the account's storage cap", ""},
157 {"--orgs", "<n>|default", "the account's cap on organizations it creates", ""},
158 },
159 Examples: []string{"admin user limits alice", "admin user limits alice --repos 50"},
160 Run: runAdminUserLimits})
161 register(Command{Path: []string{"admin", "org", "limits"},
162 Summary: "show or set an organization's repository and storage caps (instance admins)",
163 Usage: "admin org limits <org> [--repos <n>|default] [--bytes <n>|default]",
164 Flags: []Flag{
165 {"--repos", "<n>|default", "the organization's repository cap", ""},
166 {"--bytes", "<n>|default", "the organization's storage cap", ""},
167 },
168 Examples: []string{"admin org limits krz", "admin org limits krz --bytes 0"},
169 Run: runAdminOrgLimits})
170}
171
172// applyLimitFlags reads --repos/--bytes (and --orgs when orgs is true)
173// into l. set reports whether any flag was given.
174func applyLimitFlags(c *Ctx, args []string, l *store.Limits, orgs bool) (set bool, code int) {
175 for i := 0; i < len(args); i++ {
176 if i+1 >= len(args) {
177 return false, c.fail(protocol.ExitUsage, "%s requires a value", args[i])
178 }
179 v := args[i+1]
180 var target **int64
181 switch {
182 case args[i] == "--repos":
183 target = &l.Repos
184 case args[i] == "--bytes":
185 target = &l.Bytes
186 case args[i] == "--orgs" && orgs:
187 target = &l.Orgs
188 default:
189 return false, c.usage()
190 }
191 if v == "default" {
192 *target = nil
193 } else {
194 n, err := strconv.ParseInt(v, 10, 64)
195 if err != nil || n < 0 {
196 return false, c.fail(protocol.ExitUsage, "%s takes a non-negative number or default", args[i])
197 }
198 *target = &n
199 }
200 set = true
201 i++
202 }
203 return set, -1
204}
205
206func capText(n int64) string {
207 if n == 0 {
208 return "unlimited"
209 }
210 return strconv.FormatInt(n, 10)
211}
212
213func runAdminUserLimits(c *Ctx, args []string) int {
214 if code := requireInstanceAdmin(c); code >= 0 {
215 return code
216 }
217 if len(args) < 1 {
218 return c.usage()
219 }
220 u, err := c.Store.UserByUsername(args[0])
221 if err != nil {
222 return c.fail(protocol.ExitNotFound, "no user %q", args[0])
223 }
224 l, err := c.Store.OwnerLimits("user", u.ID)
225 if err != nil {
226 return c.fail(protocol.ExitFailure, "%v", err)
227 }
228 set, code := applyLimitFlags(c, args[1:], &l, true)
229 if code >= 0 {
230 return code
231 }
232 if set {
233 if err := c.Store.SetOwnerLimits("user", u.ID, l); err != nil {
234 return c.fail(protocol.ExitFailure, "%v", err)
235 }
236 c.Store.Audit(c.User.ID, "admin user.limits", map[string]any{"user": u.Username, "repos": l.Repos, "bytes": l.Bytes, "orgs": l.Orgs})
237 }
238 type out struct {
239 User string `json:"user"`
240 Repos int64 `json:"repos"` // effective cap, 0 unlimited
241 Bytes int64 `json:"bytes"` // effective cap, 0 unlimited
242 Orgs int64 `json:"orgs"` // effective cap, 0 unlimited
243 ReposOwned int64 `json:"repos_owned"`
244 BytesOwned int64 `json:"bytes_owned"`
245 OrgsCreated int64 `json:"orgs_created"`
246 Override bool `json:"override"` // any per-account value set
247 }
248 d := out{User: u.Username, Repos: RepoLimit(c.Store, limitsOf(c), "user", u.ID), Bytes: ByteLimit(c.Store, limitsOf(c), "user", u.ID),
249 Orgs: OrgLimit(c.Store, limitsOf(c), u.ID), Override: l.Repos != nil || l.Bytes != nil || l.Orgs != nil}
250 d.ReposOwned, _ = c.Store.OwnedRepoCount("user", u.ID)
251 d.BytesOwned = OwnedBytes(c.Store, c.Cfg.Server.Root, "user", u.ID)
252 d.OrgsCreated, _ = c.Store.CreatedOrgCount(u.ID)
253 return c.emit(d, func(w io.Writer) {
254 fmt.Fprintf(w, "%s\trepos %d of %s\tbytes %d of %s\torgs %d of %s\n", d.User,
255 d.ReposOwned, capText(d.Repos), d.BytesOwned, capText(d.Bytes), d.OrgsCreated, capText(d.Orgs))
256 })
257}
258
259func runAdminOrgLimits(c *Ctx, args []string) int {
260 if code := requireInstanceAdmin(c); code >= 0 {
261 return code
262 }
263 if len(args) < 1 {
264 return c.usage()
265 }
266 org, err := c.Store.OrgByName(args[0])
267 if err != nil {
268 return c.fail(protocol.ExitNotFound, "no organization %q", args[0])
269 }
270 l, err := c.Store.OwnerLimits("org", org.ID)
271 if err != nil {
272 return c.fail(protocol.ExitFailure, "%v", err)
273 }
274 set, code := applyLimitFlags(c, args[1:], &l, false)
275 if code >= 0 {
276 return code
277 }
278 if set {
279 if err := c.Store.SetOwnerLimits("org", org.ID, l); err != nil {
280 return c.fail(protocol.ExitFailure, "%v", err)
281 }
282 c.Store.Audit(c.User.ID, "admin org.limits", map[string]any{"org": org.Name, "repos": l.Repos, "bytes": l.Bytes})
283 }
284 type out struct {
285 Org string `json:"org"`
286 Repos int64 `json:"repos"` // effective cap, 0 unlimited
287 Bytes int64 `json:"bytes"` // effective cap, 0 unlimited
288 ReposOwned int64 `json:"repos_owned"`
289 BytesOwned int64 `json:"bytes_owned"`
290 Override bool `json:"override"` // any per-org value set
291 }
292 d := out{Org: org.Name, Repos: RepoLimit(c.Store, limitsOf(c), "org", org.ID), Bytes: ByteLimit(c.Store, limitsOf(c), "org", org.ID),
293 Override: l.Repos != nil || l.Bytes != nil}
294 d.ReposOwned, _ = c.Store.OwnedRepoCount("org", org.ID)
295 d.BytesOwned = OwnedBytes(c.Store, c.Cfg.Server.Root, "org", org.ID)
296 return c.emit(d, func(w io.Writer) {
297 fmt.Fprintf(w, "%s\trepos %d of %s\tbytes %d of %s\n", d.Org, d.ReposOwned, capText(d.Repos), d.BytesOwned, capText(d.Bytes))
298 })
299}