Skip to content

Commit 6f451bb

Browse files
authored
feat: Create audit records for invite and role operations (#1207)
1 parent a3e80af commit 6f451bb

24 files changed

+648
-230
lines changed

cmd/serve.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -373,14 +373,11 @@ func buildAPIDependencies(
373373
relationPGRepository := postgres.NewRelationRepository(dbc)
374374
relationService := relation.NewService(relationPGRepository, authzRelationRepository)
375375

376-
roleRepository := postgres.NewRoleRepository(dbc)
377-
roleService := role.NewService(roleRepository, relationService, permissionService)
376+
auditRecordRepository := postgres.NewAuditRecordRepository(dbc)
378377

378+
roleRepository := postgres.NewRoleRepository(dbc)
379379
policyPGRepository := postgres.NewPolicyRepository(dbc)
380-
policyService := policy.NewService(policyPGRepository, relationService, roleService)
381-
382380
userRepository := postgres.NewUserRepository(dbc)
383-
userService := user.NewService(userRepository, relationService, policyService, roleService)
384381

385382
prospectRepository := postgres.NewProspectRepository(dbc)
386383
prospectService := prospect.NewService(prospectRepository)
@@ -415,15 +412,20 @@ func buildAPIDependencies(
415412
return api.Deps{}, fmt.Errorf("failed to parse passkey config: %w", err)
416413
}
417414
}
418-
authnService := authenticate.NewService(logger, cfg.App.Authentication,
419-
postgres.NewFlowRepository(logger, dbc), mailDialer, tokenService, sessionService, userService, serviceUserService, webAuthConfig)
420415

421416
groupRepository := postgres.NewGroupRepository(dbc)
422-
groupService := group.NewService(groupRepository, relationService, authnService, policyService)
423-
424417
organizationRepository := postgres.NewOrganizationRepository(dbc)
418+
419+
roleService := role.NewService(roleRepository, relationService, permissionService, auditRecordRepository)
420+
policyService := policy.NewService(policyPGRepository, relationService, roleService)
421+
userService := user.NewService(userRepository, relationService, policyService, roleService)
422+
authnService := authenticate.NewService(logger, cfg.App.Authentication,
423+
postgres.NewFlowRepository(logger, dbc), mailDialer, tokenService, sessionService, userService, serviceUserService, webAuthConfig)
424+
groupService := group.NewService(groupRepository, relationService, authnService, policyService)
425425
organizationService := organization.NewService(organizationRepository, relationService, userService,
426-
authnService, policyService, preferenceService)
426+
authnService, policyService, preferenceService, auditRecordRepository)
427+
428+
auditRecordService := auditrecord.NewService(auditRecordRepository, userService, serviceUserService, sessionService)
427429

428430
orgKycRepository := postgres.NewOrgKycRepository(dbc)
429431
orgKycService := kyc.NewService(orgKycRepository)
@@ -478,7 +480,8 @@ func buildAPIDependencies(
478480
)
479481

480482
invitationService := invitation.NewService(mailDialer, postgres.NewInvitationRepository(logger, dbc),
481-
organizationService, groupService, userService, relationService, policyService, preferenceService)
483+
organizationService, groupService, userService, relationService, policyService, preferenceService,
484+
auditRecordRepository)
482485

483486
if GetStripeClientFunc == nil {
484487
// allow to override the stripe client creation function in tests
@@ -562,9 +565,6 @@ func buildAPIDependencies(
562565
audit.WithIgnoreList(cfg.Log.IgnoredAuditEvents),
563566
)
564567

565-
auditRecordRepository := postgres.NewAuditRecordRepository(dbc)
566-
auditRecordService := auditrecord.NewService(auditRecordRepository, userService, serviceUserService, sessionService)
567-
568568
dependencies := api.Deps{
569569
OrgService: organizationService,
570570
OrgKycService: orgKycService,

core/auditrecord/auditrecord.go

Lines changed: 10 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,12 @@
11
package auditrecord
22

3-
import (
4-
"time"
5-
6-
"github.com/raystack/frontier/pkg/metadata"
7-
"github.com/raystack/frontier/pkg/utils"
8-
)
9-
10-
type AuditRecord struct {
11-
ID string `json:"id,omitempty"`
12-
Event string `json:"event"`
13-
Actor Actor `json:"actor"`
14-
Resource Resource `json:"resource"`
15-
Target *Target `json:"target"`
16-
OccurredAt time.Time `json:"occurred_at"`
17-
OrgID string `json:"org_id"`
18-
RequestID *string `json:"request_id"`
19-
CreatedAt time.Time `json:"created_at,omitempty"`
20-
Metadata metadata.Metadata `json:"metadata"`
21-
IdempotencyKey string `json:"idempotency_key"`
22-
}
23-
24-
type Actor struct {
25-
ID string `json:"id"`
26-
Type string `json:"type"`
27-
Name string `json:"name"`
28-
Metadata metadata.Metadata `json:"metadata"`
29-
}
30-
31-
type Resource struct {
32-
ID string `json:"id"`
33-
Type string `json:"type"`
34-
Name string `json:"name"`
35-
Metadata metadata.Metadata `json:"metadata"`
36-
}
37-
38-
type Target struct {
39-
ID string `json:"id"`
40-
Type string `json:"type"`
41-
Name string `json:"name"`
42-
Metadata metadata.Metadata `json:"metadata"`
43-
}
44-
45-
type AuditRecordsList struct {
46-
AuditRecords []AuditRecord
47-
Group *utils.Group
48-
Page utils.Page
49-
}
50-
51-
// AuditRecordRQLSchema is the schema for audit record RQL queries. This is a flattened version of the AuditRecord struct.
52-
// This is needed because the RQL parser does not support nested structs.
53-
type AuditRecordRQLSchema struct {
54-
ID string `rql:"name=id,type=string"`
55-
Event string `rql:"name=event,type=string"`
56-
ActorID string `rql:"name=actor_id,type=string"`
57-
ActorType string `rql:"name=actor_type,type=string"`
58-
ActorName string `rql:"name=actor_name,type=string"`
59-
ResourceID string `rql:"name=resource_id,type=string"`
60-
ResourceType string `rql:"name=resource_type,type=string"`
61-
ResourceName string `rql:"name=resource_name,type=string"`
62-
TargetID string `rql:"name=target_id,type=string"`
63-
TargetType string `rql:"name=target_type,type=string"`
64-
TargetName string `rql:"name=target_name,type=string"`
65-
OccurredAt time.Time `rql:"name=occurred_at,type=datetime"`
66-
OrgID string `rql:"name=org_id,type=string"`
67-
RequestID string `rql:"name=request_id,type=string"`
68-
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
69-
IdempotencyKey string `rql:"name=idempotency_key,type=string"`
70-
}
3+
import "github.com/raystack/frontier/core/auditrecord/models"
4+
5+
// Models moved to a new package to avoid circular dependency with other packages.
6+
// Re-assigned for backward compatibility
7+
type AuditRecord = models.AuditRecord
8+
type Actor = models.Actor
9+
type Resource = models.Resource
10+
type Target = models.Target
11+
type AuditRecordsList = models.AuditRecordsList
12+
type AuditRecordRQLSchema = models.AuditRecordRQLSchema

core/auditrecord/models/models.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package models
2+
3+
import (
4+
"time"
5+
6+
"github.com/raystack/frontier/pkg/auditrecord"
7+
"github.com/raystack/frontier/pkg/metadata"
8+
"github.com/raystack/frontier/pkg/utils"
9+
)
10+
11+
type AuditRecord struct {
12+
ID string `json:"id,omitempty"`
13+
Event auditrecord.Event `json:"event"`
14+
Actor Actor `json:"actor"`
15+
Resource Resource `json:"resource"`
16+
Target *Target `json:"target"`
17+
OccurredAt time.Time `json:"occurred_at"`
18+
OrgID string `json:"org_id"`
19+
RequestID *string `json:"request_id"`
20+
CreatedAt time.Time `json:"created_at,omitempty"`
21+
Metadata metadata.Metadata `json:"metadata"`
22+
IdempotencyKey string `json:"idempotency_key"`
23+
}
24+
25+
type Actor struct {
26+
ID string `json:"id"`
27+
Type string `json:"type"`
28+
Name string `json:"name"`
29+
Metadata metadata.Metadata `json:"metadata"`
30+
}
31+
32+
type Resource struct {
33+
ID string `json:"id"`
34+
Type auditrecord.EntityType `json:"type"`
35+
Name string `json:"name"`
36+
Metadata metadata.Metadata `json:"metadata"`
37+
}
38+
39+
type Target struct {
40+
ID string `json:"id"`
41+
Type auditrecord.EntityType `json:"type"`
42+
Name string `json:"name"`
43+
Metadata metadata.Metadata `json:"metadata"`
44+
}
45+
46+
type AuditRecordsList struct {
47+
AuditRecords []AuditRecord
48+
Group *utils.Group
49+
Page utils.Page
50+
}
51+
52+
// AuditRecordRQLSchema is the schema for audit record RQL queries. This is a flattened version of the AuditRecord struct.
53+
// This is needed because the RQL parser does not support nested structs.
54+
type AuditRecordRQLSchema struct {
55+
ID string `rql:"name=id,type=string"`
56+
Event string `rql:"name=event,type=string"`
57+
ActorID string `rql:"name=actor_id,type=string"`
58+
ActorType string `rql:"name=actor_type,type=string"`
59+
ActorName string `rql:"name=actor_name,type=string"`
60+
ResourceID string `rql:"name=resource_id,type=string"`
61+
ResourceType string `rql:"name=resource_type,type=string"`
62+
ResourceName string `rql:"name=resource_name,type=string"`
63+
TargetID string `rql:"name=target_id,type=string"`
64+
TargetType string `rql:"name=target_type,type=string"`
65+
TargetName string `rql:"name=target_name,type=string"`
66+
OccurredAt time.Time `rql:"name=occurred_at,type=datetime"`
67+
OrgID string `rql:"name=org_id,type=string"`
68+
RequestID string `rql:"name=request_id,type=string"`
69+
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
70+
IdempotencyKey string `rql:"name=idempotency_key,type=string"`
71+
}

core/auditrecord/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (s *Service) Export(ctx context.Context, query *rql.Query) (io.Reader, stri
137137

138138
func computeHash(auditRecord AuditRecord) string {
139139
// Normalize event and IDs - trim spaces and lowercase for consistency
140-
normalisedEvent := strings.ToLower(strings.TrimSpace(auditRecord.Event))
140+
normalisedEvent := strings.ToLower(strings.TrimSpace(auditRecord.Event.String()))
141141
normalisedActorID := strings.ToLower(strings.TrimSpace(auditRecord.Actor.ID))
142142
normalisedResourceID := strings.ToLower(strings.TrimSpace(auditRecord.Resource.ID))
143143
normalisedOrgID := strings.ToLower(strings.TrimSpace(auditRecord.OrgID))

core/invitation/service.go

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import (
1515
"go.uber.org/zap"
1616

1717
"github.com/mcuadros/go-defaults"
18+
"github.com/raystack/frontier/core/auditrecord/models"
1819
"github.com/raystack/frontier/core/authenticate"
1920
"github.com/raystack/frontier/core/policy"
2021
"github.com/raystack/frontier/core/preference"
22+
pkgAuditRecord "github.com/raystack/frontier/pkg/auditrecord"
2123
"gopkg.in/mail.v2"
2224

2325
"github.com/raystack/frontier/pkg/mailer"
@@ -67,30 +69,37 @@ type PreferencesService interface {
6769
LoadPlatformPreferences(ctx context.Context) (map[string]string, error)
6870
}
6971

72+
type AuditRecordRepository interface {
73+
Create(ctx context.Context, auditRecord models.AuditRecord) (models.AuditRecord, error)
74+
}
75+
7076
type Service struct {
71-
dialer mailer.Dialer
72-
repo Repository
73-
orgSvc OrganizationService
74-
groupSvc GroupService
75-
userService UserService
76-
relationService RelationService
77-
policyService PolicyService
78-
prefService PreferencesService
77+
dialer mailer.Dialer
78+
repo Repository
79+
orgSvc OrganizationService
80+
groupSvc GroupService
81+
userService UserService
82+
relationService RelationService
83+
policyService PolicyService
84+
prefService PreferencesService
85+
auditRecordRepository AuditRecordRepository
7986
}
8087

8188
func NewService(dialer mailer.Dialer, repo Repository,
8289
orgSvc OrganizationService, grpSvc GroupService,
8390
userService UserService, relService RelationService,
84-
policyService PolicyService, prefService PreferencesService) *Service {
91+
policyService PolicyService, prefService PreferencesService,
92+
auditRecordRepository AuditRecordRepository) *Service {
8593
return &Service{
86-
dialer: dialer,
87-
repo: repo,
88-
orgSvc: orgSvc,
89-
groupSvc: grpSvc,
90-
userService: userService,
91-
relationService: relService,
92-
policyService: policyService,
93-
prefService: prefService,
94+
dialer: dialer,
95+
repo: repo,
96+
orgSvc: orgSvc,
97+
groupSvc: grpSvc,
98+
userService: userService,
99+
relationService: relService,
100+
policyService: policyService,
101+
prefService: prefService,
102+
auditRecordRepository: auditRecordRepository,
94103
}
95104
}
96105

@@ -359,6 +368,35 @@ func (s Service) Accept(ctx context.Context, id uuid.UUID) error {
359368
}
360369
}
361370

371+
// fetch organization details for audit record
372+
org, err := s.orgSvc.Get(ctx, invite.OrgID)
373+
if err != nil {
374+
return err
375+
}
376+
377+
// create audit record for invitation acceptance
378+
_, err = s.auditRecordRepository.Create(ctx, models.AuditRecord{
379+
Event: pkgAuditRecord.OrganizationInvitationAcceptedEvent,
380+
Resource: models.Resource{
381+
ID: org.ID,
382+
Type: pkgAuditRecord.OrganizationType,
383+
Name: org.Title,
384+
},
385+
Target: &models.Target{
386+
ID: userOb.ID,
387+
Type: pkgAuditRecord.UserType,
388+
Name: userOb.Title,
389+
Metadata: map[string]any{
390+
"email": userOb.Email,
391+
},
392+
},
393+
OrgID: invite.OrgID,
394+
OccurredAt: time.Now(),
395+
})
396+
if err != nil {
397+
return err
398+
}
399+
362400
// delete the invitation
363401
return s.Delete(ctx, id)
364402
}

core/invitation/service_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"testing"
66

7+
auditMocks "github.com/raystack/frontier/core/auditrecord/mocks"
78
"github.com/raystack/frontier/core/authenticate"
89
"github.com/raystack/frontier/internal/bootstrap/schema"
910

@@ -19,7 +20,7 @@ import (
1920
)
2021

2122
func mockService(t *testing.T) (*mocks2.Dialer, *mocks.Repository, *mocks.OrganizationService, *mocks.GroupService,
22-
*mocks.UserService, *mocks.RelationService, *mocks.PolicyService, *mocks.PreferencesService) {
23+
*mocks.UserService, *mocks.RelationService, *mocks.PolicyService, *mocks.PreferencesService, *auditMocks.Repository) {
2324
t.Helper()
2425
dialer := mocks2.NewDialer(t)
2526
repo := mocks.NewRepository(t)
@@ -29,7 +30,8 @@ func mockService(t *testing.T) (*mocks2.Dialer, *mocks.Repository, *mocks.Organi
2930
relationService := mocks.NewRelationService(t)
3031
policyService := mocks.NewPolicyService(t)
3132
prefService := mocks.NewPreferencesService(t)
32-
return dialer, repo, orgService, groupService, userService, relationService, policyService, prefService
33+
auditRecordRepo := auditMocks.NewRepository(t)
34+
return dialer, repo, orgService, groupService, userService, relationService, policyService, prefService, auditRecordRepo
3335
}
3436

3537
func TestService_Create(t *testing.T) {
@@ -48,7 +50,7 @@ func TestService_Create(t *testing.T) {
4850
},
4951
err: invitation.ErrAlreadyMember,
5052
setup: func() *invitation.Service {
51-
dialer, repo, orgService, groupService, userService, relationService, policyService, prefService := mockService(t)
53+
dialer, repo, orgService, groupService, userService, relationService, policyService, prefService, auditRecordRepo := mockService(t)
5254

5355
prefService.EXPECT().LoadPlatformPreferences(mock.Anything).Return(map[string]string{}, nil)
5456
orgService.EXPECT().Get(mock.Anything, "org-id").Return(organization.Organization{
@@ -69,7 +71,7 @@ func TestService_Create(t *testing.T) {
6971
}, nil)
7072

7173
return invitation.NewService(dialer, repo, orgService, groupService,
72-
userService, relationService, policyService, prefService)
74+
userService, relationService, policyService, prefService, auditRecordRepo)
7375
},
7476
},
7577
}

0 commit comments

Comments
 (0)