187 lines
7.2 KiB
Go
187 lines
7.2 KiB
Go
package repository
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/wmms/wmms/internal/model"
|
|
)
|
|
|
|
// Removing the role enabled predicate, trusting a previous login role, or
|
|
// omitting the organization status filter must expose unauthorized companies.
|
|
func TestOrganizationAccessUsesCurrentEnabledRolesAndGrants(t *testing.T) {
|
|
db := isolationDB(t, false)
|
|
mustSQL(t, db, `INSERT INTO organizations(id,code,name,status) VALUES(2,'TWO','Two','active'),(3,'THREE','Three','inactive');
|
|
INSERT INTO users(id,username,password) VALUES(1,'ordinary','x'),(2,'admin','x'),(3,'disabled-admin','x'),(4,'no-grants','x');
|
|
INSERT INTO user_roles(user_id,role_id) SELECT 2,id FROM roles WHERE code='super_admin';
|
|
INSERT INTO user_roles(user_id,role_id) SELECT 3,id FROM roles WHERE code='super_admin';
|
|
INSERT INTO roles(id,code,name,enabled) VALUES(20,'old_admin','Old',0);
|
|
INSERT INTO user_organizations(user_id,organization_id,is_default) VALUES(1,2,1),(1,3,0),(3,2,1);`)
|
|
repo := NewOrganizationRepository()
|
|
access, err := repo.ListAccessible(1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(access.Organizations) != 1 || access.Organizations[0].ID != 2 || access.DefaultOrganizationID == nil || *access.DefaultOrganizationID != 2 {
|
|
t.Fatalf("ordinary access=%+v", access)
|
|
}
|
|
allowed, err := repo.CanAccess(1, 1)
|
|
if err != nil || allowed {
|
|
t.Fatalf("ungranted organization allowed=%v err=%v", allowed, err)
|
|
}
|
|
allowed, err = repo.CanAccess(1, 3)
|
|
if err != nil || allowed {
|
|
t.Fatalf("inactive organization allowed=%v err=%v", allowed, err)
|
|
}
|
|
access, err = repo.ListAccessible(2)
|
|
if err != nil || len(access.Organizations) != 2 || access.DefaultOrganizationID != nil {
|
|
t.Fatalf("ungranted admin access=%+v err=%v", access, err)
|
|
}
|
|
allowed, err = repo.CanAccess(2, 2)
|
|
if err != nil || !allowed {
|
|
t.Fatalf("enabled admin access=%v err=%v", allowed, err)
|
|
}
|
|
mustSQL(t, db, `DELETE FROM user_organizations WHERE user_id=1`)
|
|
access, err = repo.ListAccessible(1)
|
|
if err != nil || len(access.Organizations) != 0 || access.DefaultOrganizationID != nil {
|
|
t.Fatalf("revoked grant access=%+v err=%v", access, err)
|
|
}
|
|
allowed, err = repo.CanAccess(1, 2)
|
|
if err != nil || allowed {
|
|
t.Fatalf("revoked grant allowed=%v err=%v", allowed, err)
|
|
}
|
|
mustSQL(t, db, `UPDATE roles SET enabled=0 WHERE code='super_admin'`)
|
|
access, err = repo.ListAccessible(2)
|
|
if err != nil || len(access.Organizations) != 0 {
|
|
t.Fatalf("disabled admin access=%+v err=%v", access, err)
|
|
}
|
|
allowed, err = repo.CanAccess(2, 2)
|
|
if err != nil || allowed {
|
|
t.Fatalf("disabled admin allowed=%v err=%v", allowed, err)
|
|
}
|
|
access, err = repo.ListAccessible(3)
|
|
if err != nil || len(access.Organizations) != 1 || access.Organizations[0].ID != 2 {
|
|
t.Fatalf("ordinary grant with disabled role access=%+v err=%v", access, err)
|
|
}
|
|
allowed, err = repo.CanAccess(999, 2)
|
|
if err != nil || allowed {
|
|
t.Fatalf("nonexistent user allowed=%v err=%v", allowed, err)
|
|
}
|
|
}
|
|
|
|
// An audit failure must reach the caller so its business mutation can roll back;
|
|
// a rollback must remove both records, and reads must require an organization.
|
|
func TestOrganizationAuditTransactionAndScope(t *testing.T) {
|
|
db := isolationDB(t, false)
|
|
mustSQL(t, db, `INSERT INTO users(id,username,password) VALUES(1,'actor','x'); INSERT INTO organizations(id,code,name) VALUES(2,'TWO','Two')`)
|
|
repo := NewOrganizationAuditRepository()
|
|
org1, org2 := int64(1), int64(2)
|
|
entry := &model.OrganizationAuditLog{ActorUserID: 1, OrganizationID: &org1, Action: "organization.switch", OldValue: json.RawMessage(`{"organization_id":2}`), NewValue: json.RawMessage(`{"organization_id":1}`)}
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tx.Rollback()
|
|
mustSQL(t, db, `UPDATE organizations SET name='Before' WHERE id=1`)
|
|
if _, err = tx.Exec(`UPDATE organizations SET name='Rolled back' WHERE id=1`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = repo.CreateTx(tx, entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = tx.Rollback(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
logs, err := repo.ListByOrganization(1, 20, 0)
|
|
if err != nil || len(logs) != 0 {
|
|
t.Fatalf("rolled back audit=%+v err=%v", logs, err)
|
|
}
|
|
var organizationName string
|
|
if err = db.QueryRow(`SELECT name FROM organizations WHERE id=1`).Scan(&organizationName); err != nil || organizationName != "Before" {
|
|
t.Fatalf("business rollback=%s err=%v", organizationName, err)
|
|
}
|
|
tx, err = db.Begin()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err = tx.Exec(`UPDATE organizations SET name='Audit failed' WHERE id=1`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entry.NewValue = json.RawMessage(`not-json`)
|
|
if err = repo.CreateTx(tx, entry); err == nil {
|
|
t.Fatal("invalid audit JSON was accepted")
|
|
}
|
|
tx.Rollback()
|
|
if err = db.QueryRow(`SELECT name FROM organizations WHERE id=1`).Scan(&organizationName); err != nil || organizationName != "Before" {
|
|
t.Fatalf("audit failure business rollback=%s err=%v", organizationName, err)
|
|
}
|
|
entry.NewValue = json.RawMessage(`{"organization_id":1}`)
|
|
tx, err = db.Begin()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err = tx.Exec(`UPDATE organizations SET name='Committed' WHERE id=1`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = repo.CreateTx(tx, entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = db.QueryRow(`SELECT name FROM organizations WHERE id=1`).Scan(&organizationName); err != nil || organizationName != "Committed" {
|
|
t.Fatalf("business commit=%s err=%v", organizationName, err)
|
|
}
|
|
entry.OrganizationID = &org2
|
|
entry.Action = "organization.grants.update"
|
|
entry.OldValue = json.RawMessage(`{"organization_ids":[1],"default_organization_id":1}`)
|
|
entry.NewValue = json.RawMessage(`{"organization_ids":[2],"default_organization_id":2}`)
|
|
if err = repo.Create(entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entry.OrganizationID = nil
|
|
entry.Action = "organization.grants.update"
|
|
if err = repo.Create(entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
logs, err = repo.ListByOrganization(1, 20, 0)
|
|
if err != nil || len(logs) != 1 || logs[0].Action != "organization.switch" {
|
|
t.Fatalf("scoped audit=%+v err=%v", logs, err)
|
|
}
|
|
var newValue struct {
|
|
OrganizationID int64 `json:"organization_id"`
|
|
}
|
|
if err = json.Unmarshal(logs[0].NewValue, &newValue); err != nil || newValue.OrganizationID != 1 {
|
|
t.Fatalf("audit value=%s err=%v", logs[0].NewValue, err)
|
|
}
|
|
if _, err = repo.ListByOrganization(0, 20, 0); err == nil {
|
|
t.Fatal("unscoped audit read accepted")
|
|
}
|
|
if err = repo.CreateTx(nil, entry); err == nil {
|
|
t.Fatal("nil transaction accepted")
|
|
}
|
|
}
|
|
|
|
func TestOrganizationRepositoryPropagatesDatabaseFailures(t *testing.T) {
|
|
db := isolationDB(t, false)
|
|
if err := db.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repo := NewOrganizationRepository()
|
|
if _, err := repo.ListAccessible(1); err == nil {
|
|
t.Fatal("ListAccessible swallowed database failure")
|
|
}
|
|
if allowed, err := repo.CanAccess(1, 1); err == nil || allowed {
|
|
t.Fatalf("CanAccess allowed=%v err=%v", allowed, err)
|
|
}
|
|
audit := NewOrganizationAuditRepository()
|
|
if err := audit.Create(&model.OrganizationAuditLog{ActorUserID: 1, Action: "test"}); err == nil {
|
|
t.Fatal("Create swallowed database failure")
|
|
}
|
|
if _, err := audit.ListByOrganization(1, 20, 0); err == nil {
|
|
t.Fatal("audit read swallowed database failure")
|
|
}
|
|
}
|