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.
Create accounts with a key and find the instance config
c36d507302c0f7fb24af6eba390c4121d67318c3 · Keenan Nemetz · 16d ago · parent 3abbe749
Create accounts with a key and find the instance config An account is now written with its first key in one transaction, so a name cannot be claimed without a credential. add-key remains for additional keys, and remote user create forwards the key on stdin. Local commands now resolve the config from --config, VOLE_CONFIG, ./vole.toml, or ~/.vole/vole.toml before falling back to defaults. init prints --config in its next steps when the instance will not be discovered automatically.
forge/identity/identity.go +62−17
| @@ -496,22 +496,33 @@ func (s *Store) mutate(action, actor, account, detail string, fn func(tx *sql.Tx | |||
| 496 | 496 | return nil | |
| 497 | 497 | } | |
| 498 | 498 | ||
| 499 | // Create adds an account with a fresh id and no keys. | ||
| 500 | func (s *Store) Create(name, actor string) (*Account, error) { | ||
| 499 | // Create adds an account with a fresh id and its first key. The account, the | ||
| 500 | // name reservation, and the key are written in one transaction, so a name is | ||
| 501 | // never claimed by an account that cannot authenticate. | ||
| 502 | func (s *Store) Create(name, authorizedKey, actor string) (*Account, error) { | ||
| 501 | 503 | if err := ValidateName(name); err != nil { | |
| 502 | 504 | return nil, err | |
| 503 | 505 | } | |
| 506 | key, err := parseKey(authorizedKey) | ||
| 507 | if err != nil { | ||
| 508 | return nil, err | ||
| 509 | } | ||
| 504 | 510 | now := time.Now().UTC() | |
| 505 | 511 | accountID, err := id.New(now) | |
| 506 | 512 | if err != nil { | |
| 507 | 513 | return nil, err | |
| 508 | 514 | } | |
| 509 | err = s.mutate("identity.create", actor, accountID, name, func(tx *sql.Tx) error { | ||
| 515 | err = s.mutate("identity.create", actor, accountID, name+" "+key.Fingerprint, func(tx *sql.Tx) error { | ||
| 510 | 516 | if owner, err := reservedBy(tx, name); err != nil { | |
| 511 | 517 | return err | |
| 512 | 518 | } else if owner != "" { | |
| 513 | 519 | return fmt.Errorf("identity: the name %q belongs to %s and cannot be reused; a retired or former name is never handed out twice", name, owner) | |
| 514 | 520 | } | |
| 521 | if owner, err := keyOwner(tx, key.Fingerprint); err != nil { | ||
| 522 | return err | ||
| 523 | } else if owner != "" { | ||
| 524 | return fmt.Errorf("identity: that key is already registered to %s", owner) | ||
| 525 | } | ||
| 515 | 526 | if _, err := tx.Exec(`INSERT INTO users(id, name, status, created) VALUES(?,?, 'active', ?)`, | |
| 516 | 527 | accountID, name, now.Unix()); err != nil { | |
| 517 | 528 | return fmt.Errorf("identity: create %s: %w", name, err) | |
| @@ -519,6 +530,10 @@ func (s *Store) Create(name, actor string) (*Account, error) { | |||
| 519 | 530 | if _, err := tx.Exec(`INSERT INTO user_names(name, user_id) VALUES(?,?)`, name, accountID); err != nil { | |
| 520 | 531 | return fmt.Errorf("identity: create %s: %w", name, err) | |
| 521 | 532 | } | |
| 533 | if _, err := tx.Exec(`INSERT INTO user_keys(fingerprint, user_id, type, line, comment) VALUES(?,?,?,?,?)`, | ||
| 534 | key.Fingerprint, accountID, key.Type, key.Line, key.Comment); err != nil { | ||
| 535 | return fmt.Errorf("identity: create %s: %w", name, err) | ||
| 536 | } | ||
| 522 | 537 | return nil | |
| 523 | 538 | }) | |
| 524 | 539 | if err != nil { | |
| @@ -598,32 +613,62 @@ func (s *Store) Retire(idOrName, actor string) (*Account, error) { | |||
| 598 | 613 | return s.Find(accountID) | |
| 599 | 614 | } | |
| 600 | 615 | ||
| 601 | // AddKey attaches an authorized_keys line to an account and returns its | ||
| 602 | // fingerprint. | ||
| 603 | func (s *Store) AddKey(idOrName, authorizedKey, actor string) (string, error) { | ||
| 616 | // parseKey validates one authorized_keys line and extracts what the roster stores. | ||
| 617 | func parseKey(authorizedKey string) (Key, error) { | ||
| 604 | 618 | line := strings.TrimSpace(authorizedKey) | |
| 619 | if line == "" { | ||
| 620 | return Key{}, errors.New("identity: no public key given") | ||
| 621 | } | ||
| 605 | 622 | pub, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(line)) | |
| 606 | 623 | if err != nil { | |
| 607 | return "", fmt.Errorf("identity: not an SSH public key: %w", err) | ||
| 624 | return Key{}, fmt.Errorf("identity: not an SSH public key: %w", err) | ||
| 625 | } | ||
| 626 | return Key{ | ||
| 627 | Fingerprint: ssh.FingerprintSHA256(pub), | ||
| 628 | Type: pub.Type(), | ||
| 629 | Line: line, | ||
| 630 | Comment: comment, | ||
| 631 | }, nil | ||
| 632 | } | ||
| 633 | |||
| 634 | // keyOwner returns the account name already holding a fingerprint, or "" when | ||
| 635 | // the key is unclaimed. | ||
| 636 | func keyOwner(tx *sql.Tx, fingerprint string) (string, error) { | ||
| 637 | var owner string | ||
| 638 | err := tx.QueryRow(`SELECT u.name FROM user_keys k JOIN users u ON u.id=k.user_id WHERE k.fingerprint=?`, fingerprint).Scan(&owner) | ||
| 639 | if errors.Is(err, sql.ErrNoRows) { | ||
| 640 | return "", nil | ||
| 641 | } | ||
| 642 | if err != nil { | ||
| 643 | return "", fmt.Errorf("identity: read keys: %w", err) | ||
| 644 | } | ||
| 645 | return owner, nil | ||
| 646 | } | ||
| 647 | |||
| 648 | // AddKey attaches an authorized_keys line to an account and returns its | ||
| 649 | // fingerprint. Creation requires a key; this is for adding another one. | ||
| 650 | func (s *Store) AddKey(idOrName, authorizedKey, actor string) (string, error) { | ||
| 651 | key, err := parseKey(authorizedKey) | ||
| 652 | if err != nil { | ||
| 653 | return "", err | ||
| 608 | 654 | } | |
| 609 | fp := ssh.FingerprintSHA256(pub) | ||
| 610 | err = s.mutate("identity.add-key", actor, idOrName, fp+" "+comment, func(tx *sql.Tx) error { | ||
| 655 | err = s.mutate("identity.add-key", actor, idOrName, key.Fingerprint+" "+key.Comment, func(tx *sql.Tx) error { | ||
| 611 | 656 | a, err := findTx(tx, idOrName) | |
| 612 | 657 | if err != nil { | |
| 613 | 658 | return err | |
| 614 | 659 | } | |
| 615 | var owner string | ||
| 616 | err = tx.QueryRow(`SELECT u.name FROM user_keys k JOIN users u ON u.id=k.user_id WHERE k.fingerprint=?`, fp).Scan(&owner) | ||
| 660 | owner, err := keyOwner(tx, key.Fingerprint) | ||
| 661 | if err != nil { | ||
| 662 | return err | ||
| 663 | } | ||
| 617 | 664 | switch { | |
| 618 | case err == nil && owner == a.Name: | ||
| 665 | case owner == a.Name: | ||
| 619 | 666 | return fmt.Errorf("identity: %s already has that key", a.Name) | |
| 620 | case err == nil: | ||
| 667 | case owner != "": | ||
| 621 | 668 | return fmt.Errorf("identity: that key is already registered to %s", owner) | |
| 622 | case !errors.Is(err, sql.ErrNoRows): | ||
| 623 | return fmt.Errorf("identity: read keys: %w", err) | ||
| 624 | 669 | } | |
| 625 | 670 | if _, err := tx.Exec(`INSERT INTO user_keys(fingerprint, user_id, type, line, comment) VALUES(?,?,?,?,?)`, | |
| 626 | fp, a.ID, pub.Type(), line, comment); err != nil { | ||
| 671 | key.Fingerprint, a.ID, key.Type, key.Line, key.Comment); err != nil { | ||
| 627 | 672 | return fmt.Errorf("identity: add key: %w", err) | |
| 628 | 673 | } | |
| 629 | 674 | return nil | |
| @@ -631,7 +676,7 @@ func (s *Store) AddKey(idOrName, authorizedKey, actor string) (string, error) { | |||
| 631 | 676 | if err != nil { | |
| 632 | 677 | return "", err | |
| 633 | 678 | } | |
| 634 | return fp, nil | ||
| 679 | return key.Fingerprint, nil | ||
| 635 | 680 | } | |
| 636 | 681 | ||
| 637 | 682 | // RemoveKey revokes one key. An exact fingerprint or a unique prefix is accepted. | |