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.

forge/identity/passkey.go +19−4

@@ -25,6 +25,8 @@ import (
25 25 "sort"
26 26 "strings"
27 27 "time"
28
29 "github.com/rodent-software/vole/forge/access"
28 30 )
29 31
30 32 const (
@@ -86,6 +88,12 @@ type Invite struct {
86 88 // written when the authenticator has actually produced a credential, so an
87 89 // abandoned registration reserves no name.
88 90 func (s *Store) CreatePasskey(name string, cred Credential, actor string) (*Account, error) {
91 return s.CreatePasskeyWithRole(name, access.None, cred, actor)
92 }
93
94 // CreatePasskeyWithRole is self-registration with an instance-wide role. The
95 // role is chosen by instance policy, never by the person registering.
96 func (s *Store) CreatePasskeyWithRole(name string, role access.Role, cred Credential, actor string) (*Account, error) {
89 97 if err := ValidateName(name); err != nil {
90 98 return nil, err
91 99 }
@@ -93,8 +101,8 @@ func (s *Store) CreatePasskey(name string, cred Credential, actor string) (*Acco
93 101 return nil, err
94 102 }
95 103 now := time.Now().UTC()
96 err := s.mutate("identity.create", actor, name, name+" passkey", func(tx *sql.Tx) error {
97 if err := createAccount(tx, name, now, nil, nil); err != nil {
104 err := s.mutate("identity.create", actor, name, name+" passkey role="+role.String(), func(tx *sql.Tx) error {
105 if err := createAccount(tx, name, role, now, nil, nil); err != nil {
98 106 return err
99 107 }
100 108 return insertCredential(tx, name, cred, now)
@@ -109,6 +117,13 @@ func (s *Store) CreatePasskey(name string, cred Credential, actor string) (*Acco
109 117 // in the same transaction. This is how a browser-only account — somebody who
110 118 // files issues but does not push — comes into existence.
111 119 func (s *Store) CreateWithInvite(name, actor string, ttl time.Duration) (*Account, *Invite, error) {
120 return s.CreateWithInviteRole(name, access.None, actor, ttl)
121 }
122
123 // CreateWithInviteRole creates an account with an instance-wide role and an
124 // enrollment invite in the same transaction. This is how an invited
125 // contributor comes into existence without being added to every repository.
126 func (s *Store) CreateWithInviteRole(name string, role access.Role, actor string, ttl time.Duration) (*Account, *Invite, error) {
112 127 if err := ValidateName(name); err != nil {
113 128 return nil, nil, err
114 129 }
@@ -119,8 +134,8 @@ func (s *Store) CreateWithInvite(name, actor string, ttl time.Duration) (*Accoun
119 134 }
120 135 inv := &Invite{Token: token, Expires: now.Add(inviteTTL(ttl))}
121 136 pending := &pendingInvite{tokenHash: tokenHash, expires: inv.Expires, createdBy: actor}
122 err = s.mutate("identity.create", actor, name, name+" invite", func(tx *sql.Tx) error {
123 return createAccount(tx, name, now, nil, pending)
137 err = s.mutate("identity.create", actor, name, name+" invite role="+role.String(), func(tx *sql.Tx) error {
138 return createAccount(tx, name, role, now, nil, pending)
124 139 })
125 140 if err != nil {
126 141 return nil, nil, err