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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add instance-wide roles and refine access UI

5b814e9958090cfd1fa826efb2d375bf613547c1 · Keenan Nemetz · 14d ago · parent 9579c1f9

Add instance-wide roles and refine access UI

Instance roles now live on accounts, apply to public repositories, and the Admin role is the unified instance-admin permission with access to every repository. Repository ACLs can raise an instance role but not lower it.

Adds role assignment to the users UI and CLI, open-registration role config, a reusable role picker, and separates adding a repository account from saving access rules.

server/admin_users_test.go +18−7

@@ -4,6 +4,8 @@ import (
4 4 "net/url"
5 5 "strings"
6 6 "testing"
7
8 "github.com/rodent-software/vole/forge/access"
7 9 )
8 10
9 11 func TestAdminUsersLifecycle(t *testing.T) {
@@ -26,7 +28,7 @@ func TestAdminUsersLifecycle(t *testing.T) {
26 28 }
27 29
28 30 create := url.Values{
29 "csrf": {sess.CSRF}, "name": {"newbie"}, "display": {"New Bie"},
31 "csrf": {sess.CSRF}, "name": {"newbie"}, "display": {"New Bie"}, "role": {"contribute"},
30 32 }
31 33 resp, body = f.browserPost(t, "/users", cookie, create)
32 34 if resp.StatusCode != 200 || !strings.Contains(body, "Enrollment link") || !strings.Contains(body, "newbie") {
@@ -49,7 +51,7 @@ func TestAdminUsersLifecycle(t *testing.T) {
49 51 if err != nil {
50 52 t.Fatal(err)
51 53 }
52 if len(acct.Keys) != 0 || !acct.Active() || acct.Admin {
54 if len(acct.Keys) != 0 || !acct.Active() || acct.Admin || acct.Role != access.Contribute {
53 55 t.Fatalf("created account = %+v", acct)
54 56 }
55 57
@@ -65,14 +67,23 @@ func TestAdminUsersLifecycle(t *testing.T) {
65 67 t.Fatalf("account page = %d:\n%s", resp.StatusCode, body)
66 68 }
67 69
68 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/admin", cookie, url.Values{
69 "csrf": {sess.CSRF}, "admin": {"true"},
70 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/role", cookie, url.Values{
71 "csrf": {sess.CSRF}, "role": {"maintain"},
72 })
73 if resp.StatusCode != 303 {
74 t.Fatalf("set role = %d", resp.StatusCode)
75 }
76 if acct, err = f.srv.Identity.Find(acct.Name); err != nil || acct.Role != access.Maintain {
77 t.Fatalf("role after set = %+v, %v", acct, err)
78 }
79 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/role", cookie, url.Values{
80 "csrf": {sess.CSRF}, "role": {"admin"},
70 81 })
71 82 if resp.StatusCode != 303 || !f.srv.Identity.IsAdmin(acct.Name) {
72 83 t.Fatalf("grant admin = %d, admin=%t", resp.StatusCode, f.srv.Identity.IsAdmin(acct.Name))
73 84 }
74 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/admin", cookie, url.Values{
75 "csrf": {sess.CSRF}, "admin": {"false"},
85 resp, _ = f.browserPost(t, "/users/"+acct.Name+"/role", cookie, url.Values{
86 "csrf": {sess.CSRF}, "role": {"none"},
76 87 })
77 88 if resp.StatusCode != 303 || f.srv.Identity.IsAdmin(acct.Name) {
78 89 t.Fatalf("revoke admin = %d, admin=%t", resp.StatusCode, f.srv.Identity.IsAdmin(acct.Name))
@@ -142,7 +153,7 @@ func TestAdminUsersLifecycle(t *testing.T) {
142 153 }
143 154
144 155 // CSRF is checked before any mutation.
145 if resp, _ := f.browserPost(t, "/users/"+acct.Name+"/admin", cookie, url.Values{"admin": {"true"}}); resp.StatusCode != 403 {
156 if resp, _ := f.browserPost(t, "/users/"+acct.Name+"/role", cookie, url.Values{"role": {"admin"}}); resp.StatusCode != 403 {
146 157 t.Fatalf("missing CSRF = %d, want 403", resp.StatusCode)
147 158 }
148 159 }