Export the reviewed allowlisted snapshot from private source commit 05928cebd01b586cf9e9d4b8c8537a7605a6068c. This records the exact candidate, bounded capacity result, stateful migration scratch requirement, authenticated batch identity proof, and immediate live acceptance evidence. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
345 lines
14 KiB
Go
345 lines
14 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package identity
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"gamertan.com/web/access"
|
|
"gamertan.com/web/auth"
|
|
"gamertan.com/web/organizations"
|
|
)
|
|
|
|
func TestBootstrapSeparatesPlatformAndOrganizationAccess(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
services, err := Open(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer services.Close()
|
|
result, err := services.Bootstrap(context.Background(), BootstrapInput{
|
|
Username: "operator", Email: "operator@example.test",
|
|
DisplayName: "First Operator", Password: "correct horse battery staple",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !result.Organization.Personal || result.Binding.Role != OrganizationOwner {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
token, principal, err := services.Auth.Authenticate(context.Background(), "operator", "correct horse battery staple", time.Hour)
|
|
if err != nil || token == "" || !principal.Has(PermissionPlatformOperate) {
|
|
t.Fatalf("platform session: token_present=%t principal=%+v err=%v", token != "", principal, err)
|
|
}
|
|
decision, err := services.Access.Authorize(context.Background(), result.User.ID, access.Scope{OrganizationID: result.Organization.ID}, PermissionTelemetryQuery)
|
|
if err != nil || !decision.Allowed || decision.Role != OrganizationOwner {
|
|
t.Fatalf("query decision=%+v err=%v", decision, err)
|
|
}
|
|
decision, err = services.Access.Authorize(context.Background(), result.User.ID, access.Scope{OrganizationID: result.Organization.ID}, PermissionTelemetryReadSensitive)
|
|
if err != nil || !decision.Allowed {
|
|
t.Fatalf("sensitive decision=%+v err=%v", decision, err)
|
|
}
|
|
decision, err = services.Access.Authorize(context.Background(), result.User.ID, access.Scope{OrganizationID: result.Organization.ID}, PermissionSchemaManage)
|
|
if err != nil || !decision.Allowed {
|
|
t.Fatalf("schema decision=%+v err=%v", decision, err)
|
|
}
|
|
if _, err = services.Access.Authorize(context.Background(), result.User.ID, access.Scope{OrganizationID: result.Organization.ID}, PermissionPlatformOperate); err == nil {
|
|
t.Fatal("platform permission entered organization access policy")
|
|
}
|
|
project, err := services.Organizations.CreateProject(context.Background(), organizations.CreateProject{OrganizationID: result.Organization.ID, Slug: "eql-helper", Name: "EQL Helper"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
environment, err := services.Organizations.CreateEnvironment(context.Background(), organizations.CreateEnvironment{OrganizationID: result.Organization.ID, ProjectID: project.ID, Slug: "production", Name: "Production"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
application, err := services.Organizations.CreateApplicationService(context.Background(), organizations.CreateApplicationService{OrganizationID: result.Organization.ID, ProjectID: project.ID, EnvironmentID: environment.ID, Slug: "web", Name: "Web"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scope := access.Scope{OrganizationID: result.Organization.ID, ProjectID: project.ID, EnvironmentID: environment.ID, ServiceID: application.ID}
|
|
if err = services.ValidateResourceScope(context.Background(), scope); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scope.ServiceID = "missing1"
|
|
if err = services.ValidateResourceScope(context.Background(), scope); !errors.Is(err, ErrResourceNotFound) {
|
|
t.Fatalf("missing scope err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapIsSingleUse(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
services, err := Open(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer services.Close()
|
|
input := BootstrapInput{Username: "operator", Email: "operator@example.test", DisplayName: "First Operator", Password: "correct horse battery staple"}
|
|
if _, err = services.Bootstrap(context.Background(), input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = services.Bootstrap(context.Background(), BootstrapInput{Username: "second", Email: "second@example.test", DisplayName: "Second Operator", Password: "correct horse battery staple"}); !errors.Is(err, ErrAlreadyBootstrapped) {
|
|
t.Fatalf("second bootstrap err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapCanRequirePasswordChange(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
services, err := Open(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer services.Close()
|
|
result, err := services.Bootstrap(t.Context(), BootstrapInput{
|
|
Username: "operator", Email: "operator@example.test", DisplayName: "First Operator",
|
|
Password: "temporary correct horse battery staple", RequirePasswordChange: true,
|
|
})
|
|
if err != nil || !result.User.PasswordChangeRequired {
|
|
t.Fatalf("result=%+v err=%v", result, err)
|
|
}
|
|
_, principal, err := services.Auth.Authenticate(t.Context(), "operator", "temporary correct horse battery staple", time.Hour)
|
|
if err != nil || !principal.User.PasswordChangeRequired {
|
|
t.Fatalf("principal=%+v err=%v", principal, err)
|
|
}
|
|
}
|
|
|
|
func TestRemoveSecretValidatesAndRemovesOnlyPrivateRegularFile(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "bootstrap-password")
|
|
if err := WriteSecret(path, "temporary secret"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RemoveSecret(path, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("removed path err=%v", err)
|
|
}
|
|
unsafe := filepath.Join(root, "unsafe")
|
|
if err := os.WriteFile(unsafe, []byte("secret\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RemoveSecret(unsafe, false); err == nil {
|
|
t.Fatal("world-readable secret was removed")
|
|
}
|
|
if _, err := os.Stat(unsafe); err != nil {
|
|
t.Fatalf("unsafe file changed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEvidenceRetentionPrunesOnlyExpiredAudit(t *testing.T) {
|
|
ctx := t.Context()
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
services, err := Open(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer services.Close()
|
|
owner, err := services.Bootstrap(ctx, BootstrapInput{Username: "operator", Email: "operator@example.test", DisplayName: "First Operator", Password: "correct horse battery staple"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, 8, 17, 5, 0, 0, 0, time.UTC)
|
|
for _, event := range []struct {
|
|
id string
|
|
created time.Time
|
|
}{{"audit-old", now.Add(-401 * 24 * time.Hour)}, {"audit-current", now.Add(-399 * 24 * time.Hour)}} {
|
|
if _, err = services.control.ExecContext(ctx, `INSERT INTO gwf_audit_events(id,actor_user_id,action,resource_type,resource_id,summary,created_at) VALUES(?,?,?,?,?,?,?)`, event.id, owner.User.ID, "session.test", "user", owner.User.ID, "Test event", event.created.Unix()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = services.control.ExecContext(ctx, `INSERT INTO gwf_access_audit_events(id,organization_id,actor_user_id,action,resource_type,resource_id,summary,created_at) VALUES(?,?,?,?,?,?,?,?)`, "access-"+event.id, owner.Organization.ID, owner.User.ID, "access.test", "organization", owner.Organization.ID, "Test event", event.created.Unix()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
report, err := services.PruneEvidence(ctx, 400, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if report.AuthenticationEvents != 1 || report.OrganizationEvents != 1 {
|
|
t.Fatalf("report=%+v", report)
|
|
}
|
|
for _, table := range []string{"gwf_audit_events", "gwf_access_audit_events"} {
|
|
var count int
|
|
if err = services.control.QueryRowContext(ctx, `SELECT COUNT(*) FROM `+table).Scan(&count); err != nil || count != 1 {
|
|
t.Fatalf("table=%s count=%d err=%v", table, count, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTeamsInvitationsRevocationAndBreakGlassRemainOrganizationScoped(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
services, err := Open(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer services.Close()
|
|
owner, err := services.Bootstrap(ctx, BootstrapInput{Username: "operator", Email: "operator@example.test", DisplayName: "First Operator", Password: "correct horse battery staple"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
member, err := services.Auth.CreateUser(ctx, auth.CreateUser{Username: "responder", Email: "responder@example.test", DisplayName: "Incident Responder", Password: "another correct horse battery staple"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rawInvitation, invitation, err := services.Organizations.Invite(ctx, owner.Organization.ID, member.Email, owner.User.ID, 15*time.Minute)
|
|
if err != nil || rawInvitation == "" || invitation.OrganizationID != owner.Organization.ID {
|
|
t.Fatalf("invitation=%+v token_present=%t err=%v", invitation, rawInvitation != "", err)
|
|
}
|
|
if err = services.Organizations.AcceptInvitation(ctx, rawInvitation, member.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
team, err := services.Organizations.CreateTeam(ctx, organizations.CreateTeam{OrganizationID: owner.Organization.ID, Slug: "responders", Name: "Incident Responders"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = services.Organizations.AddTeamMember(ctx, team.ID, member.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
binding, err := services.Access.Grant(ctx, access.Grant{SubjectKind: access.Team, SubjectID: team.ID, Role: IncidentResponder, Scope: access.Scope{OrganizationID: owner.Organization.ID}, GrantedBy: owner.User.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decision, err := services.Access.Authorize(ctx, member.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionIncidentsManage)
|
|
if err != nil || !decision.Allowed || decision.Source != "role" || decision.Role != IncidentResponder {
|
|
t.Fatalf("team decision=%+v err=%v", decision, err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, owner.User.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionOrganizationManage)
|
|
if err != nil || !decision.Allowed || decision.Role != OrganizationOwner {
|
|
t.Fatalf("owner organization-management decision=%+v err=%v", decision, err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, member.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionOrganizationManage)
|
|
if err != nil || decision.Allowed {
|
|
t.Fatalf("member organization-management decision=%+v err=%v", decision, err)
|
|
}
|
|
cancelToken, cancelInvitation, err := services.Organizations.Invite(ctx, owner.Organization.ID, "cancelled@example.test", owner.User.ID, 15*time.Minute)
|
|
if err != nil || cancelToken == "" {
|
|
t.Fatalf("cancel invitation=%+v token_present=%t err=%v", cancelInvitation, cancelToken != "", err)
|
|
}
|
|
if err = services.CancelUnusedInvitation(ctx, cancelInvitation.Digest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = services.Organizations.AcceptInvitation(ctx, cancelToken, member.ID); err == nil {
|
|
t.Fatal("cancelled invitation remained usable")
|
|
}
|
|
other, err := services.Organizations.CreatePersonalOrganization(ctx, member.ID, member.DisplayName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, owner.User.ID, access.Scope{OrganizationID: other.ID}, PermissionTelemetryQuery)
|
|
if err != nil || decision.Allowed {
|
|
t.Fatalf("cross-organization decision=%+v err=%v", decision, err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, member.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionTelemetryReadSensitive)
|
|
if err != nil || decision.Allowed {
|
|
t.Fatalf("unexpected sensitive decision=%+v err=%v", decision, err)
|
|
}
|
|
breakGlass, err := services.Access.ActivateBreakGlass(ctx, owner.Organization.ID, member.ID, PermissionTelemetryReadSensitive, "Investigate an active incident", "request-12345678", 15*time.Minute)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, member.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionTelemetryReadSensitive)
|
|
if err != nil || !decision.Allowed || decision.Source != "break_glass" {
|
|
t.Fatalf("break-glass decision=%+v err=%v", decision, err)
|
|
}
|
|
audit, err := services.Access.Audit(ctx, owner.Organization.ID, 10)
|
|
if err != nil || len(audit) != 1 || audit[0].Action != "break_glass.activate" || audit[0].ResourceID != owner.Organization.ID {
|
|
t.Fatalf("audit=%+v err=%v", audit, err)
|
|
}
|
|
if _, err = services.control.ExecContext(ctx, `UPDATE gwf_break_glass SET expires_at=? WHERE id=?`, time.Now().Add(-time.Minute).Unix(), breakGlass.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, member.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionTelemetryReadSensitive)
|
|
if err != nil || decision.Allowed {
|
|
t.Fatalf("expired break-glass decision=%+v err=%v", decision, err)
|
|
}
|
|
if err = services.Store.Revoke(ctx, binding.ID, owner.User.ID, time.Now().UTC()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decision, err = services.Access.Authorize(ctx, member.ID, access.Scope{OrganizationID: owner.Organization.ID}, PermissionIncidentsManage)
|
|
if err != nil || decision.Allowed {
|
|
t.Fatalf("revoked team decision=%+v err=%v", decision, err)
|
|
}
|
|
}
|
|
|
|
func TestReadSecretRejectsWeakFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
valid := filepath.Join(dir, "password")
|
|
if err := os.WriteFile(valid, []byte("correct horse battery staple\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, err := ReadSecret(valid, false)
|
|
if err != nil || secret != "correct horse battery staple" {
|
|
t.Fatalf("secret=%q err=%v", secret, err)
|
|
}
|
|
weak := filepath.Join(dir, "weak")
|
|
if err = os.WriteFile(weak, []byte("not private"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = ReadSecret(weak, false); err == nil {
|
|
t.Fatal("world-readable secret accepted")
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
link := filepath.Join(dir, "link")
|
|
if err = os.Symlink(valid, link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = ReadSecret(link, false); err == nil {
|
|
t.Fatal("symlinked secret accepted")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteSecretIsPrivateExclusiveAndReadable(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "invitation")
|
|
const secret = "single-use-invitation-token"
|
|
if err := WriteSecret(path, secret); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("info=%v err=%v", info, err)
|
|
}
|
|
got, err := ReadSecret(path, false)
|
|
if err != nil || got != secret {
|
|
t.Fatalf("secret=%q err=%v", got, err)
|
|
}
|
|
if err = WriteSecret(path, "replacement"); err == nil {
|
|
t.Fatal("existing secret was overwritten")
|
|
}
|
|
if err = WriteSecret(filepath.Join(filepath.Dir(path), "multiline"), "first\nsecond"); err == nil {
|
|
t.Fatal("multiline secret was accepted")
|
|
}
|
|
if runtime.GOOS != "windows" {
|
|
linkedParent := filepath.Join(filepath.Dir(path), "linked-parent")
|
|
if err = os.Symlink(filepath.Dir(path), linkedParent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = WriteSecret(filepath.Join(linkedParent, "through-link"), "secret"); err == nil {
|
|
t.Fatal("symlinked secret output directory was accepted")
|
|
}
|
|
}
|
|
}
|