internal/control/quota.go

299 lines · 10412 bytes

18 symbols in this file
  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}