internal/control/quota.go
165 lines · 5124 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 Flags: []Flag{
89 {"--repos", "<n>|default", "the account's repository cap", ""},
90 {"--bytes", "<n>|default", "the account's storage cap", ""},
91 },
92 Examples: []string{"admin user limits alice", "admin user limits alice --repos 50"},
93 Run: runAdminUserLimits})
94}
95
96func runAdminUserLimits(c *Ctx, args []string) int {
97 if code := requireInstanceAdmin(c); code >= 0 {
98 return code
99 }
100 if len(args) < 1 {
101 return c.usage()
102 }
103 u, err := c.Store.UserByUsername(args[0])
104 if err != nil {
105 return c.fail(protocol.ExitNotFound, "no user %q", args[0])
106 }
107 l, err := c.Store.UserLimits(u.ID)
108 if err != nil {
109 return c.fail(protocol.ExitFailure, "%v", err)
110 }
111 set := false
112 for i := 1; i < len(args); i++ {
113 if i+1 >= len(args) {
114 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
115 }
116 v := args[i+1]
117 var target **int64
118 switch args[i] {
119 case "--repos":
120 target = &l.Repos
121 case "--bytes":
122 target = &l.Bytes
123 default:
124 return c.usage()
125 }
126 if v == "default" {
127 *target = nil
128 } else {
129 n, err := strconv.ParseInt(v, 10, 64)
130 if err != nil || n < 0 {
131 return c.fail(protocol.ExitUsage, "%s takes a non-negative number or default", args[i])
132 }
133 *target = &n
134 }
135 set = true
136 i++
137 }
138 if set {
139 if err := c.Store.SetUserLimits(u.ID, l); err != nil {
140 return c.fail(protocol.ExitFailure, "%v", err)
141 }
142 c.Store.Audit(c.User.ID, "admin user.limits", map[string]any{"user": u.Username, "repos": l.Repos, "bytes": l.Bytes})
143 }
144 type out struct {
145 User string `json:"user"`
146 Repos int64 `json:"repos"` // effective cap, 0 unlimited
147 Bytes int64 `json:"bytes"` // effective cap, 0 unlimited
148 ReposOwned int64 `json:"repos_owned"`
149 BytesOwned int64 `json:"bytes_owned"`
150 Override bool `json:"override"` // any per-account value set
151 }
152 d := out{User: u.Username, Repos: RepoLimit(c.Store, limitsOf(c), u.ID), Bytes: ByteLimit(c.Store, limitsOf(c), u.ID),
153 Override: l.Repos != nil || l.Bytes != nil}
154 d.ReposOwned, _ = c.Store.OwnedRepoCount(u.ID)
155 d.BytesOwned = OwnedBytes(c.Store, c.Cfg.Server.Root, u.ID)
156 return c.emit(d, func(w io.Writer) {
157 cap := func(n int64) string {
158 if n == 0 {
159 return "unlimited"
160 }
161 return strconv.FormatInt(n, 10)
162 }
163 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))
164 })
165}