diff --git a/core/membership/service.go b/core/membership/service.go index 4f0de1c05..1b0cdf243 100644 --- a/core/membership/service.go +++ b/core/membership/service.go @@ -801,6 +801,31 @@ func (s *Service) createRelation(ctx context.Context, resourceID, resourceType, return nil } +// createAuditRecord writes the audit record and, when the write fails, logs +// every detail of the record so it can be recreated manually. +func (s *Service) createAuditRecord(ctx context.Context, record auditrecord.AuditRecord) { + if _, err := s.auditRecordRepository.Create(ctx, record); err != nil { + args := []any{ + "error", err, + "event", record.Event, + "org_id", record.OrgID, + "resource_id", record.Resource.ID, + "resource_type", record.Resource.Type, + "resource_name", record.Resource.Name, + "occurred_at", record.OccurredAt, + } + if record.Target != nil { + args = append(args, + "target_id", record.Target.ID, + "target_type", record.Target.Type, + "target_name", record.Target.Name, + "target_metadata", record.Target.Metadata, + ) + } + s.log.WarnContext(ctx, "failed to create audit record", args...) + } +} + func (s *Service) auditOrgMemberRoleChanged(ctx context.Context, org organization.Organization, p principalInfo, roleID string) { targetType, _ := principalTypeToAuditType(p.Type) meta := map[string]any{"role_id": roleID} @@ -808,7 +833,7 @@ func (s *Service) auditOrgMemberRoleChanged(ctx context.Context, org organizatio meta["email"] = p.Email } - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: pkgAuditRecord.OrganizationMemberRoleChangedEvent, Resource: auditrecord.Resource{ ID: org.ID, @@ -840,7 +865,7 @@ func (s *Service) auditOrgMemberAdded(ctx context.Context, org organization.Orga meta["email"] = p.Email } - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: pkgAuditRecord.OrganizationMemberAddedEvent, Resource: auditrecord.Resource{ ID: org.ID, @@ -866,7 +891,7 @@ func (s *Service) auditOrgMemberAdded(ctx context.Context, org organization.Orga } func (s *Service) auditOrgMemberRemoved(ctx context.Context, org organization.Organization, targetID string, targetType pkgAuditRecord.EntityType) { - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: pkgAuditRecord.OrganizationMemberRemovedEvent, Resource: auditrecord.Resource{ ID: org.ID, @@ -1103,7 +1128,7 @@ func (s *Service) auditProjectMember(ctx context.Context, event pkgAuditRecord.E meta = map[string]any{} } meta["principal_type"] = principalType - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: event, Resource: auditrecord.Resource{ ID: prj.ID, @@ -1708,7 +1733,7 @@ func (s *Service) auditGroupMemberAdded(ctx context.Context, grp group.Group, p meta["email"] = p.Email } - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: pkgAuditRecord.GroupMemberAddedEvent, Resource: auditrecord.Resource{ ID: grp.ID, @@ -1741,7 +1766,7 @@ func (s *Service) auditGroupMemberRoleChanged(ctx context.Context, grp group.Gro meta["email"] = p.Email } - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: pkgAuditRecord.GroupMemberRoleChangedEvent, Resource: auditrecord.Resource{ ID: grp.ID, @@ -1774,7 +1799,7 @@ func (s *Service) auditGroupMemberRemoved(ctx context.Context, grp group.Group, meta["email"] = p.Email } - s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{ + s.createAuditRecord(ctx, auditrecord.AuditRecord{ Event: pkgAuditRecord.GroupMemberRemovedEvent, Resource: auditrecord.Resource{ ID: grp.ID, diff --git a/core/membership/service_test.go b/core/membership/service_test.go index dfb8cf568..033a721d8 100644 --- a/core/membership/service_test.go +++ b/core/membership/service_test.go @@ -164,6 +164,22 @@ func TestService_AddOrganizationMember(t *testing.T) { roleID: viewerRoleID, wantErr: nil, }, + { + name: "should succeed even when the audit record write fails", + setup: func(policySvc *mocks.PolicyService, relSvc *mocks.RelationService, roleSvc *mocks.RoleService, orgSvc *mocks.OrgService, userSvc *mocks.UserService, auditRepo *mocks.AuditRecordRepository) { + orgSvc.EXPECT().Get(ctx, orgID).Return(enabledOrg, nil) + userSvc.EXPECT().GetByID(ctx, userID).Return(enabledUser, nil) + roleSvc.EXPECT().Get(ctx, viewerRoleID).Return(role.Role{ID: viewerRoleID, Scopes: []string{schema.OrganizationNamespace}}, nil) + policySvc.EXPECT().List(ctx, policy.Filter{OrgID: orgID, PrincipalID: userID, PrincipalType: schema.UserPrincipal}).Return([]policy.Policy{}, nil) + policySvc.EXPECT().Create(ctx, mock.Anything).Return(policy.Policy{}, nil) + relSvc.EXPECT().Create(ctx, mock.Anything).Return(relation.Relation{}, nil) + auditRepo.EXPECT().Create(ctx, mock.Anything).Return(auditrecord.AuditRecord{}, errors.New("audit store unavailable")) + }, + orgID: orgID, + userID: userID, + roleID: viewerRoleID, + wantErr: nil, + }, { name: "should succeed adding a new member with owner role and create owner relation", setup: func(policySvc *mocks.PolicyService, relSvc *mocks.RelationService, roleSvc *mocks.RoleService, orgSvc *mocks.OrgService, userSvc *mocks.UserService, auditRepo *mocks.AuditRecordRepository) {