Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions core/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,20 @@ const (
PolicyCreatedEvent EventName = "app.policy.created"
PolicyDeletedEvent EventName = "app.policy.deleted"

OrgCreatedEvent EventName = "app.organization.created"
OrgUpdatedEvent EventName = "app.organization.updated"
OrgDeletedEvent EventName = "app.organization.deleted"
OrgDisabledEvent EventName = "app.organization.disabled"
OrgMemberCreatedEvent EventName = "app.organization.member.created"
OrgMemberDeletedEvent EventName = "app.organization.member.deleted"
OrgKycUpdatedEvent EventName = "app.organization.kyc.updated"

ProjectCreatedEvent EventName = "app.project.created"
ProjectUpdatedEvent EventName = "app.project.updated"
ProjectDeletedEvent EventName = "app.project.deleted"
ProjectMemberRoleSetEvent EventName = "app.project.member.role.set"
ProjectMemberRemovedEvent EventName = "app.project.member.removed"
OrgCreatedEvent EventName = "app.organization.created"
OrgUpdatedEvent EventName = "app.organization.updated"
OrgDeletedEvent EventName = "app.organization.deleted"
OrgDisabledEvent EventName = "app.organization.disabled"
OrgMemberCreatedEvent EventName = "app.organization.member.created"
OrgMemberDeletedEvent EventName = "app.organization.member.deleted"
OrgMemberRoleChangedEvent EventName = "app.organization.member.role_changed"
OrgKycUpdatedEvent EventName = "app.organization.kyc.updated"

ProjectCreatedEvent EventName = "app.project.created"
ProjectUpdatedEvent EventName = "app.project.updated"
ProjectDeletedEvent EventName = "app.project.deleted"
ProjectMemberRoleChangedEvent EventName = "app.project.member.role_changed"
ProjectMemberRemovedEvent EventName = "app.project.member.removed"

ResourceCreatedEvent EventName = "app.resource.created"
ResourceUpdatedEvent EventName = "app.resource.updated"
Expand Down
41 changes: 41 additions & 0 deletions core/organization/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,47 @@ func (s Service) SetMemberRole(ctx context.Context, orgID, userID, newRoleID str
return err
}

// audit logging
org, err := s.repository.GetByID(ctx, orgID)
if err != nil {
return err
}

usr, err := s.userService.GetByID(ctx, userID)
if err != nil {
return err
}

_, auditErr := s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{
Event: pkgAuditRecord.OrganizationMemberRoleChangedEvent,
Resource: auditrecord.Resource{
ID: orgID,
Type: pkgAuditRecord.OrganizationType,
Name: org.Title,
},
Target: &auditrecord.Target{
ID: userID,
Type: pkgAuditRecord.UserType,
Name: usr.Title,
Metadata: map[string]any{
"email": usr.Email,
"role_id": newRoleID,
},
},
OrgID: orgID,
OccurredAt: time.Now(),
})
if auditErr != nil {
return auditErr
}
Comment thread
whoAbhishekSah marked this conversation as resolved.

audit.GetAuditor(ctx, orgID).LogWithAttrs(audit.OrgMemberRoleChangedEvent, audit.Target{
ID: userID,
Type: schema.UserPrincipal,
}, map[string]string{
"role_id": newRoleID,
})

return nil
}

Expand Down
75 changes: 60 additions & 15 deletions core/organization/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/raystack/frontier/core/user"
pat "github.com/raystack/frontier/core/userpat/models"
"github.com/raystack/frontier/internal/bootstrap/schema"
pkgAuditRecord "github.com/raystack/frontier/pkg/auditrecord"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
Expand Down Expand Up @@ -379,15 +380,15 @@ func TestService_SetMemberRole(t *testing.T) {

tests := []struct {
name string
setup func(*mocks.Repository, *mocks.UserService, *mocks.RoleService, *mocks.PolicyService)
setup func(*mocks.Repository, *mocks.UserService, *mocks.RoleService, *mocks.PolicyService, *mocks.AuditRecordRepository)
orgID string
userID string
newRoleID string
wantErr error
}{
{
name: "should return error if org does not exist",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{}, organization.ErrNotExist)
},
orgID: orgID,
Expand All @@ -397,7 +398,7 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should return error if org is disabled",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{State: organization.Disabled}, nil)
},
orgID: orgID,
Expand All @@ -407,7 +408,7 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should return error if user does not exist",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{}, user.ErrNotExist)
},
Expand All @@ -418,7 +419,7 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should return error if role does not exist",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{ID: userID}, nil)
roleSvc.EXPECT().Get(ctx, memberRoleID).Return(role.Role{}, role.ErrNotExist)
Expand All @@ -430,7 +431,7 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should return error if role is not valid for org scope",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{ID: userID}, nil)
// role exists but has project scope, not org scope
Expand All @@ -443,7 +444,7 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should return error if user is not a member of the org",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{ID: userID}, nil)
roleSvc.EXPECT().Get(ctx, memberRoleID).Return(role.Role{ID: memberRoleID, Name: "member", Scopes: []string{schema.OrganizationNamespace}}, nil)
Expand All @@ -461,7 +462,7 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should return error if demoting last owner",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, _ *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{ID: userID}, nil)
roleSvc.EXPECT().Get(ctx, memberRoleID).Return(role.Role{ID: memberRoleID, Name: "member", Scopes: []string{schema.OrganizationNamespace}}, nil)
Expand All @@ -486,9 +487,18 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should succeed when changing role with multiple owners",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{ID: userID}, nil)
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, auditRepo *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{
ID: orgID,
Name: "test-org",
Title: "Test Organization",
State: organization.Enabled,
}, nil).Times(2)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{
ID: userID,
Title: "test-user",
Email: "test-user@acme.dev",
}, nil).Times(2)
roleSvc.EXPECT().Get(ctx, memberRoleID).Return(role.Role{ID: memberRoleID, Name: "member", Scopes: []string{schema.OrganizationNamespace}}, nil)
// get user's existing policies - user is owner
policySvc.EXPECT().List(ctx, policy.Filter{
Expand Down Expand Up @@ -516,6 +526,19 @@ func TestService_SetMemberRole(t *testing.T) {
PrincipalID: userID,
PrincipalType: schema.UserPrincipal,
}).Return(policy.Policy{}, nil)
// audit logging
auditRepo.EXPECT().Create(ctx, mock.MatchedBy(func(ar auditrecord.AuditRecord) bool {
if ar.Target == nil {
return false
}
return ar.Event == pkgAuditRecord.OrganizationMemberRoleChangedEvent &&
ar.Resource.ID == orgID &&
ar.Resource.Name == "Test Organization" &&
ar.Target.ID == userID &&
ar.Target.Metadata["email"] == "test-user@acme.dev" &&
ar.Target.Metadata["role_id"] == memberRoleID &&
ar.OrgID == orgID
})).Return(auditrecord.AuditRecord{}, nil).Once()
},
orgID: orgID,
userID: userID,
Expand All @@ -524,9 +547,18 @@ func TestService_SetMemberRole(t *testing.T) {
},
{
name: "should succeed when promoting to owner",
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{ID: orgID, State: organization.Enabled}, nil)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{ID: userID}, nil)
setup: func(repo *mocks.Repository, userSvc *mocks.UserService, roleSvc *mocks.RoleService, policySvc *mocks.PolicyService, auditRepo *mocks.AuditRecordRepository) {
repo.EXPECT().GetByID(ctx, orgID).Return(organization.Organization{
ID: orgID,
Name: "test-org",
Title: "Test Organization",
State: organization.Enabled,
}, nil).Times(2)
userSvc.EXPECT().GetByID(ctx, userID).Return(user.User{
ID: userID,
Title: "test-user",
Email: "test-user@acme.dev",
}, nil).Times(2)
roleSvc.EXPECT().Get(ctx, ownerRoleID).Return(role.Role{ID: ownerRoleID, Name: schema.RoleOrganizationOwner, Scopes: []string{schema.OrganizationNamespace}}, nil)
// get user's existing policies - user is member
policySvc.EXPECT().List(ctx, policy.Filter{
Expand All @@ -547,6 +579,19 @@ func TestService_SetMemberRole(t *testing.T) {
PrincipalID: userID,
PrincipalType: schema.UserPrincipal,
}).Return(policy.Policy{}, nil)
// audit logging
auditRepo.EXPECT().Create(ctx, mock.MatchedBy(func(ar auditrecord.AuditRecord) bool {
if ar.Target == nil {
return false
}
return ar.Event == pkgAuditRecord.OrganizationMemberRoleChangedEvent &&
ar.Resource.ID == orgID &&
ar.Resource.Name == "Test Organization" &&
ar.Target.ID == userID &&
ar.Target.Metadata["email"] == "test-user@acme.dev" &&
ar.Target.Metadata["role_id"] == ownerRoleID &&
ar.OrgID == orgID
})).Return(auditrecord.AuditRecord{}, nil).Once()
},
orgID: orgID,
userID: userID,
Expand All @@ -567,7 +612,7 @@ func TestService_SetMemberRole(t *testing.T) {
mockRoleSvc := mocks.NewRoleService(t)

if tt.setup != nil {
tt.setup(mockRepo, mockUserSvc, mockRoleSvc, mockPolicySvc)
tt.setup(mockRepo, mockUserSvc, mockRoleSvc, mockPolicySvc, mockAuditRecordRepo)
}

svc := organization.NewService(mockRepo, mockRelationSvc, mockUserSvc, mockAuthnSvc, mockPolicySvc, mockPrefSvc, mockAuditRecordRepo, mockRoleSvc)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/v1beta1connect/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ func (h *ConnectHandler) SetProjectMemberRole(ctx context.Context, request *conn
}
}

audit.GetAuditor(ctx, "").LogWithAttrs(audit.ProjectMemberRoleSetEvent, audit.ProjectTarget(projectID), map[string]string{
audit.GetAuditor(ctx, "").LogWithAttrs(audit.ProjectMemberRoleChangedEvent, audit.ProjectTarget(projectID), map[string]string{
"principal_id": principalID,
"principal_type": principalType,
"role_id": roleID,
Expand Down
1 change: 1 addition & 0 deletions pkg/auditrecord/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
OrganizationInvitedEvent Event = "organization.invited"
OrganizationMemberAddedEvent Event = "organization.added"
OrganizationMemberRemovedEvent Event = "organization.removed"
OrganizationMemberRoleChangedEvent Event = "organization.role_changed"
OrganizationInvitationAcceptedEvent Event = "organization.accepted"

// KYC Events
Expand Down
Loading