forked from baron/baron-sso
Ory Keto ReBAC Policy & Relation Tuple Architecture
This commit is contained in:
@@ -14,20 +14,22 @@ import (
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
KratosAdmin *service.KratosAdminService
|
||||
OryProvider *service.OryProvider
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
UserRepo repository.UserRepository
|
||||
KratosAdmin *service.KratosAdminService
|
||||
OryProvider *service.OryProvider
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
}
|
||||
|
||||
func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, ketoService service.KetoService, userRepo repository.UserRepository) *UserHandler {
|
||||
func NewUserHandler(kratosAdmin *service.KratosAdminService, oryProvider *service.OryProvider, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository) *UserHandler {
|
||||
return &UserHandler{
|
||||
KratosAdmin: kratosAdmin,
|
||||
OryProvider: oryProvider,
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
UserRepo: userRepo,
|
||||
KratosAdmin: kratosAdmin,
|
||||
OryProvider: oryProvider,
|
||||
TenantService: tenantService,
|
||||
KetoService: ketoService,
|
||||
KetoOutboxRepo: ketoOutboxRepo,
|
||||
UserRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,21 +317,36 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}(localUser)
|
||||
}
|
||||
|
||||
// [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)
|
||||
}
|
||||
}()
|
||||
// [Keto] Sync relations via Outbox
|
||||
if h.KetoOutboxRepo != nil {
|
||||
// 1. Tenant Membership
|
||||
if localUser.TenantID != nil {
|
||||
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: *localUser.TenantID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + identityID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
// 2. Role Specifics
|
||||
if role == domain.RoleSuperAdmin {
|
||||
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
Object: "global",
|
||||
Relation: "super_admins",
|
||||
Subject: "User:" + identityID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
} else if role == domain.RoleTenantAdmin && localUser.TenantID != nil {
|
||||
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: *localUser.TenantID,
|
||||
Relation: "admins",
|
||||
Subject: "User:" + identityID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID)
|
||||
@@ -489,25 +506,50 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// [SoT Policy] Kratos가 SoT이므로 로컬 DB 저장은 비동기 Read-Model 동기화로 처리합니다.
|
||||
// [ReBAC Policy] 로컬 DB와 Keto 간의 정합성을 위해 Outbox를 함께 기록합니다.
|
||||
go func(u *domain.User, rRole *string, oRole string, oTenantID string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.UserRepo.Update(ctx, u); err == nil {
|
||||
// [Keto Sync on Role Change]
|
||||
if h.KetoService != nil && rRole != nil && *rRole != oRole {
|
||||
// [Keto Sync on Role Change] via Outbox
|
||||
if h.KetoOutboxRepo != nil && rRole != nil && *rRole != oRole {
|
||||
uID := u.ID
|
||||
newR := *rRole
|
||||
if oRole == domain.RoleSuperAdmin {
|
||||
_ = h.KetoService.DeleteRelation(ctx, "System", "global", "super_admins", uID)
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
Object: "global",
|
||||
Relation: "super_admins",
|
||||
Subject: "User:" + uID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
} else if oRole == domain.RoleTenantAdmin && oTenantID != "" {
|
||||
_ = h.KetoService.DeleteRelation(ctx, "Tenant", oTenantID, "admins", uID)
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: oTenantID,
|
||||
Relation: "admins",
|
||||
Subject: "User:" + uID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
}
|
||||
|
||||
if newR == domain.RoleSuperAdmin {
|
||||
_ = h.KetoService.CreateRelation(ctx, "System", "global", "super_admins", uID)
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
Object: "global",
|
||||
Relation: "super_admins",
|
||||
Subject: "User:" + uID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
} else if newR == domain.RoleTenantAdmin && u.TenantID != nil {
|
||||
_ = h.KetoService.CreateRelation(ctx, "Tenant", *u.TenantID, "admins", uID)
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: *u.TenantID,
|
||||
Relation: "admins",
|
||||
Subject: "User:" + uID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -552,16 +594,17 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// [Keto] Cleanup relations (Best effort)
|
||||
if h.KetoService != nil {
|
||||
go func(uID string) {
|
||||
ctx := context.Background()
|
||||
// Fetch user from DB before cleanup if needed, but here we cleanup common namespaces
|
||||
_ = h.KetoService.DeleteRelation(ctx, "System", "global", "super_admins", uID)
|
||||
|
||||
// If we had more complex relations, we would query Keto first or use user metadata
|
||||
slog.Info("Keto relations cleaned up for user", "userID", uID)
|
||||
}(userID)
|
||||
// [Keto] Cleanup relations via Outbox
|
||||
if h.KetoOutboxRepo != nil {
|
||||
ctx := context.Background()
|
||||
_ = h.KetoOutboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "System",
|
||||
Object: "global",
|
||||
Relation: "super_admins",
|
||||
Subject: "User:" + userID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
// Additional cleanup for tenants could be added here if we keep track of user's current tenants
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
|
||||
Reference in New Issue
Block a user