M-15 fix: resync hands the vault each access token built from its grant (issue 1409)

Review round 1, finding 4: the vault hands a licence's access token only to
a machine a binding names, so licences_resync builds it from the grant (the
key itself for an API key) instead of reading it back.

Builds: design 39 bus-and-switch §15, §17, §42, B23, B24

Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
2026-11-12 01:02:54 +01:00
parent cc8a354f75
commit b8e8be619d
3 changed files with 16 additions and 1 deletions
@@ -61,6 +61,9 @@ type memGrants struct {
mu sync.Mutex
held map[string]heldGrant
puts []string
// refuseAccess is the vault refusing a licence's access token to the manager's machine, as it does for a licence no
// binding there names (§17).
refuseAccess bool
}
type heldGrant struct {
@@ -86,6 +89,9 @@ func (g *memGrants) Get(_ context.Context, key, part string) (string, error) {
g.mu.Lock()
defer g.mu.Unlock()
h, ok := g.held[SecretName(key, part)]
if g.refuseAccess && part == PartAccess {
return "", fmt.Errorf("no consumer on this machine is bound to the licence %s", key)
}
if !ok {
return "", fmt.Errorf("the vault holds no secret named %s", SecretName(key, part))
}
@@ -338,7 +338,15 @@ func (m *Manager) Resync(ctx context.Context) (map[string]any, error) {
continue
}
key := KeyFor(l.Name)
access, err := m.Grants.Get(ctx, key, PartAccess)
// The access token is built from the grant, which the vault hands the manager; it hands a licence's access
// token only to a machine a binding names (§17), and the manager's machine may have none.
access, err := m.Grants.Get(ctx, key, PartGrant)
if err == nil && l.Kind == "subscription" {
var g FullGrant
if err = json.Unmarshal([]byte(access), &g); err == nil {
access = accessOf(g)
}
}
if err == nil {
err = m.Grants.Put(ctx, key, PartAccess, l.Generation, access)
}
@@ -259,6 +259,7 @@ func TestResyncSaysEachOnce(t *testing.T) {
mm.m.Settings.WarnAt = 100
_ = mm.m.SayConditions(ctx, false)
mm.events, *said = nil, nil
mm.grants.refuseAccess = true // §42: the access token is built from the grant, never read back from the vault
got := answer(t, served(t, mm)["licences_resync"], nil).(map[string]any)
count := map[string]int{}
for _, e := range mm.events {