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.

Identify accounts by their registered name

62576fa8157d431dcf2d360f3ebe481de77bda03 · Keenan Nemetz · 14d ago · parent 642e5994

Identify accounts by their registered name

Drop the internal account id and display name. Users, keys, passkeys,
invites, sessions, ACL entries, approvals, locks and CI records now all
use the registered name as the single identity.

The roster migrates in place: old id-keyed tables are rebuilt around the
name, and access-rule/lock references are rewritten through the same
mapping. Display-name editing and whoami --id are removed.

client/remote.go +25−40

@@ -94,18 +94,15 @@ func RunSSH(host, port, verb string) (string, error) {
94 94 return out.String(), nil
95 95 }
96 96
97 // Roster is the client's read of the instance's user roster.
98 //
99 // The roster is a server-side SQLite database, so the client asks the instance
100 // for the one view it needs — active account id and name — over the same
101 // key-authenticated SSH channel it already pushes with (`ssh host vole-roster`).
102 // No new endpoint, no second credential, and nothing the client can write.
97 // Roster is the client's read of the instance's user roster: the active account
98 // names. The roster is a server-side SQLite database, so the client asks the
99 // instance for the one view it needs over the same key-authenticated SSH channel
100 // it already pushes with (`ssh host vole-roster`).
103 101 type Roster struct {
104 byName map[string]string // active name -> id
105 byID map[string]string // id -> current name
102 names map[string]bool
106 103 }
107 104
108 // LoadRoster asks the instance for its roster and indexes it both ways.
105 // LoadRoster asks the instance for its roster and indexes the names.
109 106 func LoadRoster(gitDir, remote string) (*Roster, error) {
110 107 host, port, _, err := SSHTarget(gitDir, remote)
111 108 if err != nil {
@@ -115,47 +112,35 @@ func LoadRoster(gitDir, remote string) (*Roster, error) {
115 112 if err != nil {
116 113 return nil, fmt.Errorf("cannot read the instance roster from %s: %w", host, err)
117 114 }
118 r := &Roster{byName: map[string]string{}, byID: map[string]string{}}
115 r := &Roster{names: map[string]bool{}}
119 116 for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
117 line = strings.TrimSpace(line)
120 118 if line == "" {
121 119 continue
122 120 }
123 f := strings.SplitN(line, "\t", 2)
124 if len(f) != 2 || f[0] == "" || f[1] == "" {
125 return nil, fmt.Errorf("roster: cannot parse %q", line)
121 // A roster row is one name. Taking the last field also accepts a line
122 // from an instance that still prints two columns.
123 f := strings.Fields(line)
124 if len(f) == 0 {
125 continue
126 126 }
127 r.byName[f[1]] = f[0]
128 r.byID[f[0]] = f[1]
127 r.names[f[len(f)-1]] = true
129 128 }
130 129 return r, nil
131 130 }
132 131
133 // IDFor resolves a name or a full id to an account id.
134 func (r *Roster) IDFor(nameOrID string) (string, error) {
135 if id, ok := r.byName[nameOrID]; ok {
136 return id, nil
137 }
138 if _, ok := r.byID[nameOrID]; ok {
139 return nameOrID, nil
140 }
141 return "", fmt.Errorf("no active account %q in the instance roster (see `vole admin user list` on the server)", nameOrID)
142 }
143
144 // NameFor renders an account id as a name when the roster knows it.
145 func (r *Roster) NameFor(accountID string) string {
146 if n, ok := r.byID[accountID]; ok {
147 return n
148 }
149 return accountID
132 // Has reports whether name is an active account.
133 func (r *Roster) Has(name string) bool {
134 return r != nil && r.names[name]
150 135 }
151 136
152 // Actor is who a write will be attributed to. The id and URL come from the
137 // Actor is who a write will be attributed to. The name and URL come from the
153 138 // instance; Repo comes from the remote the command was given.
154 139 type Actor struct {
155 ID, Name, URL, Repo string
140 Name, URL, Repo string
156 141 // Guessed means the instance could not be asked and the name came from git
157 142 // config. It is never silent: the caller warns, and commands that need an
158 // account id (`vole change new`) refuse instead of writing a random name.
143 // active account (`vole change new`) refuse instead of writing a random name.
159 144 Guessed bool
160 145 }
161 146
@@ -172,13 +157,13 @@ func AskIdentity(gitDir, remote string) (*Actor, error) {
172 157 if err != nil {
173 158 return nil, fmt.Errorf("cannot ask %s who you are: %w", host, err)
174 159 }
175 f := strings.SplitN(strings.TrimSpace(out), "\t", 3)
176 if len(f) < 2 || f[0] == "" || f[1] == "" {
160 f := strings.SplitN(strings.TrimSpace(out), "\t", 2)
161 if len(f) < 1 || f[0] == "" {
177 162 return nil, fmt.Errorf("whoami: cannot parse %q", strings.TrimSpace(out))
178 163 }
179 a := &Actor{ID: f[0], Name: f[1], Repo: RepoNameFromPath(path)}
180 if len(f) == 3 {
181 a.URL = strings.TrimRight(f[2], "/")
164 a := &Actor{Name: f[0], Repo: RepoNameFromPath(path)}
165 if len(f) == 2 {
166 a.URL = strings.TrimRight(f[1], "/")
182 167 }
183 168 return a, nil
184 169 }