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 | |