1
0
forked from baron/baron-sso

Ory Keto ReBAC Policy & Relation Tuple Architecture

This commit is contained in:
2026-02-20 17:56:05 +09:00
parent 226a236bf2
commit 2ec2653bfb
23 changed files with 980 additions and 396 deletions

View File

@@ -71,7 +71,9 @@ type MockTenantRepository struct {
mock.Mock
}
func (m *MockTenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error { return nil }
func (m *MockTenantRepository) Create(ctx context.Context, tenant *domain.Tenant) error {
return m.Called(ctx, tenant).Error(0)
}
func (m *MockTenantRepository) Update(ctx context.Context, tenant *domain.Tenant) error { return nil }
func (m *MockTenantRepository) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
return nil, nil
@@ -98,66 +100,81 @@ func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, d
return nil
}
// --- Tests ---
func TestUserGroupService_Create(t *testing.T) {
mockRepo := new(MockUserGroupRepository)
mockKeto := new(MockKetoService)
// We don't need userRepo or tenantRepo for Create
svc := NewUserGroupService(mockRepo, nil, nil, mockKeto, nil)
mockTenantRepo := new(MockTenantRepository)
mockKeto := new(MockKetoServiceShared)
mockOutbox := new(MockKetoOutboxRepositoryShared)
svc := NewUserGroupService(mockRepo, nil, mockTenantRepo, mockKeto, mockOutbox, nil)
group := &domain.UserGroup{
ID: "group-1",
TenantID: "tenant-1",
TenantID: "company-1",
Name: "Test Group",
}
// Mock Tenant creation (Polymorphic)
mockTenantRepo.On("Create", mock.Anything, mock.MatchedBy(func(ten *domain.Tenant) bool {
return ten.Type == domain.TenantTypeUserGroup && ten.ID == group.ID
})).Return(nil)
// Mock UserGroup creation
mockRepo.On("Create", mock.Anything, group).Return(nil)
mockKeto.On("CreateRelation", mock.Anything, "UserGroup", group.ID, "parent_tenant", "Tenant:"+group.TenantID).Return(nil)
// Mock Keto sync via Outbox
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "Tenant" && e.Object == group.ID && e.Relation == "parents" && e.Subject == "Tenant:"+group.TenantID
})).Return(nil)
err := svc.Create(context.Background(), group)
assert.NoError(t, err)
mockTenantRepo.AssertExpectations(t)
mockRepo.AssertExpectations(t)
mockKeto.AssertExpectations(t)
mockOutbox.AssertExpectations(t)
}
func TestUserGroupService_AddMember(t *testing.T) {
mockKeto := new(MockKetoService)
svc := NewUserGroupService(nil, nil, nil, mockKeto, nil)
mockOutbox := new(MockKetoOutboxRepositoryShared)
svc := NewUserGroupService(nil, nil, nil, nil, mockOutbox, nil)
groupID := "group-1"
userID := "user-1"
mockKeto.On("CreateRelation", mock.Anything, "UserGroup", groupID, "members", "User:"+userID).Return(nil)
// Using Outbox and Tenant namespace
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "Tenant" && e.Object == groupID && e.Relation == "members" && e.Subject == "User:"+userID
})).Return(nil)
err := svc.AddMember(context.Background(), groupID, userID)
assert.NoError(t, err)
mockKeto.AssertExpectations(t)
mockOutbox.AssertExpectations(t)
}
func TestUserGroupService_AssignRoleToTenant(t *testing.T) {
mockKeto := new(MockKetoService)
svc := NewUserGroupService(nil, nil, nil, mockKeto, nil)
mockOutbox := new(MockKetoOutboxRepositoryShared)
svc := NewUserGroupService(nil, nil, nil, nil, mockOutbox, nil)
groupID := "group-1"
tenantID := "tenant-alpha"
relation := "manage"
expectedSubject := "UserGroup:" + groupID + "#members"
mockKeto.On("CreateRelation", mock.Anything, "Tenant", tenantID, relation, expectedSubject).Return(nil)
expectedSubject := "Tenant:" + groupID + "#members"
mockOutbox.On("Create", mock.Anything, mock.MatchedBy(func(e *domain.KetoOutbox) bool {
return e.Namespace == "Tenant" && e.Object == tenantID && e.Relation == relation && e.Subject == expectedSubject
})).Return(nil)
err := svc.AssignRoleToTenant(context.Background(), groupID, tenantID, relation)
assert.NoError(t, err)
mockKeto.AssertExpectations(t)
mockOutbox.AssertExpectations(t)
}
func TestUserGroupService_ListRoles(t *testing.T) {
mockKeto := new(MockKetoService)
mockKeto := new(MockKetoServiceShared)
mockTenantRepo := new(MockTenantRepository)
svc := NewUserGroupService(nil, nil, mockTenantRepo, mockKeto, nil)
svc := NewUserGroupService(nil, nil, mockTenantRepo, mockKeto, nil, nil)
groupID := "group-1"
subject := "UserGroup:" + groupID + "#members"
subject := "Tenant:" + groupID + "#members"
// Mock Keto relations
tuples := []RelationTuple{
@@ -186,15 +203,11 @@ func TestUserGroupService_ListRoles(t *testing.T) {
}
func TestUserGroupService_Get_WithKratosFallback(t *testing.T) {
// This tests the logic where a user is in Keto but not in local DB
mockRepo := new(MockUserGroupRepository)
mockKeto := new(MockKetoService)
mockKeto := new(MockKetoServiceShared)
mockUserRepo := new(MockUserRepository)
// We need a way to mock KratosAdminService but it's a struct, not an interface.
// For this POC test, we'll focus on the Keto and UserRepo parts.
// If needed, we can refactor KratosAdminService to an interface.
svc := NewUserGroupService(mockRepo, mockUserRepo, nil, mockKeto, nil)
svc := NewUserGroupService(mockRepo, mockUserRepo, nil, mockKeto, nil, nil)
groupID := "group-1"
mockRepo.On("FindByID", mock.Anything, groupID).Return(&domain.UserGroup{ID: groupID, Name: "Test"}, nil)
@@ -202,14 +215,13 @@ func TestUserGroupService_Get_WithKratosFallback(t *testing.T) {
tuples := []RelationTuple{
{Object: groupID, Relation: "members", SubjectID: "User:u1"},
}
mockKeto.On("ListRelations", mock.Anything, "UserGroup", groupID, "members", "").Return(tuples, nil)
// Note: Transitioned to 'Tenant' namespace for groups
mockKeto.On("ListRelations", mock.Anything, "Tenant", groupID, "members", "").Return(tuples, nil)
// User u1 not in local DB
mockUserRepo.On("FindByIDs", mock.Anything, []string{"u1"}).Return([]domain.User{}, nil)
group, err := svc.Get(context.Background(), groupID)
assert.NoError(t, err)
assert.NotNil(t, group)
// Members should be empty since Kratos is nil in this test setup
assert.Len(t, group.Members, 0)
}