1
0
forked from baron/baron-sso

feat: enforce tenant isolation for audit logs and enhance user list filtering for multi-tenant admins

This commit is contained in:
2026-03-04 14:12:39 +09:00
parent 9da97554ce
commit 39b41a4c42
7 changed files with 78 additions and 16 deletions

View File

@@ -58,6 +58,8 @@ func (h *AuditHandler) CreateLog(c *fiber.Ctx) error {
func (h *AuditHandler) ListLogs(c *fiber.Ctx) error {
limit := c.QueryInt("limit", 50)
cursorRaw := c.Query("cursor")
requestedTenantID := c.Query("tenantId")
cursor, err := parseAuditCursor(cursorRaw)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, "Invalid cursor")
@@ -67,7 +69,41 @@ func (h *AuditHandler) ListLogs(c *fiber.Ctx) error {
return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable")
}
logs, err := h.repo.FindPage(c.Context(), limit+1, cursor)
// [New] Role-based Filtering
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
var filterTenantID string
if profile != nil {
if profile.Role == domain.RoleSuperAdmin {
// Super Admin can see everything or filter by a specific tenant if requested
filterTenantID = requestedTenantID
} else if profile.Role == domain.RoleTenantAdmin {
// Tenant Admin can only see their own tenant logs (or manageable ones)
// For now, lock to their primary tenant or requested one IF it's in their manageable list
if profile.TenantID != nil {
filterTenantID = *profile.TenantID
}
// If they requested a specific tenant, verify they can manage it
if requestedTenantID != "" && requestedTenantID != filterTenantID {
canManage := false
for _, t := range profile.ManageableTenants {
if t.ID == requestedTenantID {
canManage = true
break
}
}
if !canManage {
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot view logs for this tenant")
}
filterTenantID = requestedTenantID
}
} else {
return errorJSON(c, fiber.StatusForbidden, "forbidden")
}
}
logs, err := h.repo.FindPage(c.Context(), limit+1, cursor, filterTenantID)
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs")
}

View File

@@ -3065,7 +3065,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
currentCursor := cursor
const maxBatches = 10
for batch := 0; batch < maxBatches && len(authLogs) < fetchLimit; batch++ {
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor)
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor, "")
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs")
}

View File

@@ -84,7 +84,7 @@ func (m *mockAuditRepo) Create(log *domain.AuditLog) error {
return nil
}
func (m *mockAuditRepo) FindPage(ctx context.Context, limit int, cursor *domain.AuditCursor) ([]domain.AuditLog, error) {
func (m *mockAuditRepo) FindPage(ctx context.Context, limit int, cursor *domain.AuditCursor, tenantID string) ([]domain.AuditLog, error) {
return m.logs, nil
}

View File

@@ -1253,7 +1253,7 @@ func (h *DevHandler) ListAuditLogs(c *fiber.Ctx) error {
const maxScan = 3000
for len(collected) < limit+1 && scanned < maxScan {
page, findErr := h.AuditRepo.FindPage(c.Context(), pageSize, nextCursor)
page, findErr := h.AuditRepo.FindPage(c.Context(), pageSize, nextCursor, tenantFilter)
if findErr != nil {
return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs")
}

View File

@@ -70,10 +70,8 @@ type userListResponse struct {
func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// [New] Get requester profile from middleware
var requesterRole string
var requesterCompany string
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok {
requesterRole = profile.Role
requesterCompany = profile.CompanyCode
}
limit := c.QueryInt("limit", 50)
@@ -88,6 +86,21 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
offset = 0
}
// [New] Manageable Tenants Map for efficient lookup
manageableSlugs := make(map[string]bool)
if requesterRole == domain.RoleTenantAdmin {
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
if profile != nil {
for _, t := range profile.ManageableTenants {
manageableSlugs[strings.ToLower(t.Slug)] = true
}
// Include primary tenant slug if not already there
if profile.CompanyCode != "" {
manageableSlugs[strings.ToLower(profile.CompanyCode)] = true
}
}
}
// 1. Try Kratos First
identities, err := h.KratosAdmin.ListIdentities(c.Context())
if err == nil {
@@ -97,11 +110,11 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
for _, identity := range identities {
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
compCode := extractTraitString(identity.Traits, "companyCode")
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
// Tenant Admin filtering
if requesterRole == domain.RoleTenantAdmin {
if requesterCompany == "" || !strings.EqualFold(compCode, requesterCompany) {
if !manageableSlugs[compCode] {
continue
}
}