Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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)