From c73294e52c30f05eba56a0cd61c1e4f784525030 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:30:48 -0400 Subject: [PATCH] fix(accounts): a fixture's solution installation follows the authority core's, so its teams are exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fixture's declared solution installation is made twice: in the authority core, which activates it at once (consented and composed), and in accounts' own installations table, where it stays pending until the solution's install step is acknowledged. With no consumer acknowledging it, every pass failed to expose its teams ("installation … is not an active installation in this organization", pkg/infra/postgres_installations.go:1054, which needs status 'active'), so a member reaching the solution through a team held no role. accounts' record now follows the core's: once fixture convergence finds the core's installation active, accounts' record of the same installation, pending or failed at its step, becomes active with the root scope node it was installed with (ActivateInstallationFollowingCore), and its declared teams are exposed in the same pass. The core's journal records the activation. The install-step projection only ever moves a pending row, so a later acknowledgement changes nothing. The conformance harness delivers a fixture-declared solution to the running host: accounts' record is active, the declared team is exposed to it, and a member of that team is issued a capability through the solution. It fails without the change. Co-Authored-By: Claude Opus 5.5 --- .../accounts/code/fixtures/installations.go | 33 ++++++++++------- .../host_interface_conformance_test.go | 36 +++++++++++++++++++ .../pkg/business/installation_follows_core.go | 33 +++++++++++++++++ .../code/pkg/infra/postgres_installations.go | 18 ++++++++++ 4 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 module/services/accounts/code/pkg/business/installation_follows_core.go diff --git a/module/services/accounts/code/fixtures/installations.go b/module/services/accounts/code/fixtures/installations.go index ad56195fd..f584a34de 100644 --- a/module/services/accounts/code/fixtures/installations.go +++ b/module/services/accounts/code/fixtures/installations.go @@ -153,11 +153,18 @@ func (i *Installations) Converge(ctx context.Context) error { // interfaces decide with (coreInstall). A target the core has not // composed yet keeps the row for the next pass. coreWaits := false - switch err := i.coreInstall(ctx, row); { + switch active, err := i.coreInstall(ctx, row); { case errors.Is(err, errCoreTargetNotComposed): coreWaits = true case err != nil: failures = append(failures, fmt.Errorf("fixture installation %s: %w", row.declared.describe(), err)) + case active: + // accounts' record of the installation follows the core's: the + // core's is active, so accounts' is, and its teams can be + // exposed below in this same pass. + if err := i.service.FollowCoreInstallation(ctx, row.orgID, installation.GetId()); err != nil { + failures = append(failures, fmt.Errorf("fixture installation %s: %w", row.declared.describe(), err)) + } } // Each declared team is exposed once the installation admits it (an // installation still pending its install step does not); one not @@ -187,13 +194,13 @@ var errCoreTargetNotComposed = errors.New("the authority core has not composed t // owner and its role to each declared team (GrantRoleAsHost), as the local // authority's fixture does. Each step is idempotent, so a reseed repeats it // harmlessly. A host whose core is not configured installs nothing there. -func (i *Installations) coreInstall(ctx context.Context, row pendingInstallation) error { +func (i *Installations) coreInstall(ctx context.Context, row pendingInstallation) (bool, error) { core, err := i.service.AuthorityCore() if errors.Is(err, business.ErrAuthorityCoreUnconfigured) { - return nil + return false, nil } if err != nil { - return err + return false, err } // A module's installation is accounts' own record, which Converge's install // made; the core reads it and installs no module itself. A target the core @@ -201,21 +208,23 @@ func (i *Installations) coreInstall(ctx context.Context, row pendingInstallation // composes it. m, err := core.Composed(ctx, row.declared.Target) if code, ok := domain.CodeOf(err); ok && code == domain.NotFound { - return fmt.Errorf("%w: %s", errCoreTargetNotComposed, row.declared.Target) + return false, fmt.Errorf("%w: %s", errCoreTargetNotComposed, row.declared.Target) } if err != nil { - return fmt.Errorf("the authority core: %w", err) + return false, fmt.Errorf("the authority core: %w", err) } if m.Kind != domain.KindSolution { - return nil + return false, nil } inst, err := core.InstallAsHost(ctx, row.orgID, row.declared.Target, row.declared.Role, row.ownerID, i.service.AuthorityInstallStep()) switch { case errors.Is(err, domain.ErrAlreadyInstalled): - // Installed already: its grants are granted again, idempotently. + // Installed already, and active: its grants are granted again, + // idempotently. inst.ID = authority.StableInstallationID(row.orgID, row.declared.Target) + inst.Status = domain.Active case err != nil: - return fmt.Errorf("the authority core: %w", err) + return false, fmt.Errorf("the authority core: %w", err) } grants := []authority.RoleRequest{{Organization: row.orgID, InstallationID: inst.ID, Role: domain.InstallerRole, PersonID: row.ownerID}} for _, teamID := range row.allTeamIDs { @@ -223,10 +232,10 @@ func (i *Installations) coreInstall(ctx context.Context, row pendingInstallation } for _, grant := range grants { if _, err := core.GrantRoleAsHost(ctx, grant); err != nil { - return fmt.Errorf("the authority core: grant %s: %w", grant.Role, err) + return false, fmt.Errorf("the authority core: grant %s: %w", grant.Role, err) } } - return nil + return inst.Status == domain.Active, nil } // ConvergeAuthority performs every declared installation in the authority core @@ -241,7 +250,7 @@ func (i *Installations) ConvergeAuthority(ctx context.Context) error { defer i.mu.Unlock() var failures []error for _, row := range i.pending { - err := i.coreInstall(ctx, row) + _, err := i.coreInstall(ctx, row) if code, ok := domain.CodeOf(err); errors.Is(err, errCoreTargetNotComposed) || ok && code == domain.NotFound { continue } diff --git a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go index 979c1e190..d7ca6a9b7 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go +++ b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go @@ -69,6 +69,7 @@ const ( conformanceAlex = "00000000-0000-7000-8000-0000000000b3" conformanceIvy = "00000000-0000-7000-8000-0000000000b9" conformanceZoe = "00000000-0000-7000-8000-0000000000b5" + conformanceMaya = "00000000-0000-7000-8000-0000000000b1" ) // TestAccountsPassesTheHostInterfaceConformanceSuite boots accounts over a @@ -887,6 +888,41 @@ func runHostInterfaceFixture(t *testing.T) { require.NoError(t, err) finance := teamNamed(t, ctx, svc, fixtureOrganization, "Finance") require.Contains(t, grants, domain.RoleGrant{InstallationID: inst.ID, Role: "viewer", TeamID: finance}, "the declared team holds its role") + + // accounts' own record of the installation follows the core's: active, + // with no install step acknowledged, and exposed to the declared team. + available, _, err := svc.ListAvailableSolutions(ctx, fixtureOrganization, "", 500) + require.NoError(t, err) + var target string + for _, entry := range available { + if entry.RouteAlias == "conformance-late" { + target = entry.TargetID + } + } + require.NotEmpty(t, target) + rows, _, err := svc.ListOrganizationInstallations(ctx, fixtureOrganization, "", 100) + require.NoError(t, err) + var native *gen.Installation + for _, row := range rows { + if row.Summary.GetInstallation().GetTargetId() == target { + native = row.Summary.GetInstallation() + } + } + require.NotNil(t, native, "accounts holds its own record of the installation") + require.Equal(t, gen.InstallationStatus_INSTALLATION_STATUS_ACTIVE, native.GetStatus(), "accounts' record follows the core's") + entitled, err := svc.ListSolutionEntitlements(ctx, &gen.ListSolutionEntitlementsRequest{OrgId: fixtureOrganization, SubjectId: conformanceMaya}) + require.NoError(t, err) + var exposed bool + for _, entitlement := range entitled.GetEntitlements() { + exposed = exposed || entitlement.GetInstallationId() == native.GetId() + } + require.True(t, exposed, "the declared team is exposed to it") + + // A person on the team is issued a capability through the solution. + issued, err := authorityv1.NewAuthorityServiceClient(conn).Issue(signIn("fixture-maya"), &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "conformance-late"}) + require.NoError(t, err, "a member of the declared team holds the solution's role") + require.NotEmpty(t, issued.GetContext().GetAuthorityScopes()) }) // Last, because the envelope it writes becomes every module's ceiling: a diff --git a/module/services/accounts/code/pkg/business/installation_follows_core.go b/module/services/accounts/code/pkg/business/installation_follows_core.go new file mode 100644 index 000000000..413dfad6e --- /dev/null +++ b/module/services/accounts/code/pkg/business/installation_follows_core.go @@ -0,0 +1,33 @@ +package business + +import ( + "context" + "fmt" +) + +// installationActivator activates accounts' record of an installation the +// authority core holds active. +type installationActivator interface { + ActivateInstallationFollowingCore(ctx context.Context, orgID, installationID string) (bool, error) +} + +// FollowCoreInstallation makes accounts' record of a solution installation +// follow the authority core's, which the host interfaces decide with: once +// the core's is active (consented and composed), accounts' record of the same +// installation, pending its install step or failed at it, is active too, with +// the root scope node it was installed with, so its teams can be exposed. +// One source of truth: accounts' record never activates on its own here; it +// is called with the core's installation active. An installation already +// active, or revoked, is left as it is. The activation is the core's, which +// its journal records (saas.authority.installation_activated); this records +// no second event for it. +func (s *Service) FollowCoreInstallation(ctx context.Context, orgID, installationID string) error { + store, ok := s.store.(installationActivator) + if !ok { + return fmt.Errorf("the installation store cannot follow the authority core") + } + return s.store.WithOrgTx(ctx, orgID, func(ctx context.Context) error { + _, err := store.ActivateInstallationFollowingCore(ctx, orgID, installationID) + return err + }) +} diff --git a/module/services/accounts/code/pkg/infra/postgres_installations.go b/module/services/accounts/code/pkg/infra/postgres_installations.go index 6fb041ed9..7fac6bb8a 100644 --- a/module/services/accounts/code/pkg/infra/postgres_installations.go +++ b/module/services/accounts/code/pkg/infra/postgres_installations.go @@ -1156,3 +1156,21 @@ func (s *PostgresStore) InstallationRoleRevision(ctx context.Context, org, id st } return roleID, role, revision, nil } + +// ActivateInstallationFollowingCore activates accounts' record of an +// installation whose authority core installation is active: pending its +// install step, or failed at it, it becomes active, keeping the root scope +// node it was installed with. It reports whether it changed the row; an +// active or revoked installation is left as it is. The installation-step +// projection (record_installation_step) only ever moves a pending row, so a +// later acknowledgement changes nothing. +func (s *PostgresStore) ActivateInstallationFollowingCore(ctx context.Context, orgID, installationID string) (bool, error) { + tag, err := s.getQueryExecutor(ctx).Exec(ctx, ` + UPDATE installations SET status = 'active', failure_reason = NULL + WHERE org_id = $1 AND id::text = $2 AND status IN ('pending', 'failed')`, + orgID, installationID) + if err != nil { + return false, fmt.Errorf("activate installation %s following the authority core: %w", installationID, err) + } + return tag.RowsAffected() == 1, nil +}