forked from baron/baron-sso
refactoring
This commit is contained in:
@@ -261,9 +261,6 @@ func identityTenantAccessKeys(traits map[string]interface{}) []string {
|
||||
if tenantID := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "tenant_id"))); tenantID != "" {
|
||||
keys = append(keys, tenantID)
|
||||
}
|
||||
if legacySlug := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "companyCode"))); legacySlug != "" {
|
||||
keys = append(keys, legacySlug)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -276,6 +273,60 @@ func anyTenantKeyAllowed(keys []string, allowed map[string]bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func profileTenantAccessKeys(profile *domain.UserProfileResponse) map[string]bool {
|
||||
allowed := make(map[string]bool)
|
||||
if profile == nil {
|
||||
return allowed
|
||||
}
|
||||
if profile.TenantID != nil {
|
||||
if id := strings.ToLower(strings.TrimSpace(*profile.TenantID)); id != "" {
|
||||
allowed[id] = true
|
||||
}
|
||||
}
|
||||
for _, tenant := range profile.ManageableTenants {
|
||||
if id := strings.ToLower(strings.TrimSpace(tenant.ID)); id != "" {
|
||||
allowed[id] = true
|
||||
}
|
||||
if slug := strings.ToLower(strings.TrimSpace(tenant.Slug)); slug != "" {
|
||||
allowed[slug] = true
|
||||
}
|
||||
}
|
||||
for _, tenant := range profile.JoinedTenants {
|
||||
if id := strings.ToLower(strings.TrimSpace(tenant.ID)); id != "" {
|
||||
allowed[id] = true
|
||||
}
|
||||
if slug := strings.ToLower(strings.TrimSpace(tenant.Slug)); slug != "" {
|
||||
allowed[slug] = true
|
||||
}
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
func profileCanAccessTenant(profile *domain.UserProfileResponse, tenantID, tenantSlug string) bool {
|
||||
allowed := profileTenantAccessKeys(profile)
|
||||
if id := strings.ToLower(strings.TrimSpace(tenantID)); id != "" && allowed[id] {
|
||||
return true
|
||||
}
|
||||
if slug := strings.ToLower(strings.TrimSpace(tenantSlug)); slug != "" && allowed[slug] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func userTenantID(user domain.User) string {
|
||||
if user.TenantID == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*user.TenantID)
|
||||
}
|
||||
|
||||
func userTenantSlug(user domain.User) string {
|
||||
if user.Tenant != nil {
|
||||
return strings.TrimSpace(user.Tenant.Slug)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type userSummary struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
@@ -352,10 +403,6 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
manageableSlugs[strings.ToLower(t.ID)] = true
|
||||
baseTenantIDs = append(baseTenantIDs, t.ID)
|
||||
}
|
||||
// Include primary tenant slug if not already there
|
||||
if profile.CompanyCode != "" {
|
||||
manageableSlugs[strings.ToLower(profile.CompanyCode)] = true
|
||||
}
|
||||
if profile.TenantID != nil {
|
||||
manageableSlugs[strings.ToLower(*profile.TenantID)] = true
|
||||
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
|
||||
@@ -423,21 +470,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 := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
||||
secondaryCodes := extractTraitStringArray(identity.Traits, "companyCodes")
|
||||
|
||||
// Tenant Admin & Member filtering
|
||||
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
|
||||
hasAccess := manageableSlugs[compCode] || manageableSlugs[tID]
|
||||
if !hasAccess && len(secondaryCodes) > 0 {
|
||||
for _, code := range secondaryCodes {
|
||||
if manageableSlugs[strings.ToLower(code)] {
|
||||
hasAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
hasAccess := manageableSlugs[tID]
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
@@ -445,34 +482,16 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Dedicated tenantSlug filter
|
||||
if tenantSlug != "" {
|
||||
matches := strings.EqualFold(compCode, tenantSlug) || tID == targetTenantID
|
||||
if !matches && len(secondaryCodes) > 0 {
|
||||
for _, code := range secondaryCodes {
|
||||
if strings.EqualFold(code, tenantSlug) {
|
||||
matches = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
matches := tID == targetTenantID
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Search filtering (Keyword search in email, name, or companyCode)
|
||||
// Search filtering
|
||||
if search != "" {
|
||||
matchesSearch := strings.Contains(email, searchLower) ||
|
||||
strings.Contains(name, searchLower) ||
|
||||
strings.Contains(strings.ToLower(compCode), searchLower)
|
||||
|
||||
if !matchesSearch && len(secondaryCodes) > 0 {
|
||||
for _, code := range secondaryCodes {
|
||||
if strings.Contains(strings.ToLower(code), searchLower) {
|
||||
matchesSearch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
strings.Contains(name, searchLower)
|
||||
|
||||
if !matchesSearch {
|
||||
continue
|
||||
@@ -560,22 +579,8 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||
|
||||
// Check if the target user's companyCode is in requester's manageable tenants
|
||||
allowed := false
|
||||
for _, t := range requester.ManageableTenants {
|
||||
if strings.ToLower(t.Slug) == compCode {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Also check primary company code
|
||||
if !allowed && strings.ToLower(requester.CompanyCode) == compCode {
|
||||
allowed = true
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
allowedKeys := profileTenantAccessKeys(requester)
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowedKeys) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
|
||||
}
|
||||
}
|
||||
@@ -1043,7 +1048,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Role-based access check
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
if tenantSlug != requester.CompanyCode {
|
||||
if !profileCanAccessTenant(requester, tItem.ID, tenantSlug) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
continue
|
||||
}
|
||||
@@ -1057,11 +1062,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
if appointmentTenantSlug == "" {
|
||||
continue
|
||||
}
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin && appointmentTenantSlug != requester.CompanyCode {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
appointmentFailed = true
|
||||
break
|
||||
}
|
||||
|
||||
appointmentTenant, exists := tenantCache[strings.ToLower(appointmentTenantSlug)]
|
||||
if !exists {
|
||||
@@ -1072,6 +1072,11 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin && !profileCanAccessTenant(requester, appointmentTenant.ID, appointmentTenant.Slug) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
appointmentFailed = true
|
||||
break
|
||||
}
|
||||
|
||||
appointment := make(map[string]any, len(rawAppointment)+3)
|
||||
for key, value := range rawAppointment {
|
||||
@@ -1244,7 +1249,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
Phone: normalizePhoneNumber(item.Phone),
|
||||
Role: role,
|
||||
Status: "active",
|
||||
CompanyCode: tenantSlug,
|
||||
Department: dept,
|
||||
Grade: strings.TrimSpace(item.Grade),
|
||||
AffiliationType: "internal",
|
||||
@@ -1376,9 +1380,10 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
if profile != nil && requesterRole == domain.RoleTenantAdmin {
|
||||
for _, t := range profile.ManageableTenants {
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug))
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(t.ID))
|
||||
}
|
||||
if profile.CompanyCode != "" {
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(profile.CompanyCode))
|
||||
if profile.TenantID != nil {
|
||||
manageableSlugs = append(manageableSlugs, strings.ToLower(*profile.TenantID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1396,7 +1401,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
slugMap[s] = true
|
||||
}
|
||||
for _, u := range users {
|
||||
if slugMap[strings.ToLower(u.CompanyCode)] {
|
||||
if slugMap[strings.ToLower(userTenantSlug(u))] || slugMap[strings.ToLower(userTenantID(u))] {
|
||||
filtered = append(filtered, u)
|
||||
}
|
||||
}
|
||||
@@ -1452,7 +1457,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
u.Name,
|
||||
u.Phone,
|
||||
u.Status,
|
||||
u.CompanyCode,
|
||||
userTenantSlug(u),
|
||||
u.Grade,
|
||||
u.Position,
|
||||
u.JobTitle,
|
||||
@@ -1466,7 +1471,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
u.Phone,
|
||||
u.Status,
|
||||
tenantID,
|
||||
u.CompanyCode,
|
||||
userTenantSlug(u),
|
||||
u.Grade,
|
||||
u.Position,
|
||||
u.JobTitle,
|
||||
@@ -1534,7 +1539,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
*req.Role = role
|
||||
}
|
||||
|
||||
// [New] Pre-fetch tenant cache if companyCode is being changed
|
||||
// Pre-fetch tenant cache if tenantSlug is being changed.
|
||||
type tenantCacheItem struct {
|
||||
ID string
|
||||
Schema []interface{}
|
||||
@@ -1543,12 +1548,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
|
||||
manageableSlugs := make(map[string]bool)
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
for _, t := range requester.ManageableTenants {
|
||||
manageableSlugs[strings.ToLower(t.Slug)] = true
|
||||
}
|
||||
if requester.CompanyCode != "" {
|
||||
manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
|
||||
}
|
||||
manageableSlugs = profileTenantAccessKeys(requester)
|
||||
}
|
||||
|
||||
results := make([]map[string]any, 0, len(req.UserIDs))
|
||||
@@ -1576,9 +1576,14 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"})
|
||||
continue
|
||||
}
|
||||
// If changing companyCode, must be to a manageable one
|
||||
if req.CompanyCode != nil {
|
||||
if !manageableSlugs[strings.ToLower(*req.CompanyCode)] {
|
||||
targetAllowed := manageableSlugs[strings.ToLower(*req.CompanyCode)]
|
||||
if !targetAllowed && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
||||
targetAllowed = manageableSlugs[strings.ToLower(tenant.ID)]
|
||||
}
|
||||
}
|
||||
if !targetAllowed {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: target tenant not manageable"})
|
||||
continue
|
||||
}
|
||||
@@ -1646,9 +1651,6 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
if req.Status != nil {
|
||||
localUser.Status = *req.Status
|
||||
}
|
||||
if req.CompanyCode != nil {
|
||||
localUser.CompanyCode = *req.CompanyCode
|
||||
}
|
||||
if req.Department != nil {
|
||||
localUser.Department = *req.Department
|
||||
}
|
||||
@@ -1659,7 +1661,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
localUser.JobTitle = *req.JobTitle
|
||||
}
|
||||
|
||||
// Resolve TenantID if changing companyCode
|
||||
// Resolve TenantID if changing tenantSlug.
|
||||
if req.CompanyCode != nil && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
||||
localUser.TenantID = &tenant.ID
|
||||
@@ -1705,12 +1707,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||
|
||||
manageableSlugs := make(map[string]bool)
|
||||
if requester.Role == domain.RoleTenantAdmin {
|
||||
for _, t := range requester.ManageableTenants {
|
||||
manageableSlugs[strings.ToLower(t.Slug)] = true
|
||||
}
|
||||
if requester.CompanyCode != "" {
|
||||
manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
|
||||
}
|
||||
manageableSlugs = profileTenantAccessKeys(requester)
|
||||
}
|
||||
|
||||
results := make([]map[string]any, 0, len(req.UserIDs))
|
||||
@@ -1802,12 +1799,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if requester.TenantID != nil {
|
||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||
}
|
||||
if requester.CompanyCode != "" {
|
||||
allowed[strings.ToLower(requester.CompanyCode)] = true
|
||||
}
|
||||
for _, tenant := range requester.ManageableTenants {
|
||||
allowed[strings.ToLower(tenant.ID)] = true
|
||||
allowed[strings.ToLower(tenant.Slug)] = true
|
||||
}
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot update user in another tenant")
|
||||
@@ -1855,10 +1848,19 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
*req.Role = role
|
||||
}
|
||||
|
||||
// [New] Tenant Admin restriction: Cannot change companyCode (except when adding/removing secondary membership)
|
||||
// Tenant admins can only move users within tenants they can manage.
|
||||
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
|
||||
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil {
|
||||
targetSlug := strings.TrimSpace(*req.CompanyCode)
|
||||
targetAllowed := profileCanAccessTenant(requester, "", targetSlug)
|
||||
if !targetAllowed && h.TenantService != nil && targetSlug != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), targetSlug); err == nil && tenant != nil {
|
||||
targetAllowed = profileCanAccessTenant(requester, tenant.ID, tenant.Slug)
|
||||
}
|
||||
}
|
||||
if !targetAllowed {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1884,19 +1886,20 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy/Flat metadata - validate using primary tenant schema
|
||||
schemaCompCode := extractTraitString(identity.Traits, "companyCode")
|
||||
schemaTenantSlug := ""
|
||||
if req.CompanyCode != nil {
|
||||
schemaCompCode = *req.CompanyCode
|
||||
schemaTenantSlug = *req.CompanyCode
|
||||
}
|
||||
if schemaCompCode != "" && h.TenantService != nil {
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), schemaCompCode)
|
||||
if err == nil && tenant != nil {
|
||||
if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
// For flat metadata, we validate the whole req.Metadata against this schema
|
||||
if err := h.validateMetadataWithAuth(req.Metadata, schema, isAdmin, false); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
|
||||
}
|
||||
var tenant *domain.Tenant
|
||||
if schemaTenantSlug != "" && h.TenantService != nil {
|
||||
tenant, _ = h.TenantService.GetTenantBySlug(c.Context(), schemaTenantSlug)
|
||||
} else if tenantID := extractTraitString(identity.Traits, "tenant_id"); tenantID != "" && h.TenantService != nil {
|
||||
tenant, _ = h.TenantService.GetTenant(c.Context(), tenantID)
|
||||
}
|
||||
if tenant != nil {
|
||||
if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
if err := h.validateMetadataWithAuth(req.Metadata, schema, isAdmin, false); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2106,24 +2109,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// [Self-Healing] Sync all companyCodes to Keto
|
||||
if h.KetoOutboxRepo != nil && h.TenantService != nil {
|
||||
if codes, ok := updated.Traits["companyCodes"].([]interface{}); ok {
|
||||
for _, cVal := range codes {
|
||||
if cStr, ok := cVal.(string); ok && cStr != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(bgCtx, cStr); err == nil && tenant != nil {
|
||||
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenant.ID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + updatedLocalUser.ID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if updatedLocalUser.TenantID != nil {
|
||||
// Fallback if companyCodes doesn't exist
|
||||
if updatedLocalUser.TenantID != nil {
|
||||
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: *updatedLocalUser.TenantID,
|
||||
@@ -2181,12 +2168,8 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
if requester.TenantID != nil {
|
||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||
}
|
||||
if requester.CompanyCode != "" {
|
||||
allowed[strings.ToLower(requester.CompanyCode)] = true
|
||||
}
|
||||
for _, tenant := range requester.ManageableTenants {
|
||||
allowed[strings.ToLower(tenant.ID)] = true
|
||||
allowed[strings.ToLower(tenant.Slug)] = true
|
||||
}
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
|
||||
|
||||
Reference in New Issue
Block a user