verify / verify (push) Successful in 4m25s
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
176 lines
8.2 KiB
Go
176 lines
8.2 KiB
Go
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package authsqlite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"gamertan.com/web/organizations"
|
|
)
|
|
|
|
type ownedOperation struct {
|
|
name, action string
|
|
apply func(context.Context, roleSetFixture, string, string) error
|
|
}
|
|
|
|
func ownedOperations() []ownedOperation {
|
|
return []ownedOperation{
|
|
{"profile", "organization.update", func(ctx context.Context, f roleSetFixture, actor, _ string) error {
|
|
_, err := f.organizations.UpdateOwnedOrganization(ctx, organizations.UpdateOrganization{ID: f.org.ID, Slug: "updated-business", Name: "Updated business", ActorUserID: actor, ExpectedRevision: 1, RequestID: "request-profile"})
|
|
return err
|
|
}},
|
|
{"suspend", "membership.suspended", func(ctx context.Context, f roleSetFixture, actor, target string) error {
|
|
return f.organizations.ChangeOwnedMembershipStatus(ctx, organizations.MembershipStatusChange{OrganizationID: f.org.ID, UserID: target, ActorUserID: actor, ExpectedStatus: "active", Status: "suspended", RequestID: "request-status"})
|
|
}},
|
|
{"remove", "membership.remove", func(ctx context.Context, f roleSetFixture, actor, target string) error {
|
|
return f.organizations.RemoveOwnedMembershipIfCurrent(ctx, organizations.MembershipRemoval{OrganizationID: f.org.ID, UserID: target, ActorUserID: actor, ExpectedStatus: "active", RequestID: "request-remove"})
|
|
}},
|
|
}
|
|
}
|
|
|
|
func TestOwnedManagementRechecksActorInWriteTransaction(t *testing.T) {
|
|
for _, operation := range ownedOperations() {
|
|
for _, change := range []struct{ name, sql string }{
|
|
{"role revoked", `UPDATE gwf_access_bindings SET revoked_at=2100 WHERE subject_id='customer-12345'`},
|
|
{"role narrowed", `UPDATE gwf_access_bindings SET project_id=(SELECT id FROM gwf_projects LIMIT 1) WHERE subject_id='customer-12345'`},
|
|
{"actor suspended", `UPDATE gwf_organization_memberships SET status='suspended' WHERE user_id='customer-12345'`},
|
|
{"actor removed", `DELETE FROM gwf_organization_memberships WHERE user_id='customer-12345'`},
|
|
{"account disabled", `UPDATE gwf_users SET status='disabled' WHERE id='customer-12345'`},
|
|
{"registration incomplete", `UPDATE gwf_users SET registration_pending=1 WHERE id='customer-12345'`},
|
|
{"organization archived", `UPDATE gwf_organizations SET status='archived'`},
|
|
{"personal organization", `UPDATE gwf_organizations SET personal=1,personal_owner_user_id='customer-12345'`},
|
|
} {
|
|
t.Run(operation.name+"/"+change.name, func(t *testing.T) {
|
|
f := newRoleSetFixture(t)
|
|
f.addMember(t)
|
|
if change.name == "role narrowed" {
|
|
if _, err := f.organizations.CreateProject(t.Context(), organizations.CreateProject{OrganizationID: f.org.ID, Slug: "project", Name: "Project"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Model a change committed after the caller displayed/authorized the
|
|
// operation. The repository must not rely on that earlier decision.
|
|
if _, err := f.store.db.Exec(change.sql); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := operation.apply(t.Context(), f, roleOwner, roleMember); err == nil {
|
|
t.Fatal("stale owner authority accepted")
|
|
}
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_access_audit_events WHERE action=?`, operation.action, 0)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_memberships WHERE user_id=? AND status='active'`, roleMember, 1)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organizations WHERE id=? AND revision=1`, f.org.ID, 1)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOwnedManagementDoesNotInheritDelegatedAdministratorSemantics(t *testing.T) {
|
|
for _, operation := range ownedOperations() {
|
|
t.Run(operation.name, func(t *testing.T) {
|
|
f := newRoleSetFixture(t)
|
|
f.addMember(t)
|
|
if err := operation.apply(t.Context(), f, roleMember, roleMember); !errors.Is(err, organizations.ErrOwnerAuthority) {
|
|
t.Fatalf("non-owner management: %v", err)
|
|
}
|
|
if err := operation.apply(t.Context(), f, roleOwner, roleMember); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_access_audit_events WHERE action=?`, operation.action, 1)
|
|
if err := operation.apply(t.Context(), f, roleOwner, roleMember); err == nil {
|
|
t.Fatal("replayed mutation accepted")
|
|
}
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_access_audit_events WHERE action=?`, operation.action, 1)
|
|
})
|
|
}
|
|
// Legacy callers still authorize non-owner administrative operations in
|
|
// their application policy; the new explicit methods do not alter that API.
|
|
f := newRoleSetFixture(t)
|
|
f.addMember(t)
|
|
err := f.organizations.ChangeMembershipStatus(t.Context(), organizations.MembershipStatusChange{OrganizationID: f.org.ID, UserID: roleMember, ActorUserID: roleMember, ExpectedStatus: "active", Status: "suspended"})
|
|
if err != nil {
|
|
t.Fatalf("delegated legacy operation changed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOwnedMembershipPreservesLastOwnerAndRestoresSuspendedMember(t *testing.T) {
|
|
f := newRoleSetFixture(t)
|
|
f.addMember(t)
|
|
for _, operation := range ownedOperations()[1:] {
|
|
if err := operation.apply(t.Context(), f, roleOwner, roleOwner); !errors.Is(err, organizations.ErrLastOwner) {
|
|
t.Fatalf("%s last owner: %v", operation.name, err)
|
|
}
|
|
}
|
|
if err := ownedOperations()[1].apply(t.Context(), f, roleOwner, roleMember); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := organizations.MembershipStatusChange{OrganizationID: f.org.ID, UserID: roleMember, ActorUserID: roleOwner, ExpectedStatus: "suspended", Status: "active"}
|
|
if err := f.organizations.ChangeOwnedMembershipStatus(t.Context(), input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := f.organizations.ChangeOwnedMembershipStatus(t.Context(), input); !errors.Is(err, organizations.ErrRevisionConflict) {
|
|
t.Fatalf("stale reactivation: %v", err)
|
|
}
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_memberships WHERE user_id=? AND status='active'`, roleMember, 1)
|
|
}
|
|
|
|
func TestOwnedManagementRollsBackWithAuditFailure(t *testing.T) {
|
|
for _, operation := range ownedOperations() {
|
|
t.Run(operation.name, func(t *testing.T) {
|
|
f := newRoleSetFixture(t)
|
|
f.addMember(t)
|
|
_, pending := f.invite(t, "buyer")
|
|
team, err := f.organizations.CreateTeam(t.Context(), organizations.CreateTeam{OrganizationID: f.org.ID, Slug: "team", Name: "Team", ActorUserID: roleOwner})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = f.organizations.AddTeamMember(t.Context(), team.ID, roleMember, roleOwner); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = f.store.db.Exec(`CREATE TRIGGER reject_owned_audit BEFORE INSERT ON gwf_access_audit_events WHEN NEW.action='` + operation.action + `' BEGIN SELECT RAISE(ABORT,'injected audit failure'); END`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = operation.apply(t.Context(), f, roleOwner, roleMember); err == nil {
|
|
t.Fatal("audit failure accepted")
|
|
}
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_memberships WHERE user_id=? AND status='active'`, roleMember, 1)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_team_members WHERE user_id=?`, roleMember, 1)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_access_bindings WHERE subject_id=? AND revoked_at IS NULL`, roleMember, 1)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organizations WHERE id=? AND revision=1`, f.org.ID, 1)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_organization_invitations WHERE id=? AND revoked_at IS NULL`, pending.ID, 1)
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_access_audit_events WHERE action=?`, operation.action, 0)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConcurrentOwnedManagementHasOneWinner(t *testing.T) {
|
|
for _, operation := range ownedOperations() {
|
|
t.Run(operation.name, func(t *testing.T) {
|
|
f := newRoleSetFixture(t)
|
|
f.addMember(t)
|
|
start, results := make(chan struct{}), make(chan error, 2)
|
|
for range 2 {
|
|
go func() { <-start; results <- operation.apply(t.Context(), f, roleOwner, roleMember) }()
|
|
}
|
|
close(start)
|
|
success, stale := 0, 0
|
|
for range 2 {
|
|
err := <-results
|
|
switch {
|
|
case err == nil:
|
|
success++
|
|
case errors.Is(err, organizations.ErrRevisionConflict), errors.Is(err, organizations.ErrMembershipNotFound):
|
|
stale++
|
|
default:
|
|
t.Fatalf("concurrent mutation: %v", err)
|
|
}
|
|
}
|
|
if success != 1 || stale != 1 {
|
|
t.Fatalf("success=%d stale=%d", success, stale)
|
|
}
|
|
assertCount(t, f.store, `SELECT COUNT(*) FROM gwf_access_audit_events WHERE action=?`, operation.action, 1)
|
|
})
|
|
}
|
|
}
|