The owner is the account the manifest names for the directory that holds the file; the bootstrap no longer makes that directory itself, and a run that reaches it first is refused naming what it waits on. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
115 lines
4.6 KiB
Go
115 lines
4.6 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"syscall"
|
|
)
|
|
|
|
// PushCredential is the bootstrap's one-off credential for the holders' first images (design 51 *The move*; issue 1966 ENG-5):
|
|
// a bearer token of 32 random bytes, hex, on one line, made for one fresh-mesh run. No vault and no bus holds it: this run
|
|
// holds it and writes it once, to the file the store's holder reads on each write (`Authorization: Bearer <token>` on an OCI
|
|
// write to the store's port, from any node); the holder removes the file when it answers its first `publish`, and this run
|
|
// forgets the token when it ends. On the holder's own node the bootstrap may push to the registry container's loopback with
|
|
// no credential at all.
|
|
type PushCredential struct {
|
|
// File is where the token was written.
|
|
File string
|
|
token string
|
|
}
|
|
|
|
// Token is the bearer token, for a push through the store's front from another node.
|
|
func (c *PushCredential) Token() string { return c.token }
|
|
|
|
// Forget drops the token from this run's memory. The file is the holder's to remove.
|
|
func (c *PushCredential) Forget() { c.token = "" }
|
|
|
|
// lookupAccount gives an account's user id and group id. A variable so a test can name an account this machine has.
|
|
var lookupAccount = func(name string) (uid, gid int, err error) {
|
|
u, err := user.Lookup(name)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if uid, err = strconv.Atoi(u.Uid); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
gid, err = strconv.Atoi(u.Gid)
|
|
return uid, gid, err
|
|
}
|
|
|
|
// WritePushCredential writes the one-off credential for the store's holder, read from its manifest in catalogue: the file
|
|
// goes into the holder's directory resource that holds file's path, given to the account that directory names as owner,
|
|
// and only once the node-engine has made that account and that directory, owned by it. Until then the holder's program,
|
|
// which runs as that account, could not read it, so it is refused rather than written as root's.
|
|
func WritePushCredential(catalogue, file string) (*PushCredential, error) {
|
|
manifest, err := readManifest(catalogue, RegistryModule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m struct {
|
|
Resources []struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Owner string `json:"owner"`
|
|
} `json:"resources"`
|
|
}
|
|
if err := json.Unmarshal(manifest, &m); err != nil {
|
|
return nil, fmt.Errorf("the %s manifest is not readable as JSON: %w", RegistryModule, err)
|
|
}
|
|
dir, owner, account := filepath.Dir(file), "", false
|
|
for _, r := range m.Resources {
|
|
if r.Type == "directory" && filepath.Clean(r.Path) == dir {
|
|
owner = r.Owner
|
|
}
|
|
}
|
|
for _, r := range m.Resources {
|
|
account = account || (owner != "" && r.Type == "user" && r.Name == owner)
|
|
}
|
|
if !account {
|
|
return nil, fmt.Errorf("the push credential %s has no account to go to: the %s manifest declares no directory %s "+
|
|
"owned by an account it declares, so its program could not read the file", file, RegistryModule, dir)
|
|
}
|
|
uid, gid, err := lookupAccount(owner)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the push credential waits on the account %s, which the node-engine has not made: %w", owner, err)
|
|
}
|
|
st, err := os.Stat(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the push credential waits on %s's state directory %s, which the node-engine has not made: %w", owner, dir, err)
|
|
}
|
|
if sys, ok := st.Sys().(*syscall.Stat_t); !ok || int(sys.Uid) != uid {
|
|
return nil, fmt.Errorf("the push credential waits on %s owning its state directory %s, which the node-engine has not yet given it", owner, dir)
|
|
}
|
|
return MintPushCredential(file, uid, gid)
|
|
}
|
|
|
|
// MintPushCredential makes a token and writes it to file, mode 0600, owned by the user id and group id given, into a
|
|
// directory that must be there. A file that is there is replaced: a credential left by an earlier run is never reused.
|
|
func MintPushCredential(file string, uid, gid int) (*PushCredential, error) {
|
|
raw := make([]byte, 32)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return nil, fmt.Errorf("a push credential cannot be made: %w", err)
|
|
}
|
|
token := hex.EncodeToString(raw)
|
|
tmp := file + ".new"
|
|
if err := os.WriteFile(tmp, []byte(token+"\n"), 0o600); err != nil {
|
|
return nil, fmt.Errorf("the push credential cannot be written to %s: %w", file, err)
|
|
}
|
|
if err := os.Chown(tmp, uid, gid); err != nil {
|
|
os.Remove(tmp)
|
|
return nil, fmt.Errorf("the push credential cannot be given to the store's holder: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, file); err != nil {
|
|
os.Remove(tmp)
|
|
return nil, fmt.Errorf("the push credential cannot be put at %s: %w", file, err)
|
|
}
|
|
return &PushCredential{File: file, token: token}, nil
|
|
}
|