internal/control/quota.go

c2d81105344db93058ba50f63e5e81c49abd4b7f
gitbay/internal/control/quota.go history · blame · raw

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}