forked from baron/baron-sso
Ory Keto ReBAC Policy & Relation Tuple Architecture
This commit is contained in:
@@ -29,6 +29,7 @@ type userGroupService struct {
|
||||
userRepo repository.UserRepository
|
||||
tenantRepo repository.TenantRepository
|
||||
ketoService KetoService
|
||||
outboxRepo repository.KetoOutboxRepository
|
||||
kratos *KratosAdminService
|
||||
}
|
||||
|
||||
@@ -37,6 +38,7 @@ func NewUserGroupService(
|
||||
userRepo repository.UserRepository,
|
||||
tenantRepo repository.TenantRepository,
|
||||
keto KetoService,
|
||||
outbox repository.KetoOutboxRepository,
|
||||
kratos *KratosAdminService,
|
||||
) UserGroupService {
|
||||
return &userGroupService{
|
||||
@@ -44,19 +46,55 @@ func NewUserGroupService(
|
||||
userRepo: userRepo,
|
||||
tenantRepo: tenantRepo,
|
||||
ketoService: keto,
|
||||
outboxRepo: outbox,
|
||||
kratos: kratos,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userGroupService) Create(ctx context.Context, group *domain.UserGroup) error {
|
||||
// [Polymorphic Tenant] Create corresponding Tenant record first
|
||||
parentID := group.ParentID
|
||||
if parentID == nil || *parentID == "" {
|
||||
// If no parent user group, the parent is the company tenant
|
||||
parentID = &group.TenantID
|
||||
}
|
||||
|
||||
tenant := &domain.Tenant{
|
||||
ID: group.ID, // Use same ID for 1:1 join
|
||||
Type: domain.TenantTypeUserGroup,
|
||||
ParentID: parentID,
|
||||
Name: group.Name,
|
||||
Slug: "ug-" + group.ID, // Temporary slug for user groups
|
||||
Description: group.Description,
|
||||
Status: domain.TenantStatusActive,
|
||||
}
|
||||
|
||||
if group.ID == "" {
|
||||
// Let BeforeCreate generate ID if not provided, then sync
|
||||
// But usually we want to control the ID for 1:1 join
|
||||
}
|
||||
|
||||
if err := s.tenantRepo.Create(ctx, tenant); err != nil {
|
||||
slog.Error("Failed to create tenant record for user group", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update group.ID to match tenant.ID if it was generated
|
||||
group.ID = tenant.ID
|
||||
|
||||
if err := s.repo.Create(ctx, group); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Keto: UserGroup:<id>#parent_tenant@Tenant:<tid>
|
||||
err := s.ketoService.CreateRelation(ctx, "UserGroup", group.ID, "parent_tenant", "Tenant:"+group.TenantID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create keto relation for user group", "error", err, "group_id", group.ID)
|
||||
// Keto Hierarchy via Outbox: Tenant:<child_id>#parents@Tenant:<parent_id>
|
||||
if s.outboxRepo != nil {
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: group.ID,
|
||||
Relation: "parents",
|
||||
Subject: "Tenant:" + *parentID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -77,8 +115,8 @@ func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGrou
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch members from Keto
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "UserGroup", group.ID, "members", "")
|
||||
// Fetch members from Keto (Tenant namespace)
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "Tenant", group.ID, "members", "")
|
||||
if err != nil {
|
||||
slog.Error("Failed to fetch group members from keto", "error", err, "group_id", group.ID)
|
||||
return nil, err
|
||||
@@ -142,7 +180,7 @@ func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.
|
||||
|
||||
// For each group, fetch member count from Keto
|
||||
for i := range groups {
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "UserGroup", groups[i].ID, "members", "")
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "Tenant", groups[i].ID, "members", "")
|
||||
if err == nil {
|
||||
// Create dummy members just to carry the count for the JSON response
|
||||
groups[i].Members = make([]domain.User, len(tuples))
|
||||
@@ -153,30 +191,38 @@ func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.
|
||||
}
|
||||
|
||||
func (s *userGroupService) AddMember(ctx context.Context, groupID, userID string) error {
|
||||
// Keto: UserGroup:<groupID>#members@User:<userID>
|
||||
err := s.ketoService.CreateRelation(ctx, "UserGroup", groupID, "members", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to sync group membership to keto", "error", err, "group", groupID, "user", userID)
|
||||
return err
|
||||
// Keto via Outbox: Tenant:<groupID>#members@User:<userID>
|
||||
if s.outboxRepo != nil {
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: groupID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + userID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
|
||||
// Keto: Delete relation
|
||||
err := s.ketoService.DeleteRelation(ctx, "UserGroup", groupID, "members", "User:"+userID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to remove group membership from keto", "error", err, "group", groupID, "user", userID)
|
||||
return err
|
||||
// Keto via Outbox: Delete relation
|
||||
if s.outboxRepo != nil {
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: groupID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + userID,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userGroupService) ListRoles(ctx context.Context, groupID string) ([]domain.GroupRole, error) {
|
||||
// Query: namespace=Tenant, subject=UserGroup:groupID#members
|
||||
subject := "UserGroup:" + groupID + "#members"
|
||||
// Query: namespace=Tenant, subject=Tenant:groupID#members
|
||||
subject := "Tenant:" + groupID + "#members"
|
||||
tuples, err := s.ketoService.ListRelations(ctx, "Tenant", "", "", subject)
|
||||
if err != nil {
|
||||
slog.Error("Failed to fetch group roles from keto", "error", err, "group_id", groupID)
|
||||
@@ -213,23 +259,31 @@ func (s *userGroupService) ListRoles(ctx context.Context, groupID string) ([]dom
|
||||
}
|
||||
|
||||
func (s *userGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error {
|
||||
// Keto: Tenant:<tenantID>#<relation>@UserGroup:<groupID>#members
|
||||
// This means all members of the group have the relation on the tenant.
|
||||
subject := "UserGroup:" + groupID + "#members"
|
||||
err := s.ketoService.CreateRelation(ctx, "Tenant", tenantID, relation, subject)
|
||||
if err != nil {
|
||||
slog.Error("Failed to assign group role to tenant in keto", "error", err, "group", groupID, "tenant", tenantID, "relation", relation)
|
||||
return err
|
||||
// Keto via Outbox: Tenant:<tenantID>#<relation>@Tenant:<groupID>#members
|
||||
if s.outboxRepo != nil {
|
||||
subject := "Tenant:" + groupID + "#members"
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenantID,
|
||||
Relation: relation,
|
||||
Subject: subject,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userGroupService) RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error {
|
||||
subject := "UserGroup:" + groupID + "#members"
|
||||
err := s.ketoService.DeleteRelation(ctx, "Tenant", tenantID, relation, subject)
|
||||
if err != nil {
|
||||
slog.Error("Failed to remove group role from tenant in keto", "error", err, "group", groupID, "tenant", tenantID, "relation", relation)
|
||||
return err
|
||||
// Keto via Outbox: Delete relation
|
||||
if s.outboxRepo != nil {
|
||||
subject := "Tenant:" + groupID + "#members"
|
||||
_ = s.outboxRepo.Create(ctx, &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: tenantID,
|
||||
Relation: relation,
|
||||
Subject: subject,
|
||||
Action: domain.KetoOutboxActionDelete,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user