internal/control/quota.go

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

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}