122 lines
4.8 KiB
Go
122 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")
|
||
|
|
}
|
||
|
|
}
|