diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 66d111c2c..4f1f324e4 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -75,7 +75,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l integrationUseCase := biz.NewIntegrationUseCase(newIntegrationUseCaseOpts) v := bootstrap.Onboarding organizationUseCase := biz.NewOrganizationUseCase(organizationRepo, casBackendUseCase, auditorUseCase, integrationUseCase, membershipRepo, v, logger) - membershipUseCase := biz.NewMembershipUseCase(membershipRepo, organizationUseCase, auditorUseCase, logger) + membershipUseCase := biz.NewMembershipUseCase(membershipRepo, organizationUseCase, auditorUseCase, userRepo, logger) allowList := newAuthAllowList(bootstrap) userAccessSyncerUseCase := biz.NewUserAccessSyncerUseCase(logger, userRepo, allowList) newUserUseCaseParams := &biz.NewUserUseCaseParams{ diff --git a/app/controlplane/pkg/auditor/events/testdata/users/user_role_changed.json b/app/controlplane/pkg/auditor/events/testdata/users/user_role_changed.json new file mode 100644 index 000000000..022010bc7 --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/users/user_role_changed.json @@ -0,0 +1,21 @@ +{ + "ActionType": "RoleChanged", + "TargetType": "User", + "TargetID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorType": "USER", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "john@cyberdyne.io", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "john@cyberdyne.io role changed from 'role:org:owner' to 'role:org:member'", + "Info": { + "user_id": "1089bb36-e27b-428b-8009-d015c8737c54", + "email": "john@cyberdyne.io", + "sso_groups": [ + "group1", + "group2" + ], + "old_role": "role:org:owner", + "new_role": "role:org:member" + }, + "Digest": "sha256:80bfd69a13fe4736ad2642ec20eaecc8f002e1774089d75d3c9fcee421af4247" +} \ No newline at end of file diff --git a/app/controlplane/pkg/auditor/events/user.go b/app/controlplane/pkg/auditor/events/user.go index 95c624ffe..912c1fd9b 100644 --- a/app/controlplane/pkg/auditor/events/user.go +++ b/app/controlplane/pkg/auditor/events/user.go @@ -31,9 +31,10 @@ var ( ) const ( - UserType auditor.TargetType = "User" - UserSignedUpActionType string = "SignedUp" - UserLoggedInActionType string = "LoggedIn" + UserType auditor.TargetType = "User" + UserSignedUpActionType string = "SignedUp" + UserLoggedInActionType string = "LoggedIn" + UserRoleChangedActionType string = "RoleChanged" ) // UserBase is the base struct for policy events @@ -96,3 +97,25 @@ func (p *UserLoggedIn) ActionInfo() (json.RawMessage, error) { return json.Marshal(&p) } + +type UserRoleChanged struct { + *UserBase + OldRole string `json:"old_role,omitempty"` + NewRole string `json:"new_role,omitempty"` +} + +func (p *UserRoleChanged) ActionType() string { + return UserRoleChangedActionType +} + +func (p *UserRoleChanged) Description() string { + return fmt.Sprintf("%s role changed from '%s' to '%s'", p.Email, p.OldRole, p.NewRole) +} + +func (p *UserRoleChanged) ActionInfo() (json.RawMessage, error) { + if p.UserID == nil || p.Email == "" || p.OldRole == "" || p.NewRole == "" { + return nil, errors.New("user id, email, old role and new role are required") + } + + return json.Marshal(&p) +} diff --git a/app/controlplane/pkg/auditor/events/user_test.go b/app/controlplane/pkg/auditor/events/user_test.go index 73a7ba049..2a2049779 100644 --- a/app/controlplane/pkg/auditor/events/user_test.go +++ b/app/controlplane/pkg/auditor/events/user_test.go @@ -24,6 +24,8 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -62,6 +64,19 @@ func TestUserEvents(t *testing.T) { }, expected: "testdata/users/user_logs_in.json", }, + { + name: "User role changed", + event: &events.UserRoleChanged{ + UserBase: &events.UserBase{ + UserID: uuidPtr(userUUID), + Email: testEmail, + SSOGroups: []string{"group1", "group2"}, + }, + OldRole: string(authz.RoleOwner), + NewRole: string(authz.RoleOrgMember), + }, + expected: "testdata/users/user_role_changed.json", + }, } for _, tt := range tests { diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 3a4fa5531..e85250959 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -82,14 +82,17 @@ type MembershipsRBAC interface { } type MembershipUseCase struct { - repo MembershipRepo + logger *log.Helper + // Repositories + repo MembershipRepo + userRepo UserRepo + // Use Cases orgUseCase *OrganizationUseCase - logger *log.Helper auditor *AuditorUseCase } -func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, logger log.Logger) *MembershipUseCase { - return &MembershipUseCase{repo: repo, orgUseCase: orgUC, logger: log.NewHelper(logger), auditor: auditor} +func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, userRepo UserRepo, logger log.Logger) *MembershipUseCase { + return &MembershipUseCase{repo: repo, orgUseCase: orgUC, logger: log.NewHelper(logger), userRepo: userRepo, auditor: auditor} } // LeaveAndDeleteOrg deletes a membership (and the org i) from the database associated with the current user @@ -197,7 +200,9 @@ func (uc *MembershipUseCase) UpdateRole(ctx context.Context, orgID, userID, memb m, err := uc.repo.FindByIDInOrg(ctx, orgUUID, membershipUUID) if err != nil { return nil, fmt.Errorf("failed to find membership: %w", err) - } else if m == nil { + } + + if m == nil { return nil, NewErrNotFound("membership") } @@ -205,7 +210,31 @@ func (uc *MembershipUseCase) UpdateRole(ctx context.Context, orgID, userID, memb return nil, NewErrValidationStr("cannot update yourself") } - return uc.repo.SetRole(ctx, membershipUUID, role) + userUUID, err := uuid.Parse(m.User.ID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + user, err := uc.userRepo.FindByID(ctx, userUUID) + if err != nil { + return nil, fmt.Errorf("failed to find user: %w", err) + } + + updatedMembership, err := uc.repo.SetRole(ctx, membershipUUID, role) + if err != nil { + return nil, fmt.Errorf("failed to update membership role: %w", err) + } + + uc.auditor.Dispatch(ctx, &events.UserRoleChanged{ + UserBase: &events.UserBase{ + UserID: &userUUID, + Email: user.Email, + }, + OldRole: string(m.Role), + NewRole: string(role), + }, &m.OrganizationID) + + return updatedMembership, nil } type membershipCreateOpts struct { diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index 554bfb377..01080520a 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -71,7 +71,8 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r } integrationUseCase := biz.NewIntegrationUseCase(newIntegrationUseCaseOpts) organizationUseCase := biz.NewOrganizationUseCase(organizationRepo, casBackendUseCase, auditorUseCase, integrationUseCase, membershipRepo, arg, logger) - membershipUseCase := biz.NewMembershipUseCase(membershipRepo, organizationUseCase, auditorUseCase, logger) + userRepo := data.NewUserRepo(dataData, logger) + membershipUseCase := biz.NewMembershipUseCase(membershipRepo, organizationUseCase, auditorUseCase, userRepo, logger) workflowContractRepo := data.NewWorkflowContractRepo(dataData, logger) v := NewPolicyProviderConfig(bootstrap) registry, err := policies.NewRegistry(logger, v...) @@ -101,7 +102,6 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r return nil, nil, err } prometheusUseCase := biz.NewPrometheusUseCase(v2, organizationUseCase, orgMetricsUseCase, logger) - userRepo := data.NewUserRepo(dataData, logger) allowList := newAuthAllowList(bootstrap) userAccessSyncerUseCase := biz.NewUserAccessSyncerUseCase(logger, userRepo, allowList) newUserUseCaseParams := &biz.NewUserUseCaseParams{