652 lines
16 KiB
Go
652 lines
16 KiB
Go
package lib
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestCheckAccess(t *testing.T) {
|
|
db, cfg := testDB(t)
|
|
|
|
// Create owner
|
|
ownerID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
owner := &User{
|
|
UserID: ownerID,
|
|
Email: "owner@test.com",
|
|
Name: "Owner",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, owner)
|
|
projectID := testProject(t, db, cfg, ownerID)
|
|
|
|
// IB admin can read
|
|
err := CheckAccessRead(db, ownerID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("IB admin should have read access: %v", err)
|
|
}
|
|
|
|
// IB admin can write
|
|
err = CheckAccessWrite(db, ownerID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("IB admin should have write access: %v", err)
|
|
}
|
|
|
|
// IB admin can delete
|
|
err = CheckAccessDelete(db, ownerID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("IB admin should have delete access: %v", err)
|
|
}
|
|
|
|
// Create seller user
|
|
sellerID := testUser(t, db, cfg, projectID, RoleSellerMember)
|
|
|
|
// Seller can read
|
|
err = CheckAccessRead(db, sellerID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("seller should have read access: %v", err)
|
|
}
|
|
|
|
// Seller can write
|
|
err = CheckAccessWrite(db, sellerID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("seller should have write access: %v", err)
|
|
}
|
|
|
|
// Create buyer user (read-only)
|
|
buyerID := testUser(t, db, cfg, projectID, RoleBuyerMember)
|
|
|
|
// Buyer can read
|
|
err = CheckAccessRead(db, buyerID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("buyer should have read access: %v", err)
|
|
}
|
|
|
|
// Buyer cannot write
|
|
err = CheckAccessWrite(db, buyerID, projectID, "")
|
|
if err != ErrAccessDenied {
|
|
t.Errorf("buyer should NOT have write access, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRoleHierarchy(t *testing.T) {
|
|
// Verify hierarchy levels: buyer < seller_member < seller_admin < ib_analyst < ib_member < ib_admin
|
|
expected := []struct {
|
|
role string
|
|
level int
|
|
}{
|
|
{RoleObserver, 10},
|
|
{RoleBuyerMember, 30},
|
|
{RoleBuyerAdmin, 40},
|
|
{RoleSellerMember, 50},
|
|
{RoleSellerAdmin, 70},
|
|
{RoleIBMember, 80},
|
|
{RoleIBAdmin, 100},
|
|
}
|
|
|
|
for _, tc := range expected {
|
|
level, ok := RoleHierarchy[tc.role]
|
|
if !ok {
|
|
t.Errorf("role %s not in hierarchy", tc.role)
|
|
continue
|
|
}
|
|
if level != tc.level {
|
|
t.Errorf("role %s: expected level %d, got %d", tc.role, tc.level, level)
|
|
}
|
|
}
|
|
|
|
// Verify ordering
|
|
if RoleHierarchy[RoleBuyerMember] >= RoleHierarchy[RoleSellerMember] {
|
|
t.Error("buyer should be lower than seller_member")
|
|
}
|
|
if RoleHierarchy[RoleSellerMember] >= RoleHierarchy[RoleSellerAdmin] {
|
|
t.Error("seller_member should be lower than seller_admin")
|
|
}
|
|
if RoleHierarchy[RoleSellerAdmin] >= RoleHierarchy[RoleIBMember] {
|
|
t.Error("seller_admin should be lower than ib_member")
|
|
}
|
|
if RoleHierarchy[RoleIBMember] >= RoleHierarchy[RoleIBAdmin] {
|
|
t.Error("ib_member should be lower than ib_admin")
|
|
}
|
|
}
|
|
|
|
func TestCanGrantRole(t *testing.T) {
|
|
tests := []struct {
|
|
granter string
|
|
target string
|
|
canDo bool
|
|
}{
|
|
// IB admin can grant anything
|
|
{RoleIBAdmin, RoleIBAdmin, true},
|
|
{RoleIBAdmin, RoleIBMember, true},
|
|
{RoleIBAdmin, RoleSellerAdmin, true},
|
|
{RoleIBAdmin, RoleBuyerMember, true},
|
|
|
|
// IB member can grant lower roles
|
|
{RoleIBMember, RoleIBAdmin, false},
|
|
{RoleIBMember, RoleIBMember, true},
|
|
{RoleIBMember, RoleSellerAdmin, true},
|
|
|
|
// Seller admin can grant seller and buyer roles
|
|
{RoleSellerAdmin, RoleIBMember, false},
|
|
{RoleSellerAdmin, RoleSellerAdmin, true},
|
|
{RoleSellerAdmin, RoleSellerMember, true},
|
|
{RoleSellerAdmin, RoleBuyerMember, true},
|
|
|
|
// Buyer cannot grant higher roles
|
|
{RoleBuyerAdmin, RoleSellerMember, false},
|
|
{RoleBuyerAdmin, RoleBuyerAdmin, true},
|
|
{RoleBuyerAdmin, RoleBuyerMember, true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := CanGrantRole(tc.granter, tc.target)
|
|
if result != tc.canDo {
|
|
t.Errorf("CanGrantRole(%s, %s) = %v, want %v", tc.granter, tc.target, result, tc.canDo)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGrantRevoke(t *testing.T) {
|
|
db, cfg := testDB(t)
|
|
|
|
// Create admin
|
|
adminID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
admin := &User{
|
|
UserID: adminID,
|
|
Email: "admin@test.com",
|
|
Name: "Admin",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, admin)
|
|
projectID := testProject(t, db, cfg, adminID)
|
|
|
|
// Create user with no access
|
|
userID := uuid.New().String()
|
|
user := &User{
|
|
UserID: userID,
|
|
Email: "user@test.com",
|
|
Name: "User",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, user)
|
|
|
|
// Verify no access
|
|
err := CheckAccessRead(db, userID, projectID, "")
|
|
if err != ErrAccessDenied {
|
|
t.Error("user should have no access initially")
|
|
}
|
|
|
|
// Grant access
|
|
accessID := uuid.New().String()
|
|
access := &Access{
|
|
ID: accessID,
|
|
ProjectID: projectID,
|
|
UserID: userID,
|
|
Role: RoleBuyerMember,
|
|
Ops: "r",
|
|
CanGrant: false,
|
|
GrantedBy: adminID,
|
|
GrantedAt: now,
|
|
}
|
|
if err := AccessGrant(db, access); err != nil {
|
|
t.Fatalf("AccessGrant: %v", err)
|
|
}
|
|
|
|
// Verify access granted
|
|
err = CheckAccessRead(db, userID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("user should have read access after grant: %v", err)
|
|
}
|
|
|
|
// Revoke access
|
|
if err := AccessRevoke(db, accessID, adminID); err != nil {
|
|
t.Fatalf("AccessRevoke: %v", err)
|
|
}
|
|
|
|
// Verify access revoked
|
|
err = CheckAccessRead(db, userID, projectID, "")
|
|
if err != ErrAccessDenied {
|
|
t.Error("user should have no access after revoke")
|
|
}
|
|
}
|
|
|
|
func TestIsBuyerRole(t *testing.T) {
|
|
buyers := []string{RoleBuyerAdmin, RoleBuyerMember}
|
|
nonBuyers := []string{RoleIBAdmin, RoleIBMember, RoleSellerAdmin, RoleSellerMember, RoleObserver}
|
|
|
|
for _, role := range buyers {
|
|
if !IsBuyerRole(role) {
|
|
t.Errorf("%s should be buyer role", role)
|
|
}
|
|
}
|
|
|
|
for _, role := range nonBuyers {
|
|
if IsBuyerRole(role) {
|
|
t.Errorf("%s should NOT be buyer role", role)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetUserHighestRole(t *testing.T) {
|
|
db, cfg := testDB(t)
|
|
|
|
// Create admin and project
|
|
adminID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
admin := &User{
|
|
UserID: adminID,
|
|
Email: "admin@test.com",
|
|
Name: "Admin",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, admin)
|
|
projectID := testProject(t, db, cfg, adminID)
|
|
|
|
// Admin's highest role should be ib_admin
|
|
role, err := GetUserHighestRole(db, adminID, projectID)
|
|
if err != nil {
|
|
t.Fatalf("GetUserHighestRole: %v", err)
|
|
}
|
|
if role != RoleIBAdmin {
|
|
t.Errorf("expected ib_admin, got %s", role)
|
|
}
|
|
|
|
// Create user with multiple roles
|
|
userID := uuid.New().String()
|
|
user := &User{
|
|
UserID: userID,
|
|
Email: "multi@test.com",
|
|
Name: "Multi Role User",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, user)
|
|
|
|
// Grant buyer role
|
|
AccessGrant(db, &Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
UserID: userID,
|
|
Role: RoleBuyerMember,
|
|
Ops: "r",
|
|
GrantedBy: adminID,
|
|
GrantedAt: now,
|
|
})
|
|
|
|
// Grant seller role (higher)
|
|
AccessGrant(db, &Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
UserID: userID,
|
|
Role: RoleSellerMember,
|
|
Ops: "rw",
|
|
GrantedBy: adminID,
|
|
GrantedAt: now,
|
|
})
|
|
|
|
// Highest should be seller_member
|
|
role, err = GetUserHighestRole(db, userID, projectID)
|
|
if err != nil {
|
|
t.Fatalf("GetUserHighestRole: %v", err)
|
|
}
|
|
if role != RoleSellerMember {
|
|
t.Errorf("expected seller_member (highest), got %s", role)
|
|
}
|
|
|
|
// User with no access
|
|
noAccessID := uuid.New().String()
|
|
noAccessUser := &User{
|
|
UserID: noAccessID,
|
|
Email: "noaccess@test.com",
|
|
Name: "No Access",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, noAccessUser)
|
|
|
|
_, err = GetUserHighestRole(db, noAccessID, projectID)
|
|
if err != ErrAccessDenied {
|
|
t.Errorf("expected ErrAccessDenied for user with no access, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWorkstreamAccess(t *testing.T) {
|
|
db, cfg := testDB(t)
|
|
|
|
// Create admin and project
|
|
adminID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
admin := &User{
|
|
UserID: adminID,
|
|
Email: "admin@test.com",
|
|
Name: "Admin",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, admin)
|
|
projectID := testProject(t, db, cfg, adminID)
|
|
|
|
// Create user with access to specific workstream only
|
|
userID := uuid.New().String()
|
|
user := &User{
|
|
UserID: userID,
|
|
Email: "ws@test.com",
|
|
Name: "Workstream User",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, user)
|
|
|
|
workstreamID := "workstream-1"
|
|
AccessGrant(db, &Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
WorkstreamID: workstreamID,
|
|
UserID: userID,
|
|
Role: RoleSellerMember,
|
|
Ops: "rw",
|
|
GrantedBy: adminID,
|
|
GrantedAt: now,
|
|
})
|
|
|
|
// User has access to their workstream
|
|
_, err := CheckAccess(db, userID, projectID, workstreamID, "r")
|
|
if err != nil {
|
|
t.Errorf("user should have access to their workstream: %v", err)
|
|
}
|
|
|
|
// User should NOT have access to different workstream
|
|
_, err = CheckAccess(db, userID, projectID, "different-workstream", "r")
|
|
if err != ErrAccessDenied {
|
|
t.Error("user should NOT have access to different workstream")
|
|
}
|
|
}
|
|
|
|
// ---- Additional tests for super_admin, IsSuperAdmin, and ValidateOrgDomain ----
|
|
|
|
func TestSuperAdminBypassesCheckAccess(t *testing.T) {
|
|
db, cfg := testDB(t)
|
|
|
|
// Create a regular user who owns a project
|
|
ownerID := uuid.New().String()
|
|
now := time.Now().UnixMilli()
|
|
owner := &User{
|
|
UserID: ownerID,
|
|
Email: "owner@test.com",
|
|
Name: "Owner",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, owner)
|
|
projectID := testProject(t, db, cfg, ownerID)
|
|
|
|
// Create super_admin user (no explicit project access)
|
|
superAdminID := uuid.New().String()
|
|
superAdmin := &User{
|
|
UserID: superAdminID,
|
|
Email: "superadmin@test.com",
|
|
Name: "Super Admin",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, superAdmin)
|
|
|
|
// Grant super_admin role on a dummy project (super_admin is global but stored per project)
|
|
dummyProjectID := uuid.New().String()
|
|
AccessGrant(db, &Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: dummyProjectID,
|
|
UserID: superAdminID,
|
|
Role: RoleSuperAdmin,
|
|
Ops: "rwdm",
|
|
CanGrant: true,
|
|
GrantedBy: "system",
|
|
GrantedAt: now,
|
|
})
|
|
|
|
// Verify super_admin is recognized
|
|
isSA, err := IsSuperAdmin(db, superAdminID)
|
|
if err != nil {
|
|
t.Fatalf("IsSuperAdmin failed: %v", err)
|
|
}
|
|
if !isSA {
|
|
t.Error("User with super_admin role should be recognized as super admin")
|
|
}
|
|
|
|
// super_admin should have full access to any project without explicit grant
|
|
access, err := CheckAccess(db, superAdminID, projectID, "", "r")
|
|
if err != nil {
|
|
t.Errorf("super_admin should have read access: %v", err)
|
|
}
|
|
if access == nil {
|
|
t.Fatal("CheckAccess should return an Access object for super_admin")
|
|
}
|
|
if access.Role != RoleSuperAdmin {
|
|
t.Errorf("Access role should be super_admin, got %s", access.Role)
|
|
}
|
|
if access.Ops != "rwdm" {
|
|
t.Errorf("super_admin should have rwdm ops, got %s", access.Ops)
|
|
}
|
|
|
|
// Test all operations
|
|
_, err = CheckAccess(db, superAdminID, projectID, "", "w")
|
|
if err != nil {
|
|
t.Errorf("super_admin should have write access: %v", err)
|
|
}
|
|
|
|
_, err = CheckAccess(db, superAdminID, projectID, "", "d")
|
|
if err != nil {
|
|
t.Errorf("super_admin should have delete access: %v", err)
|
|
}
|
|
|
|
_, err = CheckAccess(db, superAdminID, projectID, "", "m")
|
|
if err != nil {
|
|
t.Errorf("super_admin should have manage access: %v", err)
|
|
}
|
|
|
|
// Test convenience functions
|
|
err = CheckAccessRead(db, superAdminID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("super_admin CheckAccessRead should pass: %v", err)
|
|
}
|
|
|
|
err = CheckAccessWrite(db, superAdminID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("super_admin CheckAccessWrite should pass: %v", err)
|
|
}
|
|
|
|
err = CheckAccessDelete(db, superAdminID, projectID, "")
|
|
if err != nil {
|
|
t.Errorf("super_admin CheckAccessDelete should pass: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIsSuperAdmin(t *testing.T) {
|
|
db, _ := testDB(t)
|
|
now := time.Now().UnixMilli()
|
|
|
|
// Create regular user
|
|
regularID := uuid.New().String()
|
|
regular := &User{
|
|
UserID: regularID,
|
|
Email: "regular@test.com",
|
|
Name: "Regular User",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, regular)
|
|
|
|
// Create super admin user
|
|
superID := uuid.New().String()
|
|
superUser := &User{
|
|
UserID: superID,
|
|
Email: "super@test.com",
|
|
Name: "Super User",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, superUser)
|
|
|
|
// Grant super_admin role
|
|
AccessGrant(db, &Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: "any-project",
|
|
UserID: superID,
|
|
Role: RoleSuperAdmin,
|
|
Ops: "rwdm",
|
|
CanGrant: true,
|
|
GrantedBy: "system",
|
|
GrantedAt: now,
|
|
})
|
|
|
|
// Test IsSuperAdmin
|
|
isSA, err := IsSuperAdmin(db, superID)
|
|
if err != nil {
|
|
t.Fatalf("IsSuperAdmin failed: %v", err)
|
|
}
|
|
if !isSA {
|
|
t.Error("User with super_admin role should return true")
|
|
}
|
|
|
|
isRegularSA, err := IsSuperAdmin(db, regularID)
|
|
if err != nil {
|
|
t.Fatalf("IsSuperAdmin for regular user failed: %v", err)
|
|
}
|
|
if isRegularSA {
|
|
t.Error("Regular user should not be super admin")
|
|
}
|
|
|
|
// Test with ib_admin (should NOT be super admin)
|
|
ibAdminID := uuid.New().String()
|
|
ibAdmin := &User{
|
|
UserID: ibAdminID,
|
|
Email: "ibadmin@test.com",
|
|
Name: "IB Admin",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, ibAdmin)
|
|
AccessGrant(db, &Access{
|
|
ID: uuid.New().String(),
|
|
ProjectID: "test-project",
|
|
UserID: ibAdminID,
|
|
Role: RoleIBAdmin,
|
|
Ops: "rwdm",
|
|
CanGrant: true,
|
|
GrantedBy: "system",
|
|
GrantedAt: now,
|
|
})
|
|
|
|
isIBSA, err := IsSuperAdmin(db, ibAdminID)
|
|
if err != nil {
|
|
t.Fatalf("IsSuperAdmin for ib_admin failed: %v", err)
|
|
}
|
|
if isIBSA {
|
|
t.Error("ib_admin should NOT be super admin")
|
|
}
|
|
}
|
|
|
|
func TestIsSuperAdminRevokedGrant(t *testing.T) {
|
|
db, _ := testDB(t)
|
|
now := time.Now().UnixMilli()
|
|
|
|
// Create user
|
|
userID := uuid.New().String()
|
|
user := &User{
|
|
UserID: userID,
|
|
Email: "revoke-test@test.com",
|
|
Name: "Revoke Test",
|
|
Password: "$2a$10$test",
|
|
Active: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
UserCreate(db, user)
|
|
|
|
// Grant and then revoke super_admin
|
|
accessID := uuid.New().String()
|
|
AccessGrant(db, &Access{
|
|
ID: accessID,
|
|
ProjectID: "any-project",
|
|
UserID: userID,
|
|
Role: RoleSuperAdmin,
|
|
Ops: "rwdm",
|
|
CanGrant: true,
|
|
GrantedBy: "system",
|
|
GrantedAt: now,
|
|
})
|
|
|
|
// Should be super admin
|
|
isSA, _ := IsSuperAdmin(db, userID)
|
|
if !isSA {
|
|
t.Error("Should be super admin before revocation")
|
|
}
|
|
|
|
// Revoke the grant
|
|
AccessRevoke(db, accessID, "system")
|
|
|
|
// Should no longer be super admin
|
|
isSA, _ = IsSuperAdmin(db, userID)
|
|
if isSA {
|
|
t.Error("Should NOT be super admin after revocation")
|
|
}
|
|
}
|
|
|
|
func TestSuperAdminCanGrantAnyRole(t *testing.T) {
|
|
// super_admin (level 200) should be able to grant any role
|
|
tests := []struct {
|
|
target string
|
|
canDo bool
|
|
}{
|
|
{RoleSuperAdmin, true},
|
|
{RoleIBAdmin, true},
|
|
{RoleIBMember, true},
|
|
{RoleSellerAdmin, true},
|
|
{RoleSellerMember, true},
|
|
{RoleBuyerAdmin, true},
|
|
{RoleBuyerMember, true},
|
|
{RoleObserver, true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := CanGrantRole(RoleSuperAdmin, tc.target)
|
|
if result != tc.canDo {
|
|
t.Errorf("CanGrantRole(super_admin, %s) = %v, want %v", tc.target, result, tc.canDo)
|
|
}
|
|
}
|
|
}
|