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