Commit b79de56262
Verified · cmc
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. | ||
| 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 | // 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. |
| 74 | func (c *Ctx) usage() int { | 95 | func (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 | ||
| 110 | var registry []Command | 136 | var 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 | ||
| 17 | func init() { | 17 | func 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 @@ | |||
| 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 | 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 | ||
| 19 | func init() { | 19 | func 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 | ||
| 16 | func init() { | 16 | func 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, |