forked from baron/baron-sso
3단계 권한 모델 확장, keto 권한 정책
This commit is contained in:
@@ -17,14 +17,16 @@ type UserHandler struct {
|
||||
KratosAdmin *service.KratosAdminService
|
||||
OryProvider *service.OryProvider
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
UserRepo repository.UserRepository
|
||||
}
|
||||
|
||||
func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, userRepo repository.UserRepository) *UserHandler {
|
||||
func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, ketoService service.KetoService, userRepo repository.UserRepository) *UserHandler {
|
||||
return &UserHandler{
|
||||
KratosAdmin: kratosAdmin,
|
||||
OryProvider: oryProvider,
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
UserRepo: userRepo,
|
||||
}
|
||||
}
|
||||
@@ -57,6 +59,9 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
|
||||
}
|
||||
|
||||
// [New] Get requester profile from middleware
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
@@ -74,17 +79,28 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
filtered := make([]service.KratosIdentity, 0, len(identities))
|
||||
if search == "" {
|
||||
filtered = identities
|
||||
} else {
|
||||
searchLower := strings.ToLower(search)
|
||||
for _, identity := range identities {
|
||||
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
|
||||
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
|
||||
if strings.Contains(email, searchLower) || strings.Contains(name, searchLower) {
|
||||
filtered = append(filtered, identity)
|
||||
searchLower := strings.ToLower(search)
|
||||
|
||||
for _, identity := range identities {
|
||||
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
|
||||
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
|
||||
// 1. Tenant Admin filtering
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
continue // Skip users from other tenants
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Search filtering
|
||||
if search != "" {
|
||||
if !strings.Contains(email, searchLower) && !strings.Contains(name, searchLower) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
filtered = append(filtered, identity)
|
||||
}
|
||||
|
||||
total := int64(len(filtered))
|
||||
@@ -123,6 +139,15 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: access to user in another tenant denied"})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(h.mapIdentitySummary(c.Context(), *identity))
|
||||
}
|
||||
|
||||
@@ -245,6 +270,23 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// [Keto] Sync relations
|
||||
if h.KetoService != nil {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
// 1. Tenant Membership
|
||||
if localUser.TenantID != nil {
|
||||
_ = h.KetoService.CreateRelation(ctx, "Tenant", *localUser.TenantID, "members", identityID)
|
||||
}
|
||||
// 2. Role Specifics
|
||||
if role == domain.RoleSuperAdmin {
|
||||
_ = h.KetoService.CreateRelation(ctx, "System", "global", "super_admins", identityID)
|
||||
} else if role == domain.RoleTenantAdmin && localUser.TenantID != nil {
|
||||
_ = h.KetoService.CreateRelation(ctx, "Tenant", *localUser.TenantID, "admins", identityID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -278,6 +320,15 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: cannot update user in another tenant"})
|
||||
}
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Password *string `json:"password"`
|
||||
Name *string `json:"name"`
|
||||
@@ -292,6 +343,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
// [New] Tenant Admin restriction: Cannot change companyCode
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
if req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: tenant admins cannot change user's tenant"})
|
||||
}
|
||||
}
|
||||
|
||||
traits := identity.Traits
|
||||
if traits == nil {
|
||||
traits = map[string]interface{}{}
|
||||
@@ -395,10 +453,33 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
|
||||
}
|
||||
|
||||
// [New] Check access scope before deletion
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err == nil && identity != nil {
|
||||
compCode := extractTraitString(identity.Traits, "companyCode")
|
||||
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: cannot delete user in another tenant"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.KratosAdmin.DeleteIdentity(c.Context(), userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// [Keto] Cleanup relations (Best effort)
|
||||
if h.KetoService != nil {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
// Note: Proper cleanup requires searching all relations,
|
||||
// here we just cleanup known common ones or rely on subject cleanup if Keto supported it.
|
||||
_ = h.KetoService.DeleteRelation(ctx, "System", "global", "super_admins", userID)
|
||||
// For tenants, we'd need to know which tenant they were in.
|
||||
}()
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user