diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 6bf897b..b5a4fd2 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -490,6 +490,44 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { t.Error("node-logins' named readers") } }) + // 774/S40/AC1 (ADR 0587 §4, §6): from the lease write an enrolling node's user may publish the redeem call in its + // own name and nothing else, and subscribe its own inbox alone; before it, the controller's subject beside. + t.Run("an enrolling node redeems at the seat alone", func(t *testing.T) { + holder := Seat{Name: catalogue.EnrolmentSeat, Serves: []string{"issue", "redeem", "confirm", "revoke", "tokens", "again"}} + for _, handed := range []bool{true, false} { + users, err := Users(Records{Nodes: []string{"ace"}, Enrolling: []string{"lab"}, LeaseHandedAway: handed, + Assigned: map[string][]Declared{"ace": {{Module: "mesh-enrolment", Holds: []Seat{holder}}}}}) + if err != nil { + t.Fatal(err) + } + var lab *Principal + for i := range users { + if users[i].Username() == "enrol.lab" { + lab = &users[i] + } + } + if lab == nil { + t.Fatal("no user enrol.lab") + } + perms, err := PermissionsFor(*lab) + if err != nil { + t.Fatal(err) + } + want := []string{"mesh.seat.mesh-enrolment.call.redeem.enrol~lab"} + if !handed { + want = []string{"mesh.control.enrol", "mesh.seat.mesh-enrolment.call.redeem.enrol~lab"} + } + if !reflect.DeepEqual(perms.Publish, want) || !reflect.DeepEqual(perms.Subscribe, []string{"_INBOX.enrol.lab.>"}) || + perms.AllowResponses { + t.Errorf("handed %v: enrol.lab publishes %v, subscribes %v, answers %v", handed, perms.Publish, perms.Subscribe, + perms.AllowResponses) + } + } + // No holder serving redeem and the lease handed: the user may enrol nowhere, refused by name. + if _, err := PermissionsFor(Principal{Kind: KindEnrolment, Node: "lab", LeaseHandedAway: true}); err == nil { + t.Error("an enrolment user that may enrol nowhere was composed") + } + }) t.Run("the controller's golden server configuration", func(t *testing.T) { seat := Seat{Name: "telegram-sender", Accepts: []string{"send"}, Emits: []string{"delivered", "failed"}} got, err := Compose( diff --git a/go/buscompose/nats.go b/go/buscompose/nats.go index 7fea9e0..461b36d 100644 --- a/go/buscompose/nats.go +++ b/go/buscompose/nats.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -263,6 +264,12 @@ type Principal struct { // §3). Set while the vault's holder serves rotate-node. RotatesItsLogin bool + // Redeems is an enrolment principal that may call the seat mesh-enrolment's redeem in its own name, and nothing else + // of it (novox/hq ADR 0587 §4): set while the seat's holder serves redeem. LeaseHandedAway drops the controller's + // enrolment subject from it, which is served no more from the lease write (§6). + Redeems bool + LeaseHandedAway bool + // PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal // and never appears here: this file is written to a node's disk and read by a server, and a // secret that can be read from a configuration file is a secret with a wider blast radius @@ -398,6 +405,16 @@ var ( VaultNodeAccountConfirm = SeatVerb{Seat: "mesh-vault", Verb: delivery.NodeAccountConfirmVerb} ) +// EnrolmentRedeem is the seat mesh-enrolment's verb a joining node-engine calls as enrol. (novox/hq ADR 0587 §4, +// §6). +var EnrolmentRedeem = SeatVerb{Seat: catalogue.EnrolmentSeat, Verb: catalogue.EnrolmentVerbRedeem} + +// EnrolmentRedeemCall is the one subject an enrolment user may publish once it enrols through the seat: redeem, in its +// own name. +func EnrolmentRedeemCall(p Principal) string { + return "mesh.seat." + EnrolmentRedeem.Seat + "." + CallKind + "." + EnrolmentRedeem.Verb + "." + CallerToken(p.Username()) +} + // NodeLoginsReads is what a node's engine is granted of the vault's node-logins: its own node's key, read and watched, // and no other (novox/hq ADR 0583 §1 insight). func NodeLoginsReads(node string) []string { @@ -926,7 +943,20 @@ func PermissionsFor(p Principal) (Permissions, error) { "enrolment: a token is issued for a node record, and that record's name is " + "the token's id") } - pub = []string{"mesh.control.enrol"} + // **From the lease write a node enrols through the seat mesh-enrolment alone** (novox/hq ADR 0587 §4, §6): its + // user calls redeem in its own name, while the seat's holder serves it, and the controller's enrolment subject is + // kept only while the controller holds the lease. + pub = nil + if !p.LeaseHandedAway { + pub = append(pub, "mesh.control.enrol") + } + if p.Redeems { + pub = append(pub, EnrolmentRedeemCall(p)) + } + if len(pub) == 0 { + return Permissions{}, fmt.Errorf("the enrolment user of %s may enrol nowhere: the lease is handed away and "+ + "no holder of %s serves %s", p.Node, EnrolmentRedeem.Seat, EnrolmentRedeem.Verb) + } sub = []string{p.inbox()} case KindNode: diff --git a/go/buscompose/users.go b/go/buscompose/users.go index ebd11dd..6802033 100644 --- a/go/buscompose/users.go +++ b/go/buscompose/users.go @@ -131,7 +131,8 @@ type Records struct { Assigned map[string][]Declared // Enrolling is every node with a live token — one enrolment user each, because the inbox an // answer goes to is scoped to the token and a shared one is one machine reading another's - // sealed credentials (design 25 §6). + // sealed credentials (design 25 §6). The grants part fills it from the seat mesh-enrolment's keys at live or + // redeeming (novox/hq ADR 0587 §4), the controller from its store. Enrolling []string // People is each person's name against the tools they may invoke, `*` for an administrator. People map[string][]string @@ -147,6 +148,10 @@ type Records struct { // grants part as a placed module's are; it adds no principal, since each node's engine speaks as the node's account. // Nil where its manifest was not read. Engine *Declared + // LeaseHandedAway says the controller's lease names a holder that is no controller (ADR 0582 §3), as the grants part + // reads it: an enrolment user then enrols through the seat mesh-enrolment alone (novox/hq ADR 0587 §6). The + // controller's composer never sets it. + LeaseHandedAway bool // NodeLogins is each node's key of the vault's node-logins (novox/hq ADR 0583 §1): the login in use and the one // rotating, each composed as a user of the node's permissions. A node absent logs in as `node.` alone. NodeLogins map[string]delivery.NodeLogin @@ -284,7 +289,8 @@ func Users(r Records) ([]Principal, error) { } } for _, node := range sortedCopy(r.Enrolling) { - out = append(out, Principal{Kind: KindEnrolment, Node: node}) + out = append(out, Principal{Kind: KindEnrolment, Node: node, Redeems: servesVerb(r, EnrolmentRedeem), + LeaseHandedAway: r.LeaseHandedAway}) } for _, person := range sortedNames(r.People) { p := Principal{Kind: KindPerson, Module: person, Invokes: r.People[person]}