195 lines
6.6 KiB
Go
195 lines
6.6 KiB
Go
package delivery
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// Seals is, by slot, each value one composition carries sealed to its node's key (issue 1126, C1): a file's `sealed`
|
|
// a vault box (catalogue.VaultSealedField) and each entry of a file's secrets (SecretsField), as their JSON text (a box is carried as its JSON, a sealed value as
|
|
// the string it is). A slot is one field of one resource of the composition's module (SealSlot): a box is reused only
|
|
// at the slot that holds it, never for another resource whose value opens to the same (a provider's grant and its
|
|
// consumer's secret on one node hold one value).
|
|
type Seals map[string]string
|
|
|
|
// SealedFields are the fields of a resource that carry a value sealed to the node's key.
|
|
var SealedFields = []string{"sealed", catalogue.VaultSealedField}
|
|
|
|
// SealSlot is the slot of one sealed field of one resource, by the resource's id (its place, `#<i>`, when it has none).
|
|
// The module is the composition's: a slot is never compared across compositions.
|
|
func SealSlot(resource, field string) string { return resource + "#" + field }
|
|
|
|
// SecretsField is the field of a file that carries, by name, each sealed value its content asks for
|
|
// (${secret:<name>}, catalogue's intoFile): each entry is a slot of its own (SecretSlotField), so a secret sealed again
|
|
// is judged by the value it opens to, entry by entry, like a file's `sealed` (issue 1126, C1; the seed-7 probe: about 45
|
|
// files on ace and novox differed by their secrets' bytes alone).
|
|
const SecretsField = "secrets"
|
|
|
|
// SecretSlotField is the field part of the slot of one entry of a file's secrets (SealSlot(id, SecretSlotField(name))).
|
|
func SecretSlotField(name string) string { return SecretsField + "." + name }
|
|
|
|
// sealAt is one sealed value of a resource: its slot's field, its place (a top-level field, or an entry of the
|
|
// secrets), and its raw JSON.
|
|
type sealAt struct {
|
|
field, entry string
|
|
raw json.RawMessage
|
|
}
|
|
|
|
// sealsAt is every sealed value of one resource's fields, in a fixed order: SealedFields, then the secrets by name.
|
|
func sealsAt(fields map[string]json.RawMessage) []sealAt {
|
|
var out []sealAt
|
|
for _, field := range SealedFields {
|
|
if f, has := fields[field]; has {
|
|
out = append(out, sealAt{field: field, raw: f})
|
|
}
|
|
}
|
|
var secrets map[string]json.RawMessage
|
|
if f, has := fields[SecretsField]; has && json.Unmarshal(f, &secrets) == nil {
|
|
names := make([]string, 0, len(secrets))
|
|
for name := range secrets {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
out = append(out, sealAt{field: SecretSlotField(name), entry: name, raw: secrets[name]})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SealsIn is every sealed value c carries, by slot; nil when it carries none.
|
|
func SealsIn(c Composed) Seals {
|
|
var out Seals
|
|
for i, raw := range c.Resources {
|
|
id, fields, ok := sealedFieldsOf(raw, i)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, s := range sealsAt(fields) {
|
|
if text := sealedText(s.raw); text != "" {
|
|
if out == nil {
|
|
out = Seals{}
|
|
}
|
|
out[SealSlot(id, s.field)] = text
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// KeepSeals is c with each sealed value replaced by the one kept at the same slot, where same says the two open to the
|
|
// same value (known and same): a value the vault sealed again is then the box that runs, so the composition is the
|
|
// one that runs (ADR 0547 §3, issue 1126). A slot kept holds nothing for, or whose value differs or cannot be opened,
|
|
// keeps c's. Every other byte of a resource is kept as it is; c itself is not changed.
|
|
func KeepSeals(c Composed, kept Seals, same func(a, b string) (same, known bool)) Composed {
|
|
if len(kept) == 0 || same == nil {
|
|
return c
|
|
}
|
|
out := c
|
|
out.Resources = nil
|
|
for i, raw := range c.Resources {
|
|
id, fields, ok := sealedFieldsOf(raw, i)
|
|
if ok {
|
|
for _, s := range sealsAt(fields) {
|
|
ours := sealedText(s.raw)
|
|
theirs, held := kept[SealSlot(id, s.field)]
|
|
if !held || ours == "" || theirs == ours {
|
|
continue
|
|
}
|
|
if opens, known := same(ours, theirs); !known || !opens {
|
|
continue
|
|
}
|
|
value := json.RawMessage(theirs)
|
|
if s.raw[0] == '"' {
|
|
value, _ = json.Marshal(theirs)
|
|
}
|
|
if s.entry == "" {
|
|
if swapped, ok := withField(raw, s.field, value); ok {
|
|
raw = swapped
|
|
}
|
|
continue
|
|
}
|
|
// An entry of the secrets: swapped inside the secrets as they stand in raw, then the secrets in raw.
|
|
var now map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &now) != nil {
|
|
continue
|
|
}
|
|
if secrets, ok := withField(now[SecretsField], s.entry, value); ok {
|
|
if swapped, ok := withField(raw, SecretsField, secrets); ok {
|
|
raw = swapped
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out.Resources = append(out.Resources, raw)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sealedFieldsOf is a resource's id (`#<i>` when it has none) and its top-level fields, false when it is no object.
|
|
func sealedFieldsOf(raw json.RawMessage, i int) (string, map[string]json.RawMessage, bool) {
|
|
var fields map[string]json.RawMessage
|
|
if json.Unmarshal(raw, &fields) != nil {
|
|
return "", nil, false
|
|
}
|
|
var id string
|
|
if f, ok := fields["id"]; !ok || json.Unmarshal(f, &id) != nil || id == "" {
|
|
id = "#" + strconv.Itoa(i)
|
|
}
|
|
return id, fields, true
|
|
}
|
|
|
|
// sealedText is a sealed field as its text: a string as it is, a box as its JSON.
|
|
func sealedText(f json.RawMessage) string {
|
|
var text string
|
|
if json.Unmarshal(f, &text) == nil {
|
|
return text
|
|
}
|
|
var compact bytes.Buffer
|
|
if json.Compact(&compact, f) != nil {
|
|
return string(f)
|
|
}
|
|
return compact.String()
|
|
}
|
|
|
|
// withField is the object raw with its top-level field replaced by value, every other byte as it was (so a composition
|
|
// whose sealed values are those that run is the one that runs, byte for byte); false when raw is no object holding it.
|
|
func withField(raw json.RawMessage, field string, value json.RawMessage) (json.RawMessage, bool) {
|
|
d := json.NewDecoder(bytes.NewReader(raw))
|
|
if t, err := d.Token(); err != nil || t != json.Delim('{') {
|
|
return nil, false
|
|
}
|
|
for d.More() {
|
|
t, err := d.Token()
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
key, _ := t.(string)
|
|
before := d.InputOffset()
|
|
var skip json.RawMessage
|
|
if err := d.Decode(&skip); err != nil {
|
|
return nil, false
|
|
}
|
|
if key != field {
|
|
continue
|
|
}
|
|
after := d.InputOffset()
|
|
// The span holds the colon, any space and the value: kept up to the colon, the value replaced.
|
|
span := raw[before:after]
|
|
colon := bytes.IndexByte(span, ':')
|
|
if colon < 0 {
|
|
return nil, false
|
|
}
|
|
out := make([]byte, 0, len(raw)-len(span)+colon+1+len(value))
|
|
out = append(out, raw[:int(before)+colon+1]...)
|
|
out = append(out, value...)
|
|
out = append(out, raw[after:]...)
|
|
return out, true
|
|
}
|
|
return nil, false
|
|
}
|