go-loose/internal/store/application_access_integration_test.go
Daniel Noulet 49ab89ae46 refactor: keep tenant applications out of the administrator's reach
Claiming the system administrator role no longer grants access to every
application in the installation. Owning a tenant is what lets an administrator
administer it; entering the applications running inside it is a separate,
explicit grant that can be withdrawn again. Handing it out at claim time gave
every installation a standing administrator account inside every customer
application, which is the opposite of the boundary this product draws.

The behaviour suite now covers single sign-on between applications directly: a
second application on the same tenant must receive a code and identify the same
person with no sign-in page in between, once Go Loose already holds a session.

Also adds theme-token tests against the shared styling contract and store
coverage for application grants, and drops the redundant server and security
scheme declarations from the published specification.
2026-10-05 21:48:05 +02:00

121 lines
4.8 KiB
Go

package store_test
import (
"errors"
"testing"
"github.com/danyel/go-loose/internal/store"
)
// TestSystemAdministratorIsNotAutomaticallyInsideTenantApplications pins the
// boundary between the management plane and the client plane.
//
// Owning every tenant is what lets the first administrator reach the whole
// installation from the console. It is not permission to sign in to the
// applications running inside those tenants: ClientForAuthorization answers for a
// user_application_access row and nothing else, so a claim that inserted one per
// application would ship every installation with a standing administrator
// account inside every customer application. Access to an application is an
// explicit grant, and this test also pins that such a grant is still possible on
// purpose, because an administrator is allowed to attach themselves.
func TestSystemAdministratorIsNotAutomaticallyInsideTenantApplications(t *testing.T) {
s, _, ctx := integrationStore(t)
// Two tenants, each with a client configured, so that "every application" is
// not satisfied by a single row.
firstOwner := newUser(t, s, ctx, "first@example.test")
first := newTenant(t, s, ctx, firstOwner, "nmbs")
secondOwner := newUser(t, s, ctx, "second@example.test")
second := newTenant(t, s, ctx, secondOwner, "ypto")
const redirect = "https://app.example.test/callback"
application := func(owner, tenantID, slug string) store.Application {
t.Helper()
created, err := s.CreateApplication(ctx, owner, store.Application{
TenantID: tenantID, Slug: slug, Name: slug, AllowedHosts: []string{},
})
if err != nil {
t.Fatalf("create application %s: %v", slug, err)
}
if _, err := s.ConfigureClient(ctx, owner, created.ID, []string{redirect}, secretHash(slug)); err != nil {
t.Fatalf("configure client login for %s: %v", slug, err)
}
return created
}
firstApp := application(firstOwner, first.ID, "guess")
secondApp := application(secondOwner, second.ID, "tell")
admin := newUser(t, s, ctx, "admin@example.test")
claimed, err := s.ClaimFirstSystemAdministrator(ctx, admin)
if err != nil {
t.Fatalf("claim first system administrator: %v", err)
}
if !claimed {
t.Fatal("the first user to sign in must become the system administrator")
}
// The management plane still reaches everything: the claim owns both tenants,
// which is what the console needs to administer them.
console, err := s.HasConsoleAccess(ctx, admin)
if err != nil {
t.Fatalf("HasConsoleAccess: %v", err)
}
if !console {
t.Error("a system administrator must have console access")
}
tenants, err := s.ListTenants(ctx, admin)
if err != nil {
t.Fatalf("ListTenants: %v", err)
}
if len(tenants) != 2 {
t.Errorf("the administrator owns %d tenants, want 2", len(tenants))
}
// The client plane does not. No application was granted, so neither endpoint
// that mints or resolves a hosted-login session will recognize them.
for _, test := range []struct {
name string
application store.Application
}{
{name: "nmbs", application: firstApp},
{name: "ypto", application: secondApp},
} {
t.Run(test.name, func(t *testing.T) {
if _, err := s.ClientForAuthorization(ctx, test.application.ClientID, redirect, admin); !errors.Is(err, store.ErrNotFound) {
t.Fatalf("ClientForAuthorization = %v, want ErrNotFound: owning a tenant must not open its applications", err)
}
if _, err := s.ClientUserByToken(ctx, secretHash("no-session")); !errors.Is(err, store.ErrNotFound) {
t.Fatalf("ClientUserByToken = %v, want ErrNotFound", err)
}
})
}
// Attaching the administrator to one application is allowed, and it is the only
// way in. They hold users.manage in every tenant by virtue of owning it, which
// is what makes the deliberate act possible.
ownerRole, err := s.SystemRoleID(ctx, "owner")
if err != nil {
t.Fatalf("look up the owner role: %v", err)
}
if err := s.SetUserAccess(ctx, admin, first.ID, admin, ownerRole, []string{firstApp.ID}); err != nil {
t.Fatalf("an administrator must be able to attach themselves to an application: %v", err)
}
if _, err := s.ClientForAuthorization(ctx, firstApp.ClientID, redirect, admin); err != nil {
t.Fatalf("ClientForAuthorization after an explicit grant = %v, want the application", err)
}
// Withdrawing it takes effect immediately and leaves the console untouched.
if err := s.SetUserAccess(ctx, admin, first.ID, admin, ownerRole, nil); err != nil {
t.Fatalf("withdraw the grant: %v", err)
}
if _, err := s.ClientForAuthorization(ctx, firstApp.ClientID, redirect, admin); !errors.Is(err, store.ErrNotFound) {
t.Fatalf("ClientForAuthorization after withdrawal = %v, want ErrNotFound", err)
}
console, err = s.HasConsoleAccess(ctx, admin)
if err != nil {
t.Fatalf("HasConsoleAccess: %v", err)
}
if !console {
t.Error("withdrawing an application grant must not cost the administrator their console")
}
}