Commit b79de56262
Verified · cmc
Layout: unified · split
internal/control/admin.go +5 −4
| @@ -31,10 +31,11 @@ func init() { | ||
| 31 | 31 | Examples: []string{"admin user show alice"}, |
| 32 | 32 | ReadOnly: true, Run: runAdminUserShow}) |
| 33 | 33 | register(Command{Path: []string{"admin", "user", "promote"}, |
| 34 | Summary: "make an account an instance admin", | |
| 35 | Usage: "admin user promote <username>", | |
| 36 | Examples: []string{"admin user promote alice"}, | |
| 37 | Run: runAdminUserPromote}) | |
| 34 | NeedsRecentSignIn: true, | |
| 35 | Summary: "make an account an instance admin", | |
| 36 | Usage: "admin user promote <username>", | |
| 37 | Examples: []string{"admin user promote alice"}, | |
| 38 | Run: runAdminUserPromote}) | |
| 38 | 39 | register(Command{Path: []string{"admin", "user", "demote"}, |
| 39 | 40 | Summary: "remove instance admin from an account (never the last one)", |
| 40 | 41 | Usage: "admin user demote <username>", |
internal/control/adminhost.go +10 −9
| @@ -32,17 +32,18 @@ func init() { | ||
| 32 | 32 | }, |
| 33 | 33 | Examples: []string{"admin user create alice --email alice@example.org --key - < key.pub"}, |
| 34 | 34 | ReadsStdin: true, |
| 35 | MintsCredential: true, Run: runAdminUserCreate}) | |
| 35 | MintsCredential: true, NeedsRecentSignIn: true, Run: runAdminUserCreate}) | |
| 36 | 36 | register(Command{Path: []string{"admin", "user", "disable"}, |
| 37 | 37 | Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled", |
| 38 | 38 | Usage: "admin user disable <username>", |
| 39 | 39 | Examples: []string{"admin user disable alice"}, |
| 40 | 40 | Run: runAdminUserDisable}) |
| 41 | 41 | register(Command{Path: []string{"admin", "user", "enable"}, |
| 42 | Summary: "restore a suspended account", | |
| 43 | Usage: "admin user enable <username>", | |
| 44 | Examples: []string{"admin user enable alice"}, | |
| 45 | Run: runAdminUserEnable}) | |
| 42 | NeedsRecentSignIn: true, | |
| 43 | Summary: "restore a suspended account", | |
| 44 | Usage: "admin user enable <username>", | |
| 45 | Examples: []string{"admin user enable alice"}, | |
| 46 | Run: runAdminUserEnable}) | |
| 46 | 47 | register(Command{Path: []string{"admin", "user", "delete"}, |
| 47 | 48 | Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)", |
| 48 | 49 | Usage: "admin user delete <username> --yes", |
| @@ -55,8 +56,8 @@ func init() { | ||
| 55 | 56 | Summary: "mark an address verified by admin assertion", |
| 56 | 57 | Usage: "admin email verify <username> <address>", |
| 57 | 58 | Examples: []string{"admin email verify alice alice@example.org"}, |
| 58 | MintsCredential: true, | |
| 59 | Run: runAdminEmailVerify}) | |
| 59 | MintsCredential: true, NeedsRecentSignIn: true, | |
| 60 | Run: runAdminEmailVerify}) | |
| 60 | 61 | register(Command{Path: []string{"admin", "invite"}, |
| 61 | 62 | Summary: "issue a registration invite and mail its code", |
| 62 | 63 | Usage: "admin invite --email <address>", |
| @@ -64,8 +65,8 @@ func init() { | ||
| 64 | 65 | {"--email", "<address>", "who the invite is for", ""}, |
| 65 | 66 | }, |
| 66 | 67 | Examples: []string{"admin invite --email alice@example.org"}, |
| 67 | MintsCredential: true, | |
| 68 | Run: runAdminInvite}) | |
| 68 | MintsCredential: true, NeedsRecentSignIn: true, | |
| 69 | Run: runAdminInvite}) | |
| 69 | 70 | register(Command{Path: []string{"admin", "stats"}, |
| 70 | 71 | Summary: "instance statistics: counts and per-repository disk usage", |
| 71 | 72 | Usage: "admin stats", |
internal/control/build.go +5 −4
| @@ -69,10 +69,11 @@ func init() { | ||
| 69 | 69 | // repo's builds as environment variables. Same discipline as mirror |
| 70 | 70 | // tokens — the value never appears in argv, logs, or output. |
| 71 | 71 | register(Command{Path: []string{"repo", "secret", "set"}, |
| 72 | Summary: "set a build secret", | |
| 73 | Usage: "repo secret set <owner/name> <NAME> (value on stdin)", | |
| 74 | Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"}, | |
| 75 | ReadsStdin: true, Run: runSecretSet}) | |
| 72 | NeedsRecentSignIn: true, | |
| 73 | Summary: "set a build secret", | |
| 74 | Usage: "repo secret set <owner/name> <NAME> (value on stdin)", | |
| 75 | Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"}, | |
| 76 | ReadsStdin: true, Run: runSecretSet}) | |
| 76 | 77 | register(Command{Path: []string{"repo", "secret", "remove"}, |
| 77 | 78 | Summary: "remove a build secret", |
| 78 | 79 | Usage: "repo secret remove <owner/name> <NAME>", |
internal/control/control.go +30 −1
| @@ -69,6 +69,27 @@ type Ctx struct { | ||
| 69 | 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. | |
| 74 | const 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). | |
| 80 | const 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. | |
| 84 | var 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. | |
| 89 | func staleSignIn(at, now time.Time) bool { | |
| 90 | return now.Sub(at) > ReauthWindow | |
| 91 | } | |
| 92 | ||
| 72 | 93 | // usage reports a bad invocation with the command's registered usage, |
| 73 | 94 | // the one source of it. |
| 74 | 95 | func (c *Ctx) usage() int { |
| @@ -104,7 +125,12 @@ type Command struct { | ||
| 104 | 125 | // to obtain one: tokens, keys, login links, invites, accounts, |
| 105 | 126 | // verified addresses. An expiring credential may not run it. |
| 106 | 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 | |
| 110 | 136 | var registry []Command |
| @@ -212,6 +238,9 @@ func runChecked(c *Ctx, cmd Command, args []string) int { | ||
| 212 | 238 | if c.User.Disabled { |
| 213 | 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 | 244 | // The admin noun is gated here as well as in each handler, so a new |
| 216 | 245 | // admin command that forgets requireInstanceAdmin is still refused. |
| 217 | 246 | if cmd.Path[0] == "admin" && !c.User.IsAdmin { |
internal/control/deploykey.go +1 −1
| @@ -23,7 +23,7 @@ func init() { | ||
| 23 | 23 | }, |
| 24 | 24 | Examples: []string{"repo deploy-key add krz/gitbay < key.pub", "repo deploy-key add krz/gitbay --ttl 30d < key.pub"}, |
| 25 | 25 | ReadsStdin: true, |
| 26 | MintsCredential: true, Run: runDeployKeyAdd}) | |
| 26 | MintsCredential: true, NeedsRecentSignIn: true, Run: runDeployKeyAdd}) | |
| 27 | 27 | register(Command{Path: []string{"repo", "deploy-key", "list"}, |
| 28 | 28 | Summary: "list deploy keys", |
| 29 | 29 | Usage: "repo deploy-key list <owner/name>", |
internal/control/identity.go +2 −2
| @@ -42,8 +42,8 @@ func init() { | ||
| 42 | 42 | }, |
| 43 | 43 | Examples: []string{"keys add --label laptop < key.pub", "keys add --scope git --ttl 90d < ci.pub"}, |
| 44 | 44 | ReadsStdin: true, |
| 45 | MintsCredential: true, | |
| 46 | Run: runKeysAdd, | |
| 45 | MintsCredential: true, NeedsRecentSignIn: true, | |
| 46 | Run: runKeysAdd, | |
| 47 | 47 | }) |
| 48 | 48 | register(Command{ |
| 49 | 49 | Path: []string{"keys", "label"}, |
internal/control/mirrorcmd.go +3 −2
| @@ -16,8 +16,9 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | register(Command{Path: []string{"repo", "mirror", "add"}, |
| 19 | Summary: "mirror to or from a remote", | |
| 20 | Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]", | |
| 19 | NeedsRecentSignIn: true, | |
| 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 | 22 | Flags: []Flag{ |
| 22 | 23 | {"--direction", "push|pull", "which way the mirror syncs", ""}, |
| 23 | 24 | {"--username", "<u>", "the remote's username", ""}, |
internal/control/notifications.go +3 −2
| @@ -48,8 +48,9 @@ func init() { | ||
| 48 | 48 | Examples: []string{"notifications settings watch on"}, |
| 49 | 49 | Run: runNotificationsSettingsWatch}) |
| 50 | 50 | register(Command{Path: []string{"notifications", "device", "add"}, |
| 51 | Summary: "register an Apple device for push, token on stdin", | |
| 52 | Usage: "notifications device add [--label <name>] < token", | |
| 51 | NeedsRecentSignIn: true, | |
| 52 | Summary: "register an Apple device for push, token on stdin", | |
| 53 | Usage: "notifications device add [--label <name>] < token", | |
| 53 | 54 | Flags: []Flag{ |
| 54 | 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 | 38 | Examples: []string{"org delete krz --yes"}, Run: runOrgDelete}) |
| 39 | 39 | register(Command{Path: []string{"org", "members", "add"}, |
| 40 | Summary: "add or update a member", | |
| 41 | Usage: "org members add <org> <user> [--role member|admin]", | |
| 40 | NeedsRecentSignIn: true, | |
| 41 | Summary: "add or update a member", | |
| 42 | Usage: "org members add <org> <user> [--role member|admin]", | |
| 42 | 43 | Flags: []Flag{ |
| 43 | 44 | {"--role", "member|admin", "the member's role", "member"}, |
| 44 | 45 | }, |
internal/control/reauth_test.go added +136
| @@ -0,0 +1,136 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "slices" | |
| 5 | "strings" | |
| 6 | "testing" | |
| 7 | "time" | |
| 8 | ||
| 9 | "gitbay.org/gitbay/internal/protocol" | |
| 10 | "gitbay.org/gitbay/internal/store" | |
| 11 | ) | |
| 12 | ||
| 13 | func 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). | |
| 29 | func 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. | |
| 98 | func 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 | 38 | register(Command{Path: []string{"email", "verify"}, |
| 39 | 39 | Summary: "confirm a verification code", |
| 40 | 40 | Usage: "email verify <code>", |
| 41 | MintsCredential: true, | |
| 42 | Examples: []string{"email verify abc123"}, Run: runEmailVerify}) | |
| 41 | MintsCredential: true, NeedsRecentSignIn: true, | |
| 42 | Examples: []string{"email verify abc123"}, Run: runEmailVerify}) | |
| 43 | 43 | register(Command{Path: []string{"email", "list"}, |
| 44 | 44 | Summary: "list the addresses on your account", |
| 45 | 45 | Usage: "email list", |
internal/control/repo.go +10 −8
| @@ -50,10 +50,11 @@ func init() { | ||
| 50 | 50 | Examples: []string{"repo show krz/gitbay"}, |
| 51 | 51 | ReadOnly: true, Run: runRepoShow}) |
| 52 | 52 | register(Command{Path: []string{"repo", "transfer"}, |
| 53 | Summary: "move a repository to another owner", | |
| 54 | Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", | |
| 55 | Examples: []string{"repo transfer krz/gitbay krazywarez"}, | |
| 56 | Run: runRepoTransfer}) | |
| 53 | NeedsRecentSignIn: true, | |
| 54 | Summary: "move a repository to another owner", | |
| 55 | Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", | |
| 56 | Examples: []string{"repo transfer krz/gitbay krazywarez"}, | |
| 57 | Run: runRepoTransfer}) | |
| 57 | 58 | register(Command{Path: []string{"repo", "rename"}, |
| 58 | 59 | Summary: "rename a repository", |
| 59 | 60 | Usage: "repo rename <owner/name> <new-name> (clone URLs change)", |
| @@ -68,10 +69,11 @@ func init() { | ||
| 68 | 69 | Examples: []string{"repo delete cmc/scratch --yes"}, |
| 69 | 70 | Run: runRepoDelete}) |
| 70 | 71 | register(Command{Path: []string{"repo", "access", "grant"}, |
| 71 | Summary: "grant access", | |
| 72 | Usage: "repo access grant <owner/name> <user> read|write|admin", | |
| 73 | Examples: []string{"repo access grant krz/gitbay cmc write"}, | |
| 74 | Run: runAccessGrant}) | |
| 72 | NeedsRecentSignIn: true, | |
| 73 | Summary: "grant access", | |
| 74 | Usage: "repo access grant <owner/name> <user> read|write|admin", | |
| 75 | Examples: []string{"repo access grant krz/gitbay cmc write"}, | |
| 76 | Run: runAccessGrant}) | |
| 75 | 77 | register(Command{Path: []string{"repo", "access", "revoke"}, |
| 76 | 78 | Summary: "revoke access", |
| 77 | 79 | Usage: "repo access revoke <owner/name> <user>", |
internal/control/runnerrepo.go +1 −1
| @@ -23,7 +23,7 @@ func init() { | ||
| 23 | 23 | Usage: "repo runner add <owner/name> < key.pub", |
| 24 | 24 | Examples: []string{"repo runner add krz/gitbay < key.pub"}, |
| 25 | 25 | ReadsStdin: true, |
| 26 | MintsCredential: true, Run: runRepoRunnerAdd}) | |
| 26 | MintsCredential: true, NeedsRecentSignIn: true, Run: runRepoRunnerAdd}) | |
| 27 | 27 | register(Command{Path: []string{"repo", "runner", "list"}, |
| 28 | 28 | Summary: "list the runners attached to a repository", |
| 29 | 29 | Usage: "repo runner list <owner/name>", |
internal/control/sig.go +5 −4
| @@ -18,10 +18,11 @@ import ( | ||
| 18 | 18 | |
| 19 | 19 | func init() { |
| 20 | 20 | register(Command{Path: []string{"pgp", "add"}, |
| 21 | Summary: "register an OpenPGP public key (armored)", | |
| 22 | Usage: "pgp add < key.asc", | |
| 23 | Examples: []string{"pgp add < key.asc"}, | |
| 24 | ReadsStdin: true, Run: runPGPAdd}) | |
| 21 | NeedsRecentSignIn: true, | |
| 22 | Summary: "register an OpenPGP public key (armored)", | |
| 23 | Usage: "pgp add < key.asc", | |
| 24 | Examples: []string{"pgp add < key.asc"}, | |
| 25 | ReadsStdin: true, Run: runPGPAdd}) | |
| 25 | 26 | register(Command{Path: []string{"pgp", "list"}, |
| 26 | 27 | Summary: "list registered OpenPGP keys", |
| 27 | 28 | Usage: "pgp list", |
internal/control/teams.go +12 −9
| @@ -30,25 +30,28 @@ func init() { | ||
| 30 | 30 | Usage: "org team show <org> <team>", |
| 31 | 31 | Examples: []string{"org team show krz maintainers"}, ReadOnly: true, Run: runTeamShow}) |
| 32 | 32 | register(Command{Path: []string{"org", "team", "add"}, |
| 33 | Summary: "add org members to a team", | |
| 34 | Usage: "org team add <org> <team> <user>...", | |
| 35 | Examples: []string{"org team add krz maintainers cmc"}, Run: runTeamAdd}) | |
| 33 | NeedsRecentSignIn: true, | |
| 34 | Summary: "add org members to a team", | |
| 35 | Usage: "org team add <org> <team> <user>...", | |
| 36 | Examples: []string{"org team add krz maintainers cmc"}, Run: runTeamAdd}) | |
| 36 | 37 | register(Command{Path: []string{"org", "team", "remove"}, |
| 37 | 38 | Summary: "remove members from a team", |
| 38 | 39 | Usage: "org team remove <org> <team> <user>...", |
| 39 | 40 | Examples: []string{"org team remove krz maintainers cmc"}, Run: runTeamRemove}) |
| 40 | 41 | register(Command{Path: []string{"org", "team", "grant"}, |
| 41 | Summary: "grant a team a role on an org repo", | |
| 42 | Usage: "org team grant <org> <team> <owner/name> read|write|admin", | |
| 43 | Examples: []string{"org team grant krz maintainers krz/gitbay write"}, Run: runTeamGrant}) | |
| 42 | NeedsRecentSignIn: true, | |
| 43 | Summary: "grant a team a role on an org repo", | |
| 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 | 46 | register(Command{Path: []string{"org", "team", "revoke"}, |
| 45 | 47 | Summary: "revoke a team's grant", |
| 46 | 48 | Usage: "org team revoke <org> <team> <owner/name>", |
| 47 | 49 | Examples: []string{"org team revoke krz maintainers krz/gitbay"}, Run: runTeamRevoke}) |
| 48 | 50 | register(Command{Path: []string{"org", "settings", "members-role"}, |
| 49 | Summary: "role plain membership implies on every org repo", | |
| 50 | Usage: "org settings members-role <org> write|read|none (default write)", | |
| 51 | Examples: []string{"org settings members-role krz read"}, Run: runOrgMembersRole}) | |
| 51 | NeedsRecentSignIn: true, | |
| 52 | Summary: "role plain membership implies on every org repo", | |
| 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 | 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 | 22 | {"--ttl", "30d|720h", "how long the token is valid; an expiring token cannot mint credentials", "never expires"}, |
| 23 | 23 | }, |
| 24 | 24 | Examples: []string{"token create --name laptop --ttl 30d", "token create --name phone --scope full"}, |
| 25 | MintsCredential: true, | |
| 26 | Run: runTokenCreate}) | |
| 25 | MintsCredential: true, NeedsRecentSignIn: true, | |
| 26 | Run: runTokenCreate}) | |
| 27 | 27 | register(Command{Path: []string{"token", "list"}, |
| 28 | 28 | Summary: "list API tokens", |
| 29 | 29 | Usage: "token list", |
internal/control/web.go +2 −2
| @@ -16,8 +16,8 @@ func init() { | ||
| 16 | 16 | register(Command{Path: []string{"web", "login"}, |
| 17 | 17 | Summary: "mint a one-time browser login URL", |
| 18 | 18 | Usage: "web login", |
| 19 | MintsCredential: true, | |
| 20 | Examples: []string{"web login"}, Run: runWebLogin}) | |
| 19 | MintsCredential: true, NeedsRecentSignIn: true, | |
| 20 | Examples: []string{"web login"}, Run: runWebLogin}) | |
| 21 | 21 | register(Command{Path: []string{"web", "sessions", "list"}, |
| 22 | 22 | Summary: "list your browser sessions", |
| 23 | 23 | Usage: "web sessions list", |
internal/control/webhook.go +3 −2
| @@ -15,8 +15,9 @@ import ( | ||
| 15 | 15 | |
| 16 | 16 | func init() { |
| 17 | 17 | register(Command{Path: []string{"webhook", "add"}, |
| 18 | Summary: "add a webhook", | |
| 19 | Usage: "webhook add <owner/name> <url> [--secret -] [--events push,issue.created|*]", | |
| 18 | NeedsRecentSignIn: true, | |
| 19 | Summary: "add a webhook", | |
| 20 | Usage: "webhook add <owner/name> <url> [--secret -] [--events push,issue.created|*]", | |
| 20 | 21 | Flags: []Flag{ |
| 21 | 22 | {"--secret", "-", "read the secret that signs deliveries from stdin", ""}, |
| 22 | 23 | {"--events", "push,issue.created|*", "which events to send", "*"}, |
internal/httpd/account_test.go +2 −1
| @@ -7,6 +7,7 @@ import ( | ||
| 7 | 7 | "strconv" |
| 8 | 8 | "strings" |
| 9 | 9 | "testing" |
| 10 | "time" | |
| 10 | 11 | |
| 11 | 12 | "gitbay.org/gitbay/internal/config" |
| 12 | 13 | "gitbay.org/gitbay/internal/store" |
| @@ -300,7 +301,7 @@ func newTokenTestServer(t *testing.T) (*Server, *store.Store, store.User) { | ||
| 300 | 301 | if err != nil { |
| 301 | 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 | 307 | // The settings page lists a user's API tokens with scope and expiry, |