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.

Fold admin work into repository settings and a Users page

9579c1f976254fdeb2ec73d4016db5b25c7921c1 · Keenan Nemetz · 14d ago · parent 381d6aac

Fold admin work into repository settings and a Users page

Remove the combined /admin tabs. Per-repository access rules now live on the repository's Settings page, visible to instance admins while repository details and the danger zone stay with repository admins. User management moves to /users, linked from the top navigation only for instance admins, and repository creation moves to /repos/new. Align the invite, danger-zone, and settings forms with the shared card-and-actions layout, and make settings section headings consistent.

server/admin_users_test.go +20−24

@@ -14,25 +14,21 @@ func TestAdminUsersLifecycle(t *testing.T) {
14 14 }
15 15 sess, cookie := f.browserSession(t, f.ownerID)
16 16
17 resp, body := f.browserGet(t, "/admin/users", cookie)
17 resp, body := f.browserGet(t, "/users", cookie)
18 18 if resp.StatusCode != 200 || !strings.Contains(body, "Create an account") || !strings.Contains(body, "keenan") {
19 19 t.Fatalf("users page = %d:\n%s", resp.StatusCode, body)
20 20 }
21 if !strings.Contains(body, `class="admin-tabs"`) || !strings.Contains(body, `href="/admin/users" aria-current="page"`) {
22 t.Fatalf("users page is missing the active admin tab:\n%s", body)
21 if !strings.Contains(body, `href="/users" aria-current="page"`) {
22 t.Fatalf("users page is missing the active top-nav link:\n%s", body)
23 23 }
24 resp, _ = f.browserGet(t, "/admin", cookie)
25 if resp.StatusCode != 200 {
26 t.Fatalf("admin page = %d", resp.StatusCode)
27 }
28 if _, body := f.browserGet(t, "/admin", cookie); !strings.Contains(body, "/admin/users") {
29 t.Fatal("admin page does not link to users")
24 if _, home := f.browserGet(t, "/", cookie); !strings.Contains(home, `href="/users"`) {
25 t.Fatal("the top navigation does not link to Users for an instance admin")
30 26 }
31 27
32 28 create := url.Values{
33 29 "csrf": {sess.CSRF}, "name": {"newbie"}, "display": {"New Bie"},
34 30 }
35 resp, body = f.browserPost(t, "/admin/users", cookie, create)
31 resp, body = f.browserPost(t, "/users", cookie, create)
36 32 if resp.StatusCode != 200 || !strings.Contains(body, "Enrollment link") || !strings.Contains(body, "newbie") {
37 33 t.Fatalf("create user = %d:\n%s", resp.StatusCode, body)
38 34 }
@@ -60,29 +56,29 @@ func TestAdminUsersLifecycle(t *testing.T) {
60 56 form := url.Values{
61 57 "csrf": {sess.CSRF}, "name": {"newbie"}, "display": {""},
62 58 }
63 if resp, page := f.browserPost(t, "/admin/users", cookie, form); resp.StatusCode != 400 || !strings.Contains(page, "belongs") {
59 if resp, page := f.browserPost(t, "/users", cookie, form); resp.StatusCode != 400 || !strings.Contains(page, "belongs") {
64 60 t.Fatalf("duplicate name = %d:\n%s", resp.StatusCode, page)
65 61 }
66 62
67 resp, body = f.browserGet(t, "/admin/users/"+acct.Name, cookie)
63 resp, body = f.browserGet(t, "/users/"+acct.Name, cookie)
68 64 if resp.StatusCode != 200 || !strings.Contains(body, "No SSH keys") || !strings.Contains(body, "No browser sessions") {
69 65 t.Fatalf("account page = %d:\n%s", resp.StatusCode, body)
70 66 }
71 67
72 resp, _ = f.browserPost(t, "/admin/users/"+acct.Name+"/admin", cookie, url.Values{
68 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/admin", cookie, url.Values{
73 69 "csrf": {sess.CSRF}, "admin": {"true"},
74 70 })
75 71 if resp.StatusCode != 303 || !f.srv.Identity.IsAdmin(acct.Name) {
76 72 t.Fatalf("grant admin = %d, admin=%t", resp.StatusCode, f.srv.Identity.IsAdmin(acct.Name))
77 73 }
78 resp, _ = f.browserPost(t, "/admin/users/"+acct.Name+"/admin", cookie, url.Values{
74 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/admin", cookie, url.Values{
79 75 "csrf": {sess.CSRF}, "admin": {"false"},
80 76 })
81 77 if resp.StatusCode != 303 || f.srv.Identity.IsAdmin(acct.Name) {
82 78 t.Fatalf("revoke admin = %d, admin=%t", resp.StatusCode, f.srv.Identity.IsAdmin(acct.Name))
83 79 }
84 80
85 resp, body = f.browserPost(t, "/admin/users/"+acct.Name+"/invite", cookie, url.Values{"csrf": {sess.CSRF}})
81 resp, body = f.browserPost(t, "/users/"+acct.Name+"/invite", cookie, url.Values{"csrf": {sess.CSRF}})
86 82 if resp.StatusCode != 200 || !strings.Contains(body, "Enrollment link") {
87 83 t.Fatalf("refresh invite = %d:\n%s", resp.StatusCode, body)
88 84 }
@@ -100,18 +96,18 @@ func TestAdminUsersLifecycle(t *testing.T) {
100 96 if _, err := f.srv.Sessions.Create(writer.Name, nil, "test-browser"); err != nil {
101 97 t.Fatal(err)
102 98 }
103 resp, body = f.browserGet(t, "/admin/users/"+writer.Name, cookie)
99 resp, body = f.browserGet(t, "/users/"+writer.Name, cookie)
104 100 if resp.StatusCode != 200 || !strings.Contains(body, "ssh-ed25519") || !strings.Contains(body, "test-browser") {
105 101 t.Fatalf("account detail with key and session = %d:\n%s", resp.StatusCode, body)
106 102 }
107 resp, _ = f.browserPost(t, "/admin/users/"+writer.Name+"/sessions/revoke", cookie, url.Values{"csrf": {sess.CSRF}})
103 resp, _ = f.browserPost(t, "/users/"+writer.Name+"/sessions/revoke", cookie, url.Values{"csrf": {sess.CSRF}})
108 104 if resp.StatusCode != 303 {
109 105 t.Fatalf("revoke sessions = %d", resp.StatusCode)
110 106 }
111 107 if list, err := f.srv.Sessions.List(writer.Name); err != nil || len(list) != 0 {
112 108 t.Fatalf("sessions after revoke = %d, %v", len(list), err)
113 109 }
114 resp, _ = f.browserPost(t, "/admin/users/"+writer.Name+"/keys/remove", cookie, url.Values{
110 resp, _ = f.browserPost(t, "/users/"+writer.Name+"/keys/remove", cookie, url.Values{
115 111 "csrf": {sess.CSRF}, "fingerprint": {writer.Keys[0].Fingerprint},
116 112 })
117 113 if resp.StatusCode != 303 {
@@ -125,16 +121,16 @@ func TestAdminUsersLifecycle(t *testing.T) {
125 121 t.Fatalf("writer still has %d key(s)", len(writer.Keys))
126 122 }
127 123
128 resp, body = f.browserPost(t, "/admin/users/"+writer.Name+"/retire", cookie, url.Values{
124 resp, body = f.browserPost(t, "/users/"+writer.Name+"/retire", cookie, url.Values{
129 125 "csrf": {sess.CSRF}, "confirm": {"wrong"},
130 126 })
131 127 if resp.StatusCode != 400 || !strings.Contains(body, "Type the account name") {
132 128 t.Fatalf("retire without confirmation = %d:\n%s", resp.StatusCode, body)
133 129 }
134 resp, _ = f.browserPost(t, "/admin/users/"+writer.Name+"/retire", cookie, url.Values{
130 resp, _ = f.browserPost(t, "/users/"+writer.Name+"/retire", cookie, url.Values{
135 131 "csrf": {sess.CSRF}, "confirm": {"wilbur"},
136 132 })
137 if resp.StatusCode != 303 || resp.Header.Get("Location") != "/admin/users" {
133 if resp.StatusCode != 303 || resp.Header.Get("Location") != "/users" {
138 134 t.Fatalf("retire = %d %q", resp.StatusCode, resp.Header.Get("Location"))
139 135 }
140 136 writer, err = f.srv.Identity.Find(writer.Name)
@@ -146,7 +142,7 @@ func TestAdminUsersLifecycle(t *testing.T) {
146 142 }
147 143
148 144 // CSRF is checked before any mutation.
149 if resp, _ := f.browserPost(t, "/admin/users/"+acct.Name+"/admin", cookie, url.Values{"admin": {"true"}}); resp.StatusCode != 403 {
145 if resp, _ := f.browserPost(t, "/users/"+acct.Name+"/admin", cookie, url.Values{"admin": {"true"}}); resp.StatusCode != 403 {
150 146 t.Fatalf("missing CSRF = %d, want 403", resp.StatusCode)
151 147 }
152 148 }
@@ -154,12 +150,12 @@ func TestAdminUsersLifecycle(t *testing.T) {
154 150 func TestAdminUsersRequiresInstanceAdmin(t *testing.T) {
155 151 t.Parallel()
156 152 f := browserFixture(t)
157 if resp, _ := f.browserGet(t, "/admin/users", ""); resp.StatusCode != 303 {
153 if resp, _ := f.browserGet(t, "/users", ""); resp.StatusCode != 303 {
158 154 t.Fatalf("anonymous users page = %d, want login redirect", resp.StatusCode)
159 155 }
160 156 f.addWriter(t)
161 157 _, writerCookie := f.browserSession(t, f.writerID)
162 if resp, _ := f.browserGet(t, "/admin/users", writerCookie); resp.StatusCode != 404 {
158 if resp, _ := f.browserGet(t, "/users", writerCookie); resp.StatusCode != 404 {
163 159 t.Fatalf("non-admin users page = %d, want 404", resp.StatusCode)
164 160 }
165 161 }