Commit b79de56262

b79de56262481b9e3e545165fe3888e63c528415

parent: 887952c35a

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 00:10 UTC

control: web mints and grants need a sign-in from the last 15 minutes

Ref #297

Layout: unified · split

internal/control/admin.go +5 −4
@@ -31,10 +31,11 @@ func init() {
31 Examples: []string{"admin user show alice"}, 31 Examples: []string{"admin user show alice"},
32 ReadOnly: true, Run: runAdminUserShow}) 32 ReadOnly: true, Run: runAdminUserShow})
33 register(Command{Path: []string{"admin", "user", "promote"}, 33 register(Command{Path: []string{"admin", "user", "promote"},
34 Summary: "make an account an instance admin", 34 NeedsRecentSignIn: true,
35 Usage: "admin user promote <username>", 35 Summary: "make an account an instance admin",
36 Examples: []string{"admin user promote alice"}, 36 Usage: "admin user promote <username>",
37 Run: runAdminUserPromote}) 37 Examples: []string{"admin user promote alice"},
38 Run: runAdminUserPromote})
38 register(Command{Path: []string{"admin", "user", "demote"}, 39 register(Command{Path: []string{"admin", "user", "demote"},
39 Summary: "remove instance admin from an account (never the last one)", 40 Summary: "remove instance admin from an account (never the last one)",
40 Usage: "admin user demote <username>", 41 Usage: "admin user demote <username>",
internal/control/adminhost.go +10 −9
@@ -32,17 +32,18 @@ func init() {
32 }, 32 },
33 Examples: []string{"admin user create alice --email alice@example.org --key - < key.pub"}, 33 Examples: []string{"admin user create alice --email alice@example.org --key - < key.pub"},
34 ReadsStdin: true, 34 ReadsStdin: true,
35 MintsCredential: true, Run: runAdminUserCreate}) 35 MintsCredential: true, NeedsRecentSignIn: true, Run: runAdminUserCreate})
36 register(Command{Path: []string{"admin", "user", "disable"}, 36 register(Command{Path: []string{"admin", "user", "disable"},
37 Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled", 37 Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
38 Usage: "admin user disable <username>", 38 Usage: "admin user disable <username>",
39 Examples: []string{"admin user disable alice"}, 39 Examples: []string{"admin user disable alice"},
40 Run: runAdminUserDisable}) 40 Run: runAdminUserDisable})
41 register(Command{Path: []string{"admin", "user", "enable"}, 41 register(Command{Path: []string{"admin", "user", "enable"},
42 Summary: "restore a suspended account", 42 NeedsRecentSignIn: true,
43 Usage: "admin user enable <username>", 43 Summary: "restore a suspended account",
44 Examples: []string{"admin user enable alice"}, 44 Usage: "admin user enable <username>",
45 Run: runAdminUserEnable}) 45 Examples: []string{"admin user enable alice"},
46 Run: runAdminUserEnable})
46 register(Command{Path: []string{"admin", "user", "delete"}, 47 register(Command{Path: []string{"admin", "user", "delete"},
47 Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)", 48 Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)",
48 Usage: "admin user delete <username> --yes", 49 Usage: "admin user delete <username> --yes",
@@ -55,8 +56,8 @@ func init() {
55 Summary: "mark an address verified by admin assertion", 56 Summary: "mark an address verified by admin assertion",
56 Usage: "admin email verify <username> <address>", 57 Usage: "admin email verify <username> <address>",
57 Examples: []string{"admin email verify alice alice@example.org"}, 58 Examples: []string{"admin email verify alice alice@example.org"},
58 MintsCredential: true, 59 MintsCredential: true, NeedsRecentSignIn: true,
59 Run: runAdminEmailVerify}) 60 Run: runAdminEmailVerify})
60 register(Command{Path: []string{"admin", "invite"}, 61 register(Command{Path: []string{"admin", "invite"},
61 Summary: "issue a registration invite and mail its code", 62 Summary: "issue a registration invite and mail its code",
62 Usage: "admin invite --email <address>", 63 Usage: "admin invite --email <address>",
@@ -64,8 +65,8 @@ func init() {
64 {"--email", "<address>", "who the invite is for", ""}, 65 {"--email", "<address>", "who the invite is for", ""},
65 }, 66 },
66 Examples: []string{"admin invite --email alice@example.org"}, 67 Examples: []string{"admin invite --email alice@example.org"},
67 MintsCredential: true, 68 MintsCredential: true, NeedsRecentSignIn: true,
68 Run: runAdminInvite}) 69 Run: runAdminInvite})
69 register(Command{Path: []string{"admin", "stats"}, 70 register(Command{Path: []string{"admin", "stats"},
70 Summary: "instance statistics: counts and per-repository disk usage", 71 Summary: "instance statistics: counts and per-repository disk usage",
71 Usage: "admin stats", 72 Usage: "admin stats",
internal/control/build.go +5 −4
@@ -69,10 +69,11 @@ func init() {
69 // repo's builds as environment variables. Same discipline as mirror 69 // repo's builds as environment variables. Same discipline as mirror
70 // tokens — the value never appears in argv, logs, or output. 70 // tokens — the value never appears in argv, logs, or output.
71 register(Command{Path: []string{"repo", "secret", "set"}, 71 register(Command{Path: []string{"repo", "secret", "set"},
72 Summary: "set a build secret", 72 NeedsRecentSignIn: true,
73 Usage: "repo secret set <owner/name> <NAME> (value on stdin)", 73 Summary: "set a build secret",
74 Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"}, 74 Usage: "repo secret set <owner/name> <NAME> (value on stdin)",
75 ReadsStdin: true, Run: runSecretSet}) 75 Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"},
76 ReadsStdin: true, Run: runSecretSet})
76 register(Command{Path: []string{"repo", "secret", "remove"}, 77 register(Command{Path: []string{"repo", "secret", "remove"},
77 Summary: "remove a build secret", 78 Summary: "remove a build secret",
78 Usage: "repo secret remove <owner/name> <NAME>", 79 Usage: "repo secret remove <owner/name> <NAME>",
internal/control/control.go +30 −1
@@ -69,6 +69,27 @@ type Ctx struct {
69 Stopping <-chan struct{} 69 Stopping <-chan struct{}
70} 70}
71 71
72// SourceWeb is Ctx.Source for a request from a browser session. Its
73// User.SignedInAt is when that session signed in.
74const SourceWeb = "web"
75
76// ReauthWindow is how long after signing in a browser session may run a
77// NeedsRecentSignIn command. A session lasts days and its cookie is a
78// bearer credential; what it creates or grants must come from a recent
79// sign-in (#297).
80const ReauthWindow = 15 * time.Minute
81
82// ReauthRefusal is what a web session signed in longer ago than
83// ReauthWindow gets; the web shows a sign-in link beside it.
84var ReauthRefusal = fmt.Sprintf("this action from the web needs a sign-in from the last %d minutes; sign in again, then submit the form again",
85 int(ReauthWindow/time.Minute))
86
87// staleSignIn reports whether a web session that signed in at at is too
88// old, at now, to run a NeedsRecentSignIn command. A zero at is stale.
89func staleSignIn(at, now time.Time) bool {
90 return now.Sub(at) > ReauthWindow
91}
92
72// usage reports a bad invocation with the command's registered usage, 93// usage reports a bad invocation with the command's registered usage,
73// the one source of it. 94// the one source of it.
74func (c *Ctx) usage() int { 95func (c *Ctx) usage() int {
@@ -104,7 +125,12 @@ type Command struct {
104 // to obtain one: tokens, keys, login links, invites, accounts, 125 // to obtain one: tokens, keys, login links, invites, accounts,
105 // verified addresses. An expiring credential may not run it. 126 // verified addresses. An expiring credential may not run it.
106 MintsCredential bool 127 MintsCredential bool
107 Run func(c *Ctx, args []string) int 128 // NeedsRecentSignIn marks a command a browser session may run only
129 // within ReauthWindow of signing in: every MintsCredential command,
130 // and those that give an account lasting access or open a standing
131 // channel out of the instance.
132 NeedsRecentSignIn bool
133 Run func(c *Ctx, args []string) int
108} 134}
109 135
110var registry []Command 136var registry []Command
@@ -212,6 +238,9 @@ func runChecked(c *Ctx, cmd Command, args []string) int {
212 if c.User.Disabled { 238 if c.User.Disabled {
213 return c.fail(protocol.ExitDenied, "this account is disabled; ask an instance admin to enable it") 239 return c.fail(protocol.ExitDenied, "this account is disabled; ask an instance admin to enable it")
214 } 240 }
241 if cmd.NeedsRecentSignIn && c.Source == SourceWeb && staleSignIn(c.User.SignedInAt, time.Now()) {
242 return c.fail(protocol.ExitDenied, "%s", ReauthRefusal)
243 }
215 // The admin noun is gated here as well as in each handler, so a new 244 // The admin noun is gated here as well as in each handler, so a new
216 // admin command that forgets requireInstanceAdmin is still refused. 245 // admin command that forgets requireInstanceAdmin is still refused.
217 if cmd.Path[0] == "admin" && !c.User.IsAdmin { 246 if cmd.Path[0] == "admin" && !c.User.IsAdmin {
internal/control/deploykey.go +1 −1
@@ -23,7 +23,7 @@ func init() {
23 }, 23 },
24 Examples: []string{"repo deploy-key add krz/gitbay < key.pub", "repo deploy-key add krz/gitbay --ttl 30d < key.pub"}, 24 Examples: []string{"repo deploy-key add krz/gitbay < key.pub", "repo deploy-key add krz/gitbay --ttl 30d < key.pub"},
25 ReadsStdin: true, 25 ReadsStdin: true,
26 MintsCredential: true, Run: runDeployKeyAdd}) 26 MintsCredential: true, NeedsRecentSignIn: true, Run: runDeployKeyAdd})
27 register(Command{Path: []string{"repo", "deploy-key", "list"}, 27 register(Command{Path: []string{"repo", "deploy-key", "list"},
28 Summary: "list deploy keys", 28 Summary: "list deploy keys",
29 Usage: "repo deploy-key list <owner/name>", 29 Usage: "repo deploy-key list <owner/name>",
internal/control/identity.go +2 −2
@@ -42,8 +42,8 @@ func init() {
42 }, 42 },
43 Examples: []string{"keys add --label laptop < key.pub", "keys add --scope git --ttl 90d < ci.pub"}, 43 Examples: []string{"keys add --label laptop < key.pub", "keys add --scope git --ttl 90d < ci.pub"},
44 ReadsStdin: true, 44 ReadsStdin: true,
45 MintsCredential: true, 45 MintsCredential: true, NeedsRecentSignIn: true,
46 Run: runKeysAdd, 46 Run: runKeysAdd,
47 }) 47 })
48 register(Command{ 48 register(Command{
49 Path: []string{"keys", "label"}, 49 Path: []string{"keys", "label"},
internal/control/mirrorcmd.go +3 −2
@@ -16,8 +16,9 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"repo", "mirror", "add"}, 18 register(Command{Path: []string{"repo", "mirror", "add"},
19 Summary: "mirror to or from a remote", 19 NeedsRecentSignIn: true,
20 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]", 20 Summary: "mirror to or from a remote",
21 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
21 Flags: []Flag{ 22 Flags: []Flag{
22 {"--direction", "push|pull", "which way the mirror syncs", ""}, 23 {"--direction", "push|pull", "which way the mirror syncs", ""},
23 {"--username", "<u>", "the remote's username", ""}, 24 {"--username", "<u>", "the remote's username", ""},
internal/control/notifications.go +3 −2
@@ -48,8 +48,9 @@ func init() {
48 Examples: []string{"notifications settings watch on"}, 48 Examples: []string{"notifications settings watch on"},
49 Run: runNotificationsSettingsWatch}) 49 Run: runNotificationsSettingsWatch})
50 register(Command{Path: []string{"notifications", "device", "add"}, 50 register(Command{Path: []string{"notifications", "device", "add"},
51 Summary: "register an Apple device for push, token on stdin", 51 NeedsRecentSignIn: true,
52 Usage: "notifications device add [--label <name>] < token", 52 Summary: "register an Apple device for push, token on stdin",
53 Usage: "notifications device add [--label <name>] < token",
53 Flags: []Flag{ 54 Flags: []Flag{
54 {"--label", "<name>", "a name for the device", ""}, 55 {"--label", "<name>", "a name for the device", ""},
55 }, 56 },
internal/control/org.go +3 −2
@@ -37,8 +37,9 @@ func init() {
37 }, 37 },
38 Examples: []string{"org delete krz --yes"}, Run: runOrgDelete}) 38 Examples: []string{"org delete krz --yes"}, Run: runOrgDelete})
39 register(Command{Path: []string{"org", "members", "add"}, 39 register(Command{Path: []string{"org", "members", "add"},
40 Summary: "add or update a member", 40 NeedsRecentSignIn: true,
41 Usage: "org members add <org> <user> [--role member|admin]", 41 Summary: "add or update a member",
42 Usage: "org members add <org> <user> [--role member|admin]",
42 Flags: []Flag{ 43 Flags: []Flag{
43 {"--role", "member|admin", "the member's role", "member"}, 44 {"--role", "member|admin", "the member's role", "member"},
44 }, 45 },
internal/control/reauth_test.go added +136
@@ -0,0 +1,136 @@
1package control
2
3import (
4 "slices"
5 "strings"
6 "testing"
7 "time"
8
9 "gitbay.org/gitbay/internal/protocol"
10 "gitbay.org/gitbay/internal/store"
11)
12
13func TestStaleSignInBoundary(t *testing.T) {
14 at := time.Now()
15 if staleSignIn(at, at.Add(ReauthWindow)) {
16 t.Error("exactly ReauthWindow counted as stale")
17 }
18 if !staleSignIn(at, at.Add(ReauthWindow+time.Second)) {
19 t.Error("ReauthWindow plus a second counted as fresh")
20 }
21 if !staleSignIn(time.Time{}, at) {
22 t.Error("a zero sign-in time counted as fresh")
23 }
24}
25
26// A browser session runs NeedsRecentSignIn commands only within
27// ReauthWindow of signing in; SSH, the API and the host carry no
28// session and are not affected (#297).
29func TestRecentSignInGate(t *testing.T) {
30 refusals = &refusalLimiter{seen: map[int64]*refusalWindow{}}
31 st, repo, uid := newQueueTestRepo(t)
32 if _, err := st.CreateUser("bob", false); err != nil {
33 t.Fatal(err)
34 }
35 run := func(source string, signedIn time.Time, stdin string, argv ...string) (string, int) {
36 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid, Username: "alice", SignedInAt: signedIn})
37 c.Cfg.Limits.WriteRate = -1
38 c.Source = source
39 c.ViaAPI = source == SourceWeb || source == "api"
40 c.Stdin = strings.NewReader(stdin)
41 code := Dispatch(c, argv)
42 return strings.TrimSpace(errOut.String()), code
43 }
44 fresh := time.Now().Add(-time.Minute)
45 stale := time.Now().Add(-ReauthWindow - time.Minute)
46 staleKey := authorizedKey(t, "stale")
47
48 for _, tc := range []struct {
49 name string
50 signedIn time.Time
51 stdin string
52 argv []string
53 }{
54 {"stale keys add", stale, staleKey, []string{"keys", "add"}},
55 {"stale token create", stale, "", []string{"token", "create", "--name", "x"}},
56 {"stale repo access grant", stale, "", []string{"repo", "access", "grant", repo.Path(), "bob", "write"}},
57 {"zero sign-in time", time.Time{}, authorizedKey(t, "zero"), []string{"keys", "add"}},
58 } {
59 if msg, code := run(SourceWeb, tc.signedIn, tc.stdin, tc.argv...); code != protocol.ExitDenied || msg != ReauthRefusal {
60 t.Errorf("%s: exit %d, %q", tc.name, code, msg)
61 }
62 }
63 if msg, code := run(SourceWeb, fresh, authorizedKey(t, "fresh"), "keys", "add"); code != protocol.ExitOK {
64 t.Fatalf("fresh session: exit %d, %q", code, msg)
65 }
66 // SSH, the API and the host have no session; a zero SignedInAt is
67 // what they carry.
68 for _, source := range []string{"SHA256:abc", "api", "host"} {
69 if msg, code := run(source, time.Time{}, authorizedKey(t, source), "keys", "add"); code != protocol.ExitOK {
70 t.Fatalf("%s: exit %d, %q", source, code, msg)
71 }
72 }
73 // A command that grants nothing is not held back.
74 if msg, code := run(SourceWeb, stale, "", "keys", "list"); code != protocol.ExitOK {
75 t.Fatalf("keys list on a stale session: exit %d, %q", code, msg)
76 }
77 keys, err := st.ListSSHKeys(uid)
78 if err != nil || len(keys) != 4 {
79 t.Fatalf("keys: %d %v, want the fresh, ssh, api and host ones", len(keys), err)
80 }
81 got, err := st.AuditEntries(store.AuditFilter{ActionPrefix: "refused ", Limit: 10})
82 if err != nil {
83 t.Fatal(err)
84 }
85 if len(got) != 4 {
86 t.Fatalf("refusal audit rows: %+v", got)
87 }
88 keyText := strings.Fields(staleKey)[1]
89 for _, e := range got {
90 if strings.Contains(e.Data, keyText) {
91 t.Errorf("%s kept the key: %s", e.Action, e.Data)
92 }
93 }
94}
95
96// The set of commands a stale web session is refused. Adding one is a
97// decision; it shows up here.
98func TestNeedsRecentSignInSet(t *testing.T) {
99 var got []string
100 for _, cmd := range Commands() {
101 if cmd.MintsCredential && !cmd.NeedsRecentSignIn {
102 t.Errorf("%s mints a credential without NeedsRecentSignIn", joinPath(cmd.Path))
103 }
104 if cmd.NeedsRecentSignIn {
105 got = append(got, joinPath(cmd.Path))
106 }
107 }
108 slices.Sort(got)
109 want := []string{
110 "admin email verify",
111 "admin invite",
112 "admin user create",
113 "admin user enable",
114 "admin user promote",
115 "email verify",
116 "keys add",
117 "notifications device add",
118 "org members add",
119 "org settings members-role",
120 "org team add",
121 "org team grant",
122 "pgp add",
123 "repo access grant",
124 "repo deploy-key add",
125 "repo mirror add",
126 "repo runner add",
127 "repo secret set",
128 "repo transfer",
129 "token create",
130 "web login",
131 "webhook add",
132 }
133 if !slices.Equal(got, want) {
134 t.Fatalf("NeedsRecentSignIn commands:\n got %q\nwant %q", got, want)
135 }
136}
internal/control/register.go +2 −2
@@ -38,8 +38,8 @@ func init() {
38 register(Command{Path: []string{"email", "verify"}, 38 register(Command{Path: []string{"email", "verify"},
39 Summary: "confirm a verification code", 39 Summary: "confirm a verification code",
40 Usage: "email verify <code>", 40 Usage: "email verify <code>",
41 MintsCredential: true, 41 MintsCredential: true, NeedsRecentSignIn: true,
42 Examples: []string{"email verify abc123"}, Run: runEmailVerify}) 42 Examples: []string{"email verify abc123"}, Run: runEmailVerify})
43 register(Command{Path: []string{"email", "list"}, 43 register(Command{Path: []string{"email", "list"},
44 Summary: "list the addresses on your account", 44 Summary: "list the addresses on your account",
45 Usage: "email list", 45 Usage: "email list",
internal/control/repo.go +10 −8
@@ -50,10 +50,11 @@ func init() {
50 Examples: []string{"repo show krz/gitbay"}, 50 Examples: []string{"repo show krz/gitbay"},
51 ReadOnly: true, Run: runRepoShow}) 51 ReadOnly: true, Run: runRepoShow})
52 register(Command{Path: []string{"repo", "transfer"}, 52 register(Command{Path: []string{"repo", "transfer"},
53 Summary: "move a repository to another owner", 53 NeedsRecentSignIn: true,
54 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", 54 Summary: "move a repository to another owner",
55 Examples: []string{"repo transfer krz/gitbay krazywarez"}, 55 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)",
56 Run: runRepoTransfer}) 56 Examples: []string{"repo transfer krz/gitbay krazywarez"},
57 Run: runRepoTransfer})
57 register(Command{Path: []string{"repo", "rename"}, 58 register(Command{Path: []string{"repo", "rename"},
58 Summary: "rename a repository", 59 Summary: "rename a repository",
59 Usage: "repo rename <owner/name> <new-name> (clone URLs change)", 60 Usage: "repo rename <owner/name> <new-name> (clone URLs change)",
@@ -68,10 +69,11 @@ func init() {
68 Examples: []string{"repo delete cmc/scratch --yes"}, 69 Examples: []string{"repo delete cmc/scratch --yes"},
69 Run: runRepoDelete}) 70 Run: runRepoDelete})
70 register(Command{Path: []string{"repo", "access", "grant"}, 71 register(Command{Path: []string{"repo", "access", "grant"},
71 Summary: "grant access", 72 NeedsRecentSignIn: true,
72 Usage: "repo access grant <owner/name> <user> read|write|admin", 73 Summary: "grant access",
73 Examples: []string{"repo access grant krz/gitbay cmc write"}, 74 Usage: "repo access grant <owner/name> <user> read|write|admin",
74 Run: runAccessGrant}) 75 Examples: []string{"repo access grant krz/gitbay cmc write"},
76 Run: runAccessGrant})
75 register(Command{Path: []string{"repo", "access", "revoke"}, 77 register(Command{Path: []string{"repo", "access", "revoke"},
76 Summary: "revoke access", 78 Summary: "revoke access",
77 Usage: "repo access revoke <owner/name> <user>", 79 Usage: "repo access revoke <owner/name> <user>",
internal/control/runnerrepo.go +1 −1
@@ -23,7 +23,7 @@ func init() {
23 Usage: "repo runner add <owner/name> < key.pub", 23 Usage: "repo runner add <owner/name> < key.pub",
24 Examples: []string{"repo runner add krz/gitbay < key.pub"}, 24 Examples: []string{"repo runner add krz/gitbay < key.pub"},
25 ReadsStdin: true, 25 ReadsStdin: true,
26 MintsCredential: true, Run: runRepoRunnerAdd}) 26 MintsCredential: true, NeedsRecentSignIn: true, Run: runRepoRunnerAdd})
27 register(Command{Path: []string{"repo", "runner", "list"}, 27 register(Command{Path: []string{"repo", "runner", "list"},
28 Summary: "list the runners attached to a repository", 28 Summary: "list the runners attached to a repository",
29 Usage: "repo runner list <owner/name>", 29 Usage: "repo runner list <owner/name>",
internal/control/sig.go +5 −4
@@ -18,10 +18,11 @@ import (
18 18
19func init() { 19func init() {
20 register(Command{Path: []string{"pgp", "add"}, 20 register(Command{Path: []string{"pgp", "add"},
21 Summary: "register an OpenPGP public key (armored)", 21 NeedsRecentSignIn: true,
22 Usage: "pgp add < key.asc", 22 Summary: "register an OpenPGP public key (armored)",
23 Examples: []string{"pgp add < key.asc"}, 23 Usage: "pgp add < key.asc",
24 ReadsStdin: true, Run: runPGPAdd}) 24 Examples: []string{"pgp add < key.asc"},
25 ReadsStdin: true, Run: runPGPAdd})
25 register(Command{Path: []string{"pgp", "list"}, 26 register(Command{Path: []string{"pgp", "list"},
26 Summary: "list registered OpenPGP keys", 27 Summary: "list registered OpenPGP keys",
27 Usage: "pgp list", 28 Usage: "pgp list",
internal/control/teams.go +12 −9
@@ -30,25 +30,28 @@ func init() {
30 Usage: "org team show <org> <team>", 30 Usage: "org team show <org> <team>",
31 Examples: []string{"org team show krz maintainers"}, ReadOnly: true, Run: runTeamShow}) 31 Examples: []string{"org team show krz maintainers"}, ReadOnly: true, Run: runTeamShow})
32 register(Command{Path: []string{"org", "team", "add"}, 32 register(Command{Path: []string{"org", "team", "add"},
33 Summary: "add org members to a team", 33 NeedsRecentSignIn: true,
34 Usage: "org team add <org> <team> <user>...", 34 Summary: "add org members to a team",
35 Examples: []string{"org team add krz maintainers cmc"}, Run: runTeamAdd}) 35 Usage: "org team add <org> <team> <user>...",
36 Examples: []string{"org team add krz maintainers cmc"}, Run: runTeamAdd})
36 register(Command{Path: []string{"org", "team", "remove"}, 37 register(Command{Path: []string{"org", "team", "remove"},
37 Summary: "remove members from a team", 38 Summary: "remove members from a team",
38 Usage: "org team remove <org> <team> <user>...", 39 Usage: "org team remove <org> <team> <user>...",
39 Examples: []string{"org team remove krz maintainers cmc"}, Run: runTeamRemove}) 40 Examples: []string{"org team remove krz maintainers cmc"}, Run: runTeamRemove})
40 register(Command{Path: []string{"org", "team", "grant"}, 41 register(Command{Path: []string{"org", "team", "grant"},
41 Summary: "grant a team a role on an org repo", 42 NeedsRecentSignIn: true,
42 Usage: "org team grant <org> <team> <owner/name> read|write|admin", 43 Summary: "grant a team a role on an org repo",
43 Examples: []string{"org team grant krz maintainers krz/gitbay write"}, Run: runTeamGrant}) 44 Usage: "org team grant <org> <team> <owner/name> read|write|admin",
45 Examples: []string{"org team grant krz maintainers krz/gitbay write"}, Run: runTeamGrant})
44 register(Command{Path: []string{"org", "team", "revoke"}, 46 register(Command{Path: []string{"org", "team", "revoke"},
45 Summary: "revoke a team's grant", 47 Summary: "revoke a team's grant",
46 Usage: "org team revoke <org> <team> <owner/name>", 48 Usage: "org team revoke <org> <team> <owner/name>",
47 Examples: []string{"org team revoke krz maintainers krz/gitbay"}, Run: runTeamRevoke}) 49 Examples: []string{"org team revoke krz maintainers krz/gitbay"}, Run: runTeamRevoke})
48 register(Command{Path: []string{"org", "settings", "members-role"}, 50 register(Command{Path: []string{"org", "settings", "members-role"},
49 Summary: "role plain membership implies on every org repo", 51 NeedsRecentSignIn: true,
50 Usage: "org settings members-role <org> write|read|none (default write)", 52 Summary: "role plain membership implies on every org repo",
51 Examples: []string{"org settings members-role krz read"}, Run: runOrgMembersRole}) 53 Usage: "org settings members-role <org> write|read|none (default write)",
54 Examples: []string{"org settings members-role krz read"}, Run: runOrgMembersRole})
52} 55}
53 56
54// orgAdminRef resolves an org and requires the caller to admin it. 57// orgAdminRef resolves an org and requires the caller to admin it.
internal/control/token.go +2 −2
@@ -22,8 +22,8 @@ func init() {
22 {"--ttl", "30d|720h", "how long the token is valid; an expiring token cannot mint credentials", "never expires"}, 22 {"--ttl", "30d|720h", "how long the token is valid; an expiring token cannot mint credentials", "never expires"},
23 }, 23 },
24 Examples: []string{"token create --name laptop --ttl 30d", "token create --name phone --scope full"}, 24 Examples: []string{"token create --name laptop --ttl 30d", "token create --name phone --scope full"},
25 MintsCredential: true, 25 MintsCredential: true, NeedsRecentSignIn: true,
26 Run: runTokenCreate}) 26 Run: runTokenCreate})
27 register(Command{Path: []string{"token", "list"}, 27 register(Command{Path: []string{"token", "list"},
28 Summary: "list API tokens", 28 Summary: "list API tokens",
29 Usage: "token list", 29 Usage: "token list",
internal/control/web.go +2 −2
@@ -16,8 +16,8 @@ func init() {
16 register(Command{Path: []string{"web", "login"}, 16 register(Command{Path: []string{"web", "login"},
17 Summary: "mint a one-time browser login URL", 17 Summary: "mint a one-time browser login URL",
18 Usage: "web login", 18 Usage: "web login",
19 MintsCredential: true, 19 MintsCredential: true, NeedsRecentSignIn: true,
20 Examples: []string{"web login"}, Run: runWebLogin}) 20 Examples: []string{"web login"}, Run: runWebLogin})
21 register(Command{Path: []string{"web", "sessions", "list"}, 21 register(Command{Path: []string{"web", "sessions", "list"},
22 Summary: "list your browser sessions", 22 Summary: "list your browser sessions",
23 Usage: "web sessions list", 23 Usage: "web sessions list",
internal/control/webhook.go +3 −2
@@ -15,8 +15,9 @@ import (
15 15
16func init() { 16func init() {
17 register(Command{Path: []string{"webhook", "add"}, 17 register(Command{Path: []string{"webhook", "add"},
18 Summary: "add a webhook", 18 NeedsRecentSignIn: true,
19 Usage: "webhook add <owner/name> <url> [--secret -] [--events push,issue.created|*]", 19 Summary: "add a webhook",
20 Usage: "webhook add <owner/name> <url> [--secret -] [--events push,issue.created|*]",
20 Flags: []Flag{ 21 Flags: []Flag{
21 {"--secret", "-", "read the secret that signs deliveries from stdin", ""}, 22 {"--secret", "-", "read the secret that signs deliveries from stdin", ""},
22 {"--events", "push,issue.created|*", "which events to send", "*"}, 23 {"--events", "push,issue.created|*", "which events to send", "*"},
internal/httpd/account_test.go +2 −1
@@ -7,6 +7,7 @@ import (
7 "strconv" 7 "strconv"
8 "strings" 8 "strings"
9 "testing" 9 "testing"
10 "time"
10 11
11 "gitbay.org/gitbay/internal/config" 12 "gitbay.org/gitbay/internal/config"
12 "gitbay.org/gitbay/internal/store" 13 "gitbay.org/gitbay/internal/store"
@@ -300,7 +301,7 @@ func newTokenTestServer(t *testing.T) (*Server, *store.Store, store.User) {
300 if err != nil { 301 if err != nil {
301 t.Fatal(err) 302 t.Fatal(err)
302 } 303 }
303 return New(config.Default(), st, nil), st, store.User{ID: uid, Username: "alice"} 304 return New(config.Default(), st, nil), st, store.User{ID: uid, Username: "alice", SignedInAt: time.Now()}
304} 305}
305 306
306// The settings page lists a user's API tokens with scope and expiry, 307// The settings page lists a user's API tokens with scope and expiry,