package xfer
import "log"
// Claim is the forge's answer to a request to hold a project code.
type Claim int
const (
// ClaimOK: the repository now holds the code.
ClaimOK Claim = iota
// ClaimTaken: another repository holds it.
ClaimTaken
// ClaimUnsure: it could not be shown that none does. Refused.
ClaimUnsure
)
// Adoption is what the front needs from the forge to let an empty
// repository take the project code of the first project pushed into it.
//
// A store `zit init` made has a project code of its own, and a sync
// login is signed with SHA1(projectCode/user/token), so a push from a
// repository with any other code never verified: the forge derived the
// expected secret from the store's code and the client signed with its
// own. With `zit serve --adopt-project` the child accepts the first push
// into an EMPTY store whatever its code; this lets the login through to
// it, and only that far.
//
// Nothing here is asked until a login has verified, with the token, under
// the code the card names, and the account holds the check-in letter: a
// caller who proved nothing starts no work, so a repository that does not
// exist, one that is private and one that is empty are asked about
// identically, which is not at all.
type Adoption struct {
// Code is the project code the forge holds for the repository, the
// one its logins are derived from. A lookup in memory.
Code func(repo string) (string, bool)
// Empty reports whether the repository holds no check-in, from the
// forge's own index and without running anything. False when unsure.
// A lagging index can only say "empty" too long, and zit refuses the
// push itself when the store is not.
Empty func(repo string) bool
// Recorded is the project code kept in the repository's row, "" if
// none: what a claim leaves there until the store has answered.
Recorded func(repo string) string
// Claim gives repo the code if no other repository has it, in one
// step. It fails closed.
Claim func(repo, code string) Claim
// Adopted reads the store's own project code and makes every cache
// of it, and the repository's row, say so; a claim that did not come
// true is given back by it. It reports whether it succeeded, and
// retries by itself when it did not.
Adopted func(repo string) bool
}
// SetAdoption turns first-push adoption on, which also makes every
// child start with --adopt-project: pass it only when the zit on PATH
// takes that option (zitcli.AdoptProjectVersion), as an older one
// rejects it and would not start.
func (f *Fronter) SetAdoption(a *Adoption) { f.adopt = a }
// AdoptAuth is an Auth that can derive a repository's credentials as if
// its project code were another one. Without it adoption is off.
type AdoptAuth interface {
SecretsAt(repo, user, code string) []Credential
}
// What a caller who has proved who they are is told when their push
// cannot be adopted. Only ever said after a login verified with the
// token under the card's code and the account holds the check-in letter.
const (
adoptTakenSentence = "not accepted: this project is already hosted on this forge, so it cannot also be pushed into a new repository; clone or mirror the existing one"
adoptUnsureSentence = "not accepted: this forge cannot yet tell whether this project is already hosted here; try again shortly"
)
// ValidProjectCode is validProjectCode, for tests outside the package.
func ValidProjectCode(code string) bool { return validProjectCode(code) }
// validProjectCode is the shape zit adopts: a full artifact hash, 40
// (SHA-1) or 64 (SHA-3) lowercase hex digits. Anything else is never
// verified against, so an empty, short or odd code derives nothing.
func validProjectCode(code string) bool {
if len(code) != 40 && len(code) != 64 {
return false
}
for _, c := range code {
if !('0' <= c && c <= '9' || 'a' <= c && c <= 'f') {
return false
}
}
return true
}
// cardCode is the project code the request's login may ALSO be verified
// against, or "": the request pushes, its push cards agree on one
// well-formed code, and it is not the code the forge holds. Strings
// only: no lookup beyond memory, no process, nothing that depends on
// whether the caller is anybody.
func (f *Fronter) cardCode(name string, pr pushRequest) string {
a := f.adopt
if a == nil || f.auth == nil || !pr.hasPush || pr.codeMixed || !validProjectCode(pr.projectCode) {
return ""
}
if _, ok := f.auth.(AdoptAuth); !ok {
return ""
}
if held, ok := a.Code(name); ok && held == pr.projectCode {
return ""
}
return pr.projectCode
}
// cardSecrets is the credentials the card's code derives, or none.
func (f *Fronter) cardSecrets(repo, user, code string) []Credential {
if code == "" {
return nil
}
if aa, ok := f.auth.(AdoptAuth); ok {
return aa.SecretsAt(repo, user, code)
}
return nil
}
// adoption is the verdict on a push whose login verified under the
// card's code and whose account may check in.
type adoption struct {
// admit: the push goes to the child, which is told to adopt.
admit bool
// claimed: the repository's row holds the code until Adopted runs.
claimed bool
// refusal is what to say, "" for the one unverified sentence.
refusal string
}
// judgeAdoption decides whether a push of project code to repo, from a
// caller already proved, is let through. In order:
//
// 1. A row that records this very code, with no claim in flight on it,
// means an earlier adoption landed and its refresh did not: the store is read, the caches are put
// right, and the push is then an ordinary one.
// 2. The forge's index must show the repository empty. The caller who
// fails this is refused as for any unverified card.
// 3. The code is claimed, in one step, against every other repository.
//
// The child is the last line: it refuses a different project from a
// store that holds anything.
func (f *Fronter) judgeAdoption(name, code string) adoption {
a := f.adopt
if !f.inFlight(name) && a.Recorded(name) == code {
if a.Adopted(name) {
if held, ok := a.Code(name); ok && held == code {
return adoption{admit: true}
}
}
}
if !a.Empty(name) {
return adoption{}
}
switch a.Claim(name, code) {
case ClaimOK:
return adoption{admit: true, claimed: true}
case ClaimTaken:
log.Printf("xfer: %s: first push not adopted: another repository has its project code", name)
return adoption{refusal: adoptTakenSentence}
default:
log.Printf("xfer: %s: first push not adopted: could not show its project code is unused", name)
return adoption{refusal: adoptUnsureSentence}
}
}
// adopted reads back what the child did with a push that was let in to
// adopt a project code, before its reply reaches the pusher, whose next
// request is judged by the caches this puts right. It reports whether
// there was anything to do and it was done.
func (f *Fronter) adopted(name, adopting string) bool {
if adopting == "" || f.adopt == nil {
return false
}
return f.adopt.Adopted(name)
}
// flight counts, per repository, the pushes let in on a claim that the
// child has not yet answered. A row that holds a live claim is not
// "healed" by another pusher's request, or the claim would be undone
// under the push that holds it and another repository could take the code.
func (f *Fronter) flight(name string, delta int) {
f.adoptMu.Lock()
defer f.adoptMu.Unlock()
if f.adoptFlight == nil {
f.adoptFlight = map[string]int{}
}
f.adoptFlight[name] += delta
if f.adoptFlight[name] <= 0 {
delete(f.adoptFlight, name)
}
}
func (f *Fronter) inFlight(name string) bool {
f.adoptMu.Lock()
defer f.adoptMu.Unlock()
return f.adoptFlight[name] > 0
}