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.

Give a job a credential for the bot it acts as

6e1fca5115b709d31eeb51864b187d79aa104e5f · Keenan Nemetz · 6d ago · parent 446542e6

Give a job a credential for the bot it acts as

A workflow can name a bot account with `bot:`. When the job is leased the
instance generates a key for that run, stores the public half with the run,
repository and expiry, and sends the private half on the lease channel. The
runner writes it into the job's run directory with a known_hosts entry for the
instance, so a step's `git push` or `vole issue comment` authenticates as the
account and nothing has to be wired per job.

The account's role is the permission, in full. The credential replaces exactly one
requirement — that writing to the issues branch needs `maintain` — and adds no rule
of its own. What a job may write there is what its account may do, asked the same
way the browser asks: discuss, file an issue, manage an item it wrote, open a
change from a branch it may push, triage and approve when the role allows it. Those
are the ACL's own capabilities (`CanDiscuss`, `CanOpenRequest`, `CanTriage`,
`CanApprove`), so a maintainer's job may do everything a maintainer's key may do,
including labels, states, and other people's items.

A session is scoped only when the fingerprint it authenticated with is a live
credential row; an expired one reads as absent, so the push falls back to the
account's own role and is refused where a contribute account cannot write.

Two things end the key: the run finishing, which drops the row, and the expiry,
which is what stops a credential whose runner was killed and never reported. The
key is also masked in the log like a secret, since a step that cats it should not
leave it in the attempt.

Closes #165.

server/hook/hook.go +38−3

@@ -26,8 +26,12 @@ import (
26 26 )
27 27
28 28 type Config struct {
29 RepoDir string
30 Actor string
29 RepoDir string
30 Actor string
31 // KeyID is the fingerprint the session authenticated with. It is not the account:
32 // it is what tells a run credential apart from the account's own key, because a
33 // credential may write less than the account it acts as.
34 KeyID string
31 35 MaxBlobBytes int64
32 36 // UsersDB, when set, is the roster database, read here for access rules and
33 37 // file locks. Attribution itself is the Actor name resolved at the handshake.
@@ -148,6 +152,31 @@ func PreReceive(cfg Config) int {
148 152 return 1
149 153 }
150 154
155 // A session opened with a run credential acts for one job, in one repository,
156 // until that job ends. The role is still the account's; the credential only
157 // changes what a push to the issues branch may contain, because writing there
158 // otherwise requires maintain — a grant that is about vouching for everything,
159 // which a job cannot do.
160 var cred *runs.RunCredential
161 if keyID := strings.TrimSpace(cfg.KeyID); keyID != "" {
162 found, err := lookupRunCredential(cfg, keyID)
163 if err != nil {
164 reject(cfg.Out, "cannot read this job's credential: %v", err)
165 return 1
166 }
167 if found != nil {
168 switch {
169 case found.Account != actor:
170 reject(cfg.Out, "this job's credential is for %s, not %s", found.Account, actor)
171 return 1
172 case found.Repo != m.Name:
173 reject(cfg.Out, "this job's credential is for %s, not %s", found.Repo, m.Name)
174 return 1
175 }
176 cred = found
177 }
178 }
179
151 180 changes, err := readChanges(cfg.In)
152 181 if err != nil {
153 182 reject(cfg.Out, "cannot read ref updates: %v", err)
@@ -189,7 +218,8 @@ func PreReceive(cfg Config) int {
189 218 // else — metadata, shared branches, tags — belongs to write or admin
190 219 // access. The check is here rather than at the role gate because the gate
191 220 // only knows that the account may push something.
192 if !role.AtLeast(access.Maintain) && !access.IsContributorRef(actor, c.ref) {
221 if !role.AtLeast(access.Maintain) && !access.IsContributorRef(actor, c.ref) &&
222 !(cred != nil && c.ref == metadata.Ref) {
193 223 problems = append(problems, fmt.Sprintf(
194 224 "%s: the contribute role may only push branches under %s (role: %s)",
195 225 c.ref, access.ContributorBranchPrefix(actor), role))
@@ -399,6 +429,11 @@ func PreReceive(cfg Config) int {
399 429 for _, prob := range commentOwnership(g, c, actor, role, cfg.AuditPath, cfg.Out, m.Name) {
400 430 problems = append(problems, prob)
401 431 }
432 // A job acting with a run credential may write to the issues branch with the
433 // capabilities its account has, and nothing more.
434 if cred != nil {
435 problems = append(problems, scopedMetadata(g, c, a, actor)...)
436 }
402 437 }
403 438 }
404 439