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.
Allow opt-in open registration
f920a9f12c4ff12e4f47497a084f0201d8914181 · Keenan Nemetz · 16d ago · parent 6c84d387
Allow opt-in open registration Add open_registration, off by default and effective only with an HTTPS public_url. /register creates an account and its first passkey without an administrator; the account row is written only after the WebAuthn ceremony succeeds, so an abandoned registration reserves no name. New accounts start with no repository-specific ACL entries. Add self-service SSH key management on the Account page so a registered contributor can add the key needed to push to their contributor namespace. Cover registration enablement, name validation, deferred account creation, and self-service key add/remove in tests, and document the policy.
forge/identity/passkey.go +28
| @@ -83,6 +83,34 @@ type Invite struct { | |||
| 83 | 83 | Expires time.Time | |
| 84 | 84 | } | |
| 85 | 85 | ||
| 86 | // CreatePasskey creates a self-registered account and its first passkey in one | ||
| 87 | // transaction. The account id is chosen by the caller because the WebAuthn user | ||
| 88 | // handle is fixed when the ceremony begins; the row is only written when the | ||
| 89 | // authenticator has actually produced a credential, so an abandoned registration | ||
| 90 | // reserves no name. | ||
| 91 | func (s *Store) CreatePasskey(name, accountID string, cred Credential, actor string) (*Account, error) { | ||
| 92 | if err := ValidateName(name); err != nil { | ||
| 93 | return nil, err | ||
| 94 | } | ||
| 95 | if !id.Valid(accountID) { | ||
| 96 | return nil, fmt.Errorf("identity: %q is not a valid account id", accountID) | ||
| 97 | } | ||
| 98 | if err := validateCredential(cred); err != nil { | ||
| 99 | return nil, err | ||
| 100 | } | ||
| 101 | now := time.Now().UTC() | ||
| 102 | err := s.mutate("identity.create", actor, accountID, name+" passkey", func(tx *sql.Tx) error { | ||
| 103 | if err := createAccount(tx, name, accountID, now, nil, nil); err != nil { | ||
| 104 | return err | ||
| 105 | } | ||
| 106 | return insertCredential(tx, accountID, cred, now) | ||
| 107 | }) | ||
| 108 | if err != nil { | ||
| 109 | return nil, err | ||
| 110 | } | ||
| 111 | return s.Find(accountID) | ||
| 112 | } | ||
| 113 | |||
| 86 | 114 | // CreateWithInvite creates an account with no SSH key and an enrollment invite | |
| 87 | 115 | // in the same transaction. This is how a browser-only account — somebody who | |
| 88 | 116 | // files issues but does not push — comes into existence. | |