internal/control/quota.go
160 lines · 5094 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 one account owns directly. The limit is the account's
16// override when set, else the configured default; 0 is unlimited.
17
18// RepoLimit is the account's repository cap, 0 for none.
19func RepoLimit(st *store.Store, cfg configLimits, userID int64) int64 {
20 if l, err := st.UserLimits(userID); err == nil && l.Repos != nil {
21 return *l.Repos
22 }
23 return int64(cfg.MaxReposPerUser)
24}
25
26// ByteLimit is the account's storage cap in bytes, 0 for none.
27func ByteLimit(st *store.Store, cfg configLimits, userID int64) int64 {
28 if l, err := st.UserLimits(userID); err == nil && l.Bytes != nil {
29 return *l.Bytes
30 }
31 return cfg.MaxBytesPerUser
32}
33
34// OwnedBytes is the disk taken by the repositories a user owns directly.
35func OwnedBytes(st *store.Store, root string, userID int64) int64 {
36 repos, err := st.ListReposForOwner("user", userID)
37 if err != nil {
38 return 0
39 }
40 var total int64
41 for _, r := range repos {
42 total += gitutil.DirSize(RepoDir(root, r.OwnerName, r.Name))
43 }
44 return total
45}
46
47// configLimits is the slice of config the quota functions read, so the
48// sshd package can pass its Limits without importing control's Ctx.
49type configLimits struct {
50 MaxReposPerUser int
51 MaxBytesPerUser int64
52}
53
54// QuotaConfig is what sshd passes: the limits section of the config.
55func QuotaConfig(cfg config.Config) configLimits {
56 return configLimits{cfg.Limits.MaxReposPerUser, cfg.Limits.MaxBytesPerUser}
57}
58
59func limitsOf(c *Ctx) configLimits {
60 return configLimits{c.Cfg.Limits.MaxReposPerUser, c.Cfg.Limits.MaxBytesPerUser}
61}
62
63// checkRepoQuota refuses a new user-owned repository past the cap.
64// repoCreateMu serialises the quota check with the insert that follows
65// it, so two concurrent creates cannot both pass the count (#108). One
66// process serves the instance, so a process-wide lock is the whole story.
67var repoCreateMu sync.Mutex
68
69func checkRepoQuota(c *Ctx) int {
70 limit := RepoLimit(c.Store, limitsOf(c), c.User.ID)
71 if limit == 0 {
72 return -1
73 }
74 n, err := c.Store.OwnedRepoCount(c.User.ID)
75 if err != nil {
76 return c.fail(protocol.ExitFailure, "%v", err)
77 }
78 if n >= limit {
79 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)
80 }
81 return -1
82}
83
84func init() {
85 register(Command{Path: []string{"admin", "user", "limits"},
86 Summary: "show or set an account's repository and storage caps (instance admins)",
87 Usage: "admin user limits <username> [--repos <n>|default] [--bytes <n>|default]",
88 SSHOnly: true, Run: runAdminUserLimits})
89}
90
91func runAdminUserLimits(c *Ctx, args []string) int {
92 if code := requireInstanceAdmin(c); code >= 0 {
93 return code
94 }
95 if len(args) < 1 {
96 return c.fail(protocol.ExitUsage, "usage: admin user limits <username> [--repos <n>|default] [--bytes <n>|default]")
97 }
98 u, err := c.Store.UserByUsername(args[0])
99 if err != nil {
100 return c.fail(protocol.ExitNotFound, "no user %q", args[0])
101 }
102 l, err := c.Store.UserLimits(u.ID)
103 if err != nil {
104 return c.fail(protocol.ExitFailure, "%v", err)
105 }
106 set := false
107 for i := 1; i < len(args); i++ {
108 if i+1 >= len(args) {
109 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
110 }
111 v := args[i+1]
112 var target **int64
113 switch args[i] {
114 case "--repos":
115 target = &l.Repos
116 case "--bytes":
117 target = &l.Bytes
118 default:
119 return c.fail(protocol.ExitUsage, "usage: admin user limits <username> [--repos <n>|default] [--bytes <n>|default]")
120 }
121 if v == "default" {
122 *target = nil
123 } else {
124 n, err := strconv.ParseInt(v, 10, 64)
125 if err != nil || n < 0 {
126 return c.fail(protocol.ExitUsage, "%s takes a non-negative number or default", args[i])
127 }
128 *target = &n
129 }
130 set = true
131 i++
132 }
133 if set {
134 if err := c.Store.SetUserLimits(u.ID, l); err != nil {
135 return c.fail(protocol.ExitFailure, "%v", err)
136 }
137 c.Store.Audit(c.User.ID, "admin user.limits", map[string]any{"user": u.Username, "repos": l.Repos, "bytes": l.Bytes})
138 }
139 type out struct {
140 User string `json:"user"`
141 Repos int64 `json:"repos"` // effective cap, 0 unlimited
142 Bytes int64 `json:"bytes"` // effective cap, 0 unlimited
143 ReposOwned int64 `json:"repos_owned"`
144 BytesOwned int64 `json:"bytes_owned"`
145 Override bool `json:"override"` // any per-account value set
146 }
147 d := out{User: u.Username, Repos: RepoLimit(c.Store, limitsOf(c), u.ID), Bytes: ByteLimit(c.Store, limitsOf(c), u.ID),
148 Override: l.Repos != nil || l.Bytes != nil}
149 d.ReposOwned, _ = c.Store.OwnedRepoCount(u.ID)
150 d.BytesOwned = OwnedBytes(c.Store, c.Cfg.Server.Root, u.ID)
151 return c.emit(d, func(w io.Writer) {
152 cap := func(n int64) string {
153 if n == 0 {
154 return "unlimited"
155 }
156 return strconv.FormatInt(n, 10)
157 }
158 fmt.Fprintf(w, "%s\trepos %d of %s\tbytes %d of %s\n", d.User, d.ReposOwned, cap(d.Repos), d.BytesOwned, cap(d.Bytes))
159 })
160}