vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Unify account terminology and clarify ACL commands
2e024cc96a391c0959ad2914caeed78b600fcfc1 · Keenan Nemetz · 14d ago · parent ab326d3a
Unify account terminology and clarify ACL commands Use account consistently in the CLI, help, docs, and remote admin messages. Rename the CLI account command from user to account while retaining the old user spelling as an alias. Replace positional ACL mutations with explicit verbs such as set-role, protect add, runners remove, and checks enable. Update local and remote admin parsing, hints, docs, and tests. Tidy repository filter alignment and remove duplicated issue/change counts from page subtitles.
server/admin_test.go +60−28
| @@ -45,7 +45,7 @@ func (f *fixture) adminSSH(key string, stdin []byte, args ...string) (string, st | |||
| 45 | 45 | func TestRemoteAdminRequiresInstanceAdmin(t *testing.T) { | |
| 46 | 46 | t.Parallel() | |
| 47 | 47 | f := newFixture(t) | |
| 48 | out, errOut, err := f.adminSSH(f.keyPath, nil, "user", "list") | ||
| 48 | out, errOut, err := f.adminSSH(f.keyPath, nil, "account", "list") | ||
| 49 | 49 | if err == nil { | |
| 50 | 50 | t.Fatalf("non-admin used the admin API: %s%s", out, errOut) | |
| 51 | 51 | } | |
| @@ -54,61 +54,61 @@ func TestRemoteAdminRequiresInstanceAdmin(t *testing.T) { | |||
| 54 | 54 | } | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | func TestRemoteAdminManagesUsersAndRepos(t *testing.T) { | ||
| 57 | func TestRemoteAdminManagesAccountsAndRepos(t *testing.T) { | ||
| 58 | 58 | t.Parallel() | |
| 59 | 59 | f := newFixture(t) | |
| 60 | 60 | if _, err := f.srv.Identity.SetAdmin("keenan", true, "test"); err != nil { | |
| 61 | 61 | t.Fatal(err) | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | out, errOut, err := f.adminSSH(f.keyPath, nil, "user", "create", "alice") | ||
| 64 | out, errOut, err := f.adminSSH(f.keyPath, nil, "account", "create", "alice") | ||
| 65 | 65 | if err == nil { | |
| 66 | t.Fatalf("user create without a key or --no-key succeeded: %s%s", out, errOut) | ||
| 66 | t.Fatalf("account create without a key or --no-key succeeded: %s%s", out, errOut) | ||
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | // --no-key is the explicit passkey-only path; it must not wait for stdin. | |
| 70 | out, errOut, err = f.adminSSH(f.keyPath, nil, "user", "create", "bob", "--no-key") | ||
| 70 | out, errOut, err = f.adminSSH(f.keyPath, nil, "account", "create", "bob", "--no-key") | ||
| 71 | 71 | if err != nil { | |
| 72 | t.Fatalf("user create --no-key: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 72 | t.Fatalf("account create --no-key: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 73 | 73 | } | |
| 74 | 74 | if a, err := f.srv.Identity.Find("bob"); err != nil || len(a.Keys) != 0 { | |
| 75 | 75 | t.Fatalf("passkey-only account has keys: %+v, %v", a, err) | |
| 76 | 76 | } | |
| 77 | 77 | if !strings.Contains(out, "enrollment") { | |
| 78 | t.Fatalf("user create --no-key did not print an enrollment link: %s", out) | ||
| 78 | t.Fatalf("account create --no-key did not print an enrollment link: %s", out) | ||
| 79 | 79 | } | |
| 80 | out, errOut, err = f.adminSSH(f.keyPath, nil, "user", "role", "bob", "contribute") | ||
| 80 | out, errOut, err = f.adminSSH(f.keyPath, nil, "account", "role", "bob", "contribute") | ||
| 81 | 81 | if err != nil { | |
| 82 | t.Fatalf("user role: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 82 | t.Fatalf("account role: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 83 | 83 | } | |
| 84 | 84 | if a, err := f.srv.Identity.Find("bob"); err != nil || a.Role != access.Contribute { | |
| 85 | 85 | t.Fatalf("remote role did not land: %+v, %v", a, err) | |
| 86 | 86 | } | |
| 87 | 87 | ||
| 88 | 88 | pub, _ := keypair(t, filepath.Join(f.cfg.DataDir, "id_alice")) | |
| 89 | out, errOut, err = f.adminSSH(f.keyPath, []byte(pub), "user", "create", "alice") | ||
| 89 | out, errOut, err = f.adminSSH(f.keyPath, []byte(pub), "account", "create", "alice") | ||
| 90 | 90 | if err != nil { | |
| 91 | t.Fatalf("user create: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 91 | t.Fatalf("account create: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 92 | 92 | } | |
| 93 | 93 | if !strings.Contains(out, "alice") { | |
| 94 | t.Fatalf("user create did not name alice: %s", out) | ||
| 94 | t.Fatalf("account create did not name alice: %s", out) | ||
| 95 | 95 | } | |
| 96 | 96 | if a, err := f.srv.Identity.Find("alice"); err != nil || len(a.Keys) != 1 { | |
| 97 | t.Fatalf("user create did not attach the key: %+v, %v", a, err) | ||
| 97 | t.Fatalf("account create did not attach the key: %+v, %v", a, err) | ||
| 98 | 98 | } | |
| 99 | 99 | ||
| 100 | 100 | second, _ := keypair(t, filepath.Join(f.cfg.DataDir, "id_alice_second")) | |
| 101 | out, errOut, err = f.adminSSH(f.keyPath, []byte(second), "user", "add-key", "alice") | ||
| 101 | out, errOut, err = f.adminSSH(f.keyPath, []byte(second), "account", "add-key", "alice") | ||
| 102 | 102 | if err != nil { | |
| 103 | t.Fatalf("user add-key: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 103 | t.Fatalf("account add-key: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 104 | 104 | } | |
| 105 | 105 | if !strings.Contains(out, "added key") { | |
| 106 | 106 | t.Fatalf("add-key output = %q", out) | |
| 107 | 107 | } | |
| 108 | 108 | ||
| 109 | out, errOut, err = f.adminSSH(f.keyPath, nil, "--json", "user", "list") | ||
| 109 | out, errOut, err = f.adminSSH(f.keyPath, nil, "--json", "account", "list") | ||
| 110 | 110 | if err != nil { | |
| 111 | t.Fatalf("user list --json: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 111 | t.Fatalf("account list --json: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 112 | 112 | } | |
| 113 | 113 | var listed struct { | |
| 114 | 114 | OK bool `json:"ok"` | |
| @@ -117,7 +117,7 @@ func TestRemoteAdminManagesUsersAndRepos(t *testing.T) { | |||
| 117 | 117 | } `json:"data"` | |
| 118 | 118 | } | |
| 119 | 119 | if err := json.Unmarshal([]byte(out), &listed); err != nil { | |
| 120 | t.Fatalf("user list --json is not JSON: %v\n%s", err, out) | ||
| 120 | t.Fatalf("account list --json is not JSON: %v\n%s", err, out) | ||
| 121 | 121 | } | |
| 122 | 122 | found := false | |
| 123 | 123 | for _, u := range listed.Data { | |
| @@ -126,7 +126,13 @@ func TestRemoteAdminManagesUsersAndRepos(t *testing.T) { | |||
| 126 | 126 | } | |
| 127 | 127 | } | |
| 128 | 128 | if !listed.OK || !found { | |
| 129 | t.Fatalf("user list did not include alice: %s", out) | ||
| 129 | t.Fatalf("account list did not include alice: %s", out) | ||
| 130 | } | ||
| 131 | |||
| 132 | // Older clients still send the historical resource spelling. | ||
| 133 | out, errOut, err = f.adminSSH(f.keyPath, nil, "user", "list") | ||
| 134 | if err != nil || !strings.Contains(out, "alice") { | ||
| 135 | t.Fatalf("legacy user list: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 130 | 136 | } | |
| 131 | 137 | ||
| 132 | 138 | out, errOut, err = f.adminSSH(f.keyPath, nil, "repo", "create", "remote-demo", "--owner", "alice") | |
| @@ -137,9 +143,20 @@ func TestRemoteAdminManagesUsersAndRepos(t *testing.T) { | |||
| 137 | 143 | t.Fatalf("repository was not created: %v", err) | |
| 138 | 144 | } | |
| 139 | 145 | ||
| 140 | out, errOut, err = f.adminSSH(f.keyPath, nil, "acl", "default", "remote-demo", "read") | ||
| 146 | out, errOut, err = f.adminSSH(f.keyPath, nil, "acl", "set-default-role", "remote-demo", "read") | ||
| 141 | 147 | if err != nil { | |
| 142 | t.Fatalf("acl default: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 148 | t.Fatalf("acl set-default-role: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 149 | } | ||
| 150 | for _, args := range [][]string{ | ||
| 151 | {"acl", "set-role", "remote-demo", "bob", "contribute"}, | ||
| 152 | {"acl", "set-approvals", "remote-demo", "2"}, | ||
| 153 | {"acl", "checks", "disable", "remote-demo"}, | ||
| 154 | {"acl", "protect", "add", "remote-demo", "refs/heads/main"}, | ||
| 155 | {"acl", "runners", "add", "remote-demo", "bob"}, | ||
| 156 | } { | ||
| 157 | if out, errOut, err = f.adminSSH(f.keyPath, nil, args...); err != nil { | ||
| 158 | t.Fatalf("%v: %v\nstdout: %s\nstderr: %s", args, err, out, errOut) | ||
| 159 | } | ||
| 143 | 160 | } | |
| 144 | 161 | m, err := f.srv.Repos.Open("remote-demo") | |
| 145 | 162 | if err != nil { | |
| @@ -152,6 +169,21 @@ func TestRemoteAdminManagesUsersAndRepos(t *testing.T) { | |||
| 152 | 169 | if a.Default != "read" { | |
| 153 | 170 | t.Fatalf("default role = %q, want read", a.Default) | |
| 154 | 171 | } | |
| 172 | if a.Entries["bob"] != "contribute" { | ||
| 173 | t.Fatalf("bob role = %q, want contribute", a.Entries["bob"]) | ||
| 174 | } | ||
| 175 | if got := a.RequiredApprovals(); got != 2 { | ||
| 176 | t.Fatalf("approvals required = %d, want 2", got) | ||
| 177 | } | ||
| 178 | if a.RequiresChecks() { | ||
| 179 | t.Fatal("checks disable left checks required") | ||
| 180 | } | ||
| 181 | if got := strings.Join(a.Protected, ","); got != "refs/heads/main" { | ||
| 182 | t.Fatalf("protected = %q", got) | ||
| 183 | } | ||
| 184 | if got := strings.Join(a.Runners, ","); got != "bob" { | ||
| 185 | t.Fatalf("runners = %q", got) | ||
| 186 | } | ||
| 155 | 187 | ||
| 156 | 188 | out, errOut, err = f.adminSSH(f.keyPath, nil, "repo", "rename", "remote-demo", "renamed-demo") | |
| 157 | 189 | if err != nil { | |
| @@ -169,9 +201,9 @@ func TestRemoteAdminManagesUsersAndRepos(t *testing.T) { | |||
| 169 | 201 | t.Fatal("deleted repository still exists") | |
| 170 | 202 | } | |
| 171 | 203 | ||
| 172 | out, errOut, err = f.adminSSH(f.keyPath, nil, "user", "retire", "alice") | ||
| 204 | out, errOut, err = f.adminSSH(f.keyPath, nil, "account", "retire", "alice") | ||
| 173 | 205 | if err != nil { | |
| 174 | t.Fatalf("user retire: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 206 | t.Fatalf("account retire: %v\nstdout: %s\nstderr: %s", err, out, errOut) | ||
| 175 | 207 | } | |
| 176 | 208 | if a, err := f.srv.Identity.Find("alice"); err == nil && a.Active() { | |
| 177 | 209 | t.Fatal("retired account is still active") | |
| @@ -184,7 +216,7 @@ func TestAdminCLIForwardsToRemote(t *testing.T) { | |||
| 184 | 216 | if _, err := f.srv.Identity.SetAdmin("keenan", true, "test"); err != nil { | |
| 185 | 217 | t.Fatal(err) | |
| 186 | 218 | } | |
| 187 | cmd := exec.Command(binPath, "admin", "--remote", "ssh://git@"+f.sshAddr, "user", "list") | ||
| 219 | cmd := exec.Command(binPath, "admin", "--remote", "ssh://git@"+f.sshAddr, "account", "list") | ||
| 188 | 220 | cmd.Env = append(cmd.Environ(), | |
| 189 | 221 | "VOLE_SSH_COMMAND=ssh -i "+f.keyPath+" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR", | |
| 190 | 222 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", | |
| @@ -205,23 +237,23 @@ func TestAdminCLIForwardsToRemote(t *testing.T) { | |||
| 205 | 237 | if err := os.WriteFile(pubPath, []byte(pub), 0o600); err != nil { | |
| 206 | 238 | t.Fatal(err) | |
| 207 | 239 | } | |
| 208 | cmd = exec.Command(binPath, "admin", "--remote", "ssh://git@"+f.sshAddr, "user", "create", "alice", "--key", pubPath) | ||
| 240 | cmd = exec.Command(binPath, "admin", "--remote", "ssh://git@"+f.sshAddr, "account", "create", "alice", "--key", pubPath) | ||
| 209 | 241 | cmd.Env = append(cmd.Environ(), | |
| 210 | 242 | "VOLE_SSH_COMMAND=ssh -i "+f.keyPath+" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR", | |
| 211 | 243 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", | |
| 212 | 244 | ) | |
| 213 | 245 | if err := cmd.Run(); err != nil { | |
| 214 | t.Fatalf("remote user create: %v", err) | ||
| 246 | t.Fatalf("remote account create: %v", err) | ||
| 215 | 247 | } | |
| 216 | 248 | second, _ := keypair(t, filepath.Join(f.cfg.DataDir, "id_cli_alice_second")) | |
| 217 | cmd = exec.Command(binPath, "admin", "--remote", "ssh://git@"+f.sshAddr, "user", "add-key", "alice") | ||
| 249 | cmd = exec.Command(binPath, "admin", "--remote", "ssh://git@"+f.sshAddr, "account", "add-key", "alice") | ||
| 218 | 250 | cmd.Env = append(cmd.Environ(), | |
| 219 | 251 | "VOLE_SSH_COMMAND=ssh -i "+f.keyPath+" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR", | |
| 220 | 252 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", | |
| 221 | 253 | ) | |
| 222 | 254 | cmd.Stdin = strings.NewReader(second) | |
| 223 | 255 | if err := cmd.Run(); err != nil { | |
| 224 | t.Fatalf("remote user add-key: %v", err) | ||
| 256 | t.Fatalf("remote account add-key: %v", err) | ||
| 225 | 257 | } | |
| 226 | 258 | if a, err := f.srv.Identity.Find("alice"); err != nil || len(a.Keys) != 2 { | |
| 227 | 259 | t.Fatalf("alice's keys did not land: %+v, %v", a, err) | |