Merge pull request 'Grant the controller mesh-config's config verb in its own name (issue 925)' (#361) from fix/925-controller-asks-mesh-config into main
This commit was merged in pull request #361.
This commit is contained in:
@@ -160,3 +160,87 @@ func TestNatsOnlyMeshConfigsAccountPutsToItsBuckets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **The controller asks mesh-config's `config` in its own name** (issue 925): on a real server whose users are
|
||||||
|
// composed from records that hold mesh-config on novox serving `config`, the controller's request on
|
||||||
|
// `mesh.seat.mesh-config.call.config.controller` reaches mesh-config's account and its reply reaches the
|
||||||
|
// controller's inbox; the same request under another caller's name is refused by the server.
|
||||||
|
func TestNatsTheControllerAsksMeshConfigInItsOwnName(t *testing.T) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte("pw"), bcrypt.MinCost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
seat := Seat{Name: "mesh-config", Scope: "mesh", Serves: []string{"config"}}
|
||||||
|
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{
|
||||||
|
"novox": {{Module: RuntimeModule}, {Module: "mesh-config", RunsAs: "mesh-config", Holds: []Seat{seat}}},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i := range users {
|
||||||
|
users[i].PasswordHash = string(hash)
|
||||||
|
}
|
||||||
|
accounts, err := ComposeAccounts(users)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conf := filepath.Join(t.TempDir(), "bus.conf")
|
||||||
|
if err := os.WriteFile(conf, []byte("listen: 127.0.0.1:-1\n"+accounts), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
opts, err := server.ProcessConfigFile(conf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("the composed accounts do not parse: %v", err)
|
||||||
|
}
|
||||||
|
opts.NoLog, opts.NoSigs = true, true
|
||||||
|
s, err := server.NewServer(opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go s.Start()
|
||||||
|
if !s.ReadyForConnections(10 * time.Second) {
|
||||||
|
t.Fatal("the bus did not come up")
|
||||||
|
}
|
||||||
|
t.Cleanup(s.Shutdown)
|
||||||
|
connect := func(user string) (*nats.Conn, chan error) {
|
||||||
|
t.Helper()
|
||||||
|
refused := make(chan error, 8)
|
||||||
|
nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, "pw"), nats.CustomInboxPrefix("_INBOX."+user),
|
||||||
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { refused <- err }))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s cannot connect: %v", user, err)
|
||||||
|
}
|
||||||
|
t.Cleanup(nc.Close)
|
||||||
|
return nc, refused
|
||||||
|
}
|
||||||
|
owner, _ := connect("novox.mesh-config")
|
||||||
|
subject := CalledSubject("mesh.seat.mesh-config.tool.config", ControllerName)
|
||||||
|
if subject != "mesh.seat.mesh-config.call.config.controller" {
|
||||||
|
t.Fatalf("the subject is %s", subject)
|
||||||
|
}
|
||||||
|
if _, err := owner.Subscribe("mesh.seat.mesh-config.call.config.*", func(m *nats.Msg) {
|
||||||
|
_ = m.Respond([]byte(`{"ok":true}`))
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := owner.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
controller, refused := connect(ControllerName)
|
||||||
|
reply, err := controller.Request(subject, []byte(`{"action":"set"}`), 5*time.Second)
|
||||||
|
if err != nil || string(reply.Data) != `{"ok":true}` {
|
||||||
|
t.Fatalf("the controller's own-name request got %v (%v), want mesh-config's reply", reply, err)
|
||||||
|
}
|
||||||
|
if err := controller.Publish(CalledSubject("mesh.seat.mesh-config.tool.config", "anchor.node-tools"), nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = controller.Flush()
|
||||||
|
select {
|
||||||
|
case err := <-refused:
|
||||||
|
if !errors.Is(err, nats.ErrPermissionViolation) {
|
||||||
|
t.Errorf("the server said %v, want a permissions violation", err)
|
||||||
|
}
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Error("the server did not refuse the controller a request in another's name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -226,3 +226,45 @@ func TestTheControllerMayAskTheVaultInItsOwnName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **The controller may ask mesh-config's `config` in its own name** (issue 925): move-config and the settings verb
|
||||||
|
// set a trusted value through mesh-config, which takes it as the controller carries it only from the subject that
|
||||||
|
// names the controller as caller. Granted when the records hold a holder serving the verb, and not otherwise;
|
||||||
|
// the installer's first user list carries none.
|
||||||
|
func TestTheControllerMayAskMeshConfigInItsOwnName(t *testing.T) {
|
||||||
|
config := Declared{Module: "mesh-config", Holds: []Seat{{Name: "mesh-config", Serves: []string{"config"}}}}
|
||||||
|
users, err := Users(Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {config}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Join(users[0].AsksTheConfig, ",") != "config" {
|
||||||
|
t.Fatalf("composed %v", users[0].AsksTheConfig)
|
||||||
|
}
|
||||||
|
p, err := PermissionsFor(users[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !slices.Contains(p.Publish, "mesh.seat.mesh-config.call.config.controller") {
|
||||||
|
t.Errorf("the controller may not publish mesh.seat.mesh-config.call.config.controller: %v", p.Publish)
|
||||||
|
}
|
||||||
|
if !MayPublish(p, CalledSubject("mesh.seat.mesh-config.tool.config", ControllerName)) {
|
||||||
|
t.Error("the controller may not ask mesh-config's config in its own name")
|
||||||
|
}
|
||||||
|
if MayPublish(p, CalledSubject("mesh.seat.mesh-config.tool.config", "anchor.node-tools")) {
|
||||||
|
t.Error("the controller may ask mesh-config's config in another's name")
|
||||||
|
}
|
||||||
|
// No holder serving it composes no grant, nor does genesis.
|
||||||
|
other := Declared{Module: "mesh-config", Holds: []Seat{{Name: "mesh-config", Serves: []string{"list"}}}}
|
||||||
|
for _, r := range []Records{{Nodes: []string{"novox"}}, {Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {other}}}} {
|
||||||
|
users, _ = Users(r)
|
||||||
|
if len(users[0].AsksTheConfig) != 0 {
|
||||||
|
t.Errorf("composed %v without a holder serving config", users[0].AsksTheConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
genesis, _ := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
||||||
|
for _, s := range genesis.Publish {
|
||||||
|
if strings.Contains(s, "mesh-config") {
|
||||||
|
t.Errorf("the installer's controller may publish %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ type Principal struct {
|
|||||||
// controller asks. Composed from the records like AsksIssued, so the installer's first user list carries none.
|
// controller asks. Composed from the records like AsksIssued, so the installer's first user list carries none.
|
||||||
AsksTheVault []string
|
AsksTheVault []string
|
||||||
|
|
||||||
|
// AsksTheConfig are the mesh-config verbs the controller asks in its own name (VerbsTheControllerAsksTheConfig,
|
||||||
|
// issue 925), those mesh-config's holder serves: on the subject that names its caller, so mesh-config takes a
|
||||||
|
// value as the controller carries it (ADR 0348 §8). Composed from the records like AsksTheVault, so the
|
||||||
|
// installer's first user list carries none.
|
||||||
|
AsksTheConfig []string
|
||||||
|
|
||||||
// AsksTheDesk says this principal is the vault's holder, which asks the desks' secret prompt in its own name
|
// AsksTheDesk says this principal is the vault's holder, which asks the desks' secret prompt in its own name
|
||||||
// (novox/hq ADR 0349 clause 9, issue 723): `node-launcher.secret` on any machine, on the subject that names
|
// (novox/hq ADR 0349 clause 9, issue 723): `node-launcher.secret` on any machine, on the subject that names
|
||||||
// its caller, and nothing else of that seat. With AsksIssued on the same principal, the vault's whole reach
|
// its caller, and nothing else of that seat. With AsksIssued on the same principal, the vault's whole reach
|
||||||
@@ -316,6 +322,12 @@ var VerbsTheControllerAsksTheVault = []SeatVerb{{Seat: "mesh-vault", Verb: "acce
|
|||||||
{Seat: "mesh-vault", Verb: "secret-ask"}, {Seat: "mesh-vault", Verb: "issued"}, {Seat: "mesh-vault", Verb: "rotate"},
|
{Seat: "mesh-vault", Verb: "secret-ask"}, {Seat: "mesh-vault", Verb: "issued"}, {Seat: "mesh-vault", Verb: "rotate"},
|
||||||
{Seat: "mesh-vault", Verb: "move-asks"}, {Seat: "mesh-vault", Verb: "get"}}
|
{Seat: "mesh-vault", Verb: "move-asks"}, {Seat: "mesh-vault", Verb: "get"}}
|
||||||
|
|
||||||
|
// VerbsTheControllerAsksTheConfig are the mesh-config seat's verbs the controller calls in its own name (issue 925):
|
||||||
|
// `config`, the one verb mesh-config serves, which move-config uses to set each value and the settings verb to set,
|
||||||
|
// clear and propose through mesh-config. mesh-config takes a value as the controller carries it only from the
|
||||||
|
// subject that names the controller as caller (ADR 0348 §8); granted as Principal.AsksTheConfig says.
|
||||||
|
var VerbsTheControllerAsksTheConfig = []SeatVerb{{Seat: "mesh-config", Verb: "config"}}
|
||||||
|
|
||||||
// VaultMoveIn is the vault's verb a machine's node-engine asks in its own name to hand in a value sealed to its
|
// VaultMoveIn is the vault's verb a machine's node-engine asks in its own name to hand in a value sealed to its
|
||||||
// machine (design 57's V3, issue 676); granted as Principal.MovesIntoTheVault says.
|
// machine (design 57's V3, issue 676); granted as Principal.MovesIntoTheVault says.
|
||||||
var VaultMoveIn = SeatVerb{Seat: "mesh-vault", Verb: "move-in"}
|
var VaultMoveIn = SeatVerb{Seat: "mesh-vault", Verb: "move-in"}
|
||||||
@@ -1089,6 +1101,13 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+CallerToken(p.Username()))
|
pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+CallerToken(p.Username()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **And mesh-config's verb** (issue 925): it takes a trusted value from the controller alone, known from the
|
||||||
|
// subject's last token.
|
||||||
|
for _, v := range VerbsTheControllerAsksTheConfig {
|
||||||
|
if slices.Contains(p.AsksTheConfig, v.Verb) {
|
||||||
|
pub = append(pub, "mesh.seat."+v.Seat+"."+CallKind+"."+v.Verb+"."+CallerToken(p.Username()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(pub)
|
sort.Strings(pub)
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ type Records struct {
|
|||||||
// is a mesh that cannot be told anything, and there is no state of the records in which that is
|
// is a mesh that cannot be told anything, and there is no state of the records in which that is
|
||||||
// correct.
|
// correct.
|
||||||
func Users(r Records) ([]Principal, error) {
|
func Users(r Records) ([]Principal, error) {
|
||||||
out := []Principal{{Kind: KindController, Uses: asksSeatOf(r), AsksIssued: issuedOf(r), AsksTheVault: vaultVerbsOf(r)}}
|
out := []Principal{{Kind: KindController, Uses: asksSeatOf(r), AsksIssued: issuedOf(r), AsksTheVault: vaultVerbsOf(r),
|
||||||
|
AsksTheConfig: servedOf(r, VerbsTheControllerAsksTheConfig)}}
|
||||||
|
|
||||||
for _, node := range sortedCopy(r.Nodes) {
|
for _, node := range sortedCopy(r.Nodes) {
|
||||||
witness := false
|
witness := false
|
||||||
@@ -290,13 +291,16 @@ func holdsSeat(d Declared, seat string) bool {
|
|||||||
|
|
||||||
// vaultVerbsOf are the vault's intake verbs its holder serves, as the records say (issue 675): what the controller
|
// vaultVerbsOf are the vault's intake verbs its holder serves, as the records say (issue 675): what the controller
|
||||||
// is granted to ask it in its own name. None while no holder serves them.
|
// is granted to ask it in its own name. None while no holder serves them.
|
||||||
func vaultVerbsOf(r Records) []string {
|
func vaultVerbsOf(r Records) []string { return servedOf(r, VerbsTheControllerAsksTheVault) }
|
||||||
|
|
||||||
|
// servedOf are the verbs of asked that a seat's holder serves, as the records say, by name.
|
||||||
|
func servedOf(r Records, asked []SeatVerb) []string {
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
var out []string
|
var out []string
|
||||||
for _, node := range sortedCopy(r.Nodes) {
|
for _, node := range sortedCopy(r.Nodes) {
|
||||||
for _, d := range r.Assigned[node] {
|
for _, d := range r.Assigned[node] {
|
||||||
for _, s := range d.Holds {
|
for _, s := range d.Holds {
|
||||||
for _, v := range VerbsTheControllerAsksTheVault {
|
for _, v := range asked {
|
||||||
if s.Name == v.Seat && slices.Contains(s.Serves, v.Verb) && !seen[v.Verb] {
|
if s.Name == v.Seat && slices.Contains(s.Serves, v.Verb) && !seen[v.Verb] {
|
||||||
seen[v.Verb] = true
|
seen[v.Verb] = true
|
||||||
out = append(out, v.Verb)
|
out = append(out, v.Verb)
|
||||||
|
|||||||
Reference in New Issue
Block a user