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

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 3.2 KiB
1package client
2
3// Repository secrets over the instance's SSH verb.
4//
5// The values live in the instance's own store, not in a clone, so every question
6// about them is a question for the server: the CLI sends `vole-secret` and the
7// server checks the maintainer capability and records the read. The value of a
8// `set` travels on stdin, never in the command line, because the exec line is
9// visible to every process on both hosts and the instance records the command it
10// ran.
11
12import (
13 "fmt"
14 "strings"
15)
16
17// SetSecret stores a value and returns the server's one-line answer.
18func SetSecret(gitDir, remote, repoName, name string, value []byte) (string, error) {
19 host, port, verb, err := secretTarget(gitDir, remote, "set", repoName, name)
20 if err != nil {
21 return "", err
22 }
23 return runSSH(host, port, verb, strings.NewReader(string(value)))
24}
25
26// GetSecret returns a stored value exactly as it was written, with nothing
27// appended. A caller that redirects it to a file gets the bytes that were stored.
28func GetSecret(gitDir, remote, repoName, name string) (string, error) {
29 host, port, verb, err := secretTarget(gitDir, remote, "get", repoName, name)
30 if err != nil {
31 return "", err
32 }
33 return runSSH(host, port, verb, nil)
34}
35
36// ListSecrets returns the instance's rendering of a repository's secret names,
37// versions and read times. It never includes a value.
38func ListSecrets(gitDir, remote, repoName string) (string, error) {
39 host, port, verb, err := secretTarget(gitDir, remote, "list", repoName, "")
40 if err != nil {
41 return "", err
42 }
43 return runSSH(host, port, verb, nil)
44}
45
46// RemoveSecret forgets one value and returns the server's one-line answer.
47func RemoveSecret(gitDir, remote, repoName, name string) (string, error) {
48 host, port, verb, err := secretTarget(gitDir, remote, "rm", repoName, name)
49 if err != nil {
50 return "", err
51 }
52 return runSSH(host, port, verb, nil)
53}
54
55// secretTarget resolves the instance from the remote and names the repository on
56// it. The remote supplies the host and port; the repository is an argument, so a
57// maintainer of two repositories can manage either from one clone, and the server
58// checks the capability for the repository that was named.
59func secretTarget(gitDir, remote, op, repoName, name string) (host, port, verb string, err error) {
60 if remote == "" {
61 // A developer's clone already names the instance they push to, so the
62 // default is the remote they would push to as well.
63 remote = "origin"
64 }
65 host, port, _, err = SSHTarget(gitDir, remote)
66 if err != nil {
67 return "", "", "", err
68 }
69 if !validRepoArgument(repoName) {
70 return "", "", "", fmt.Errorf("%q is not a repository name", repoName)
71 }
72 args := []string{"vole-secret", op, "/" + repoName + ".git"}
73 if name != "" {
74 args = append(args, name)
75 }
76 quoted := make([]string, len(args))
77 for i, arg := range args {
78 quoted[i] = quoteSSHArg(arg)
79 }
80 return host, port, strings.Join(quoted, " "), nil
81}
82
83// validRepoArgument is the cheap shape check: the instance's own name rule is the
84// authority, and it reports the problem in the server's words.
85func validRepoArgument(name string) bool {
86 return name != "" && !strings.ContainsAny(name, "/\\ \t\n\x00") && !strings.Contains(name, "..")
87}
88