feat(audit): register the audited event surface
One config-gated block in RegisterListeners maps every opted-in event to its audit entry. Events with interface-typed doers are decoded via a small doer ref that distinguishes link shares by their hash field.
This commit is contained in:
parent
5f4a21a4c5
commit
869bec38b5
|
|
@ -22,6 +22,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/audit"
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
|
@ -82,6 +83,310 @@ func RegisterListeners() {
|
|||
// Internal delivery listener — one message per webhook with its own retry lifecycle
|
||||
events.RegisterListener((&WebhookDeliveryEvent{}).Name(), &WebhookDeliveryListener{})
|
||||
}
|
||||
if config.AuditEnabled.GetBool() {
|
||||
registerEventsForAuditLogging()
|
||||
}
|
||||
}
|
||||
|
||||
// auditDoerRef decodes the doer of events whose Doer field is an interface
|
||||
// and thus can't be unmarshaled into the event struct directly.
|
||||
type auditDoerRef struct {
|
||||
ID int64 `json:"id"`
|
||||
Hash string `json:"hash"` // only set when the doer is a link share
|
||||
}
|
||||
|
||||
func auditActorFromDoerRef(d *auditDoerRef) audit.Actor {
|
||||
if d == nil {
|
||||
return audit.SystemActor()
|
||||
}
|
||||
if d.Hash != "" {
|
||||
return audit.LinkShareActor(d.ID)
|
||||
}
|
||||
return audit.ActorFromDoerID(d.ID)
|
||||
}
|
||||
|
||||
func auditActorFromUser(u *user.User) audit.Actor {
|
||||
if u == nil {
|
||||
return audit.SystemActor()
|
||||
}
|
||||
return audit.ActorFromDoerID(u.ID)
|
||||
}
|
||||
|
||||
// registerEventsForAuditLogging opts events into audit logging. This block is
|
||||
// the catalog of the entire audited surface — an event without a registration
|
||||
// here is not audited.
|
||||
func registerEventsForAuditLogging() {
|
||||
// Auth boundary
|
||||
audit.RegisterEventForAudit(func(e *user.LoginSucceededEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionLoginSucceeded,
|
||||
Actor: audit.UserActor(e.User.ID),
|
||||
Target: audit.UserTarget(e.User.ID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *user.LoginFailedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionLoginFailed,
|
||||
Actor: audit.UserActor(e.User.ID),
|
||||
Target: audit.UserTarget(e.User.ID),
|
||||
Outcome: audit.OutcomeFailure,
|
||||
Reason: "wrong password",
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *user.LogoutEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionLogout,
|
||||
Actor: audit.UserActor(e.UserID),
|
||||
Target: audit.UserTarget(e.UserID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *APITokenIssuedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionAPITokenIssued,
|
||||
Actor: audit.UserActor(e.DoerID),
|
||||
Target: audit.APITokenTarget(e.TokenID),
|
||||
Metadata: map[string]any{"owner_id": e.OwnerID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *APITokenRevokedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionAPITokenRevoked,
|
||||
Actor: audit.UserActor(e.DoerID),
|
||||
Target: audit.APITokenTarget(e.TokenID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *APITokenUsedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionAPITokenUsed,
|
||||
Actor: audit.UserActor(e.OwnerID),
|
||||
Target: audit.APITokenTarget(e.TokenID),
|
||||
}
|
||||
})
|
||||
|
||||
// Users
|
||||
audit.RegisterEventForAudit(func(e *user.CreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionUserCreated,
|
||||
Actor: audit.UserActor(e.User.ID),
|
||||
Target: audit.UserTarget(e.User.ID),
|
||||
}
|
||||
})
|
||||
|
||||
// Tasks
|
||||
audit.RegisterEventForAudit(func(e *TaskCreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskCreated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskUpdatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskUpdated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskDeletedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskDeleted,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskAssigneeCreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskAssigneeAdded,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"assignee_id": e.Assignee.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskAssigneeDeletedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskAssigneeRemoved,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"assignee_id": e.Assignee.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskCommentCreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskCommentCreated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"comment_id": e.Comment.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskCommentUpdatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskCommentUpdated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"comment_id": e.Comment.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskCommentDeletedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskCommentDeleted,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"comment_id": e.Comment.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskAttachmentCreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskAttachmentCreated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"attachment_id": e.Attachment.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskAttachmentDeletedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskAttachmentDeleted,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{"attachment_id": e.Attachment.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskRelationCreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskRelationCreated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{
|
||||
"other_task_id": e.Relation.OtherTaskID,
|
||||
"relation_kind": e.Relation.RelationKind,
|
||||
},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TaskRelationDeletedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTaskRelationDeleted,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TaskTarget(e.Task.ID),
|
||||
Metadata: map[string]any{
|
||||
"other_task_id": e.Relation.OtherTaskID,
|
||||
"relation_kind": e.Relation.RelationKind,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Projects
|
||||
audit.RegisterEventForAudit(func(e *ProjectCreatedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionProjectCreated,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.ProjectTarget(e.Project.ID),
|
||||
}
|
||||
})
|
||||
audit.RegisterEventNameForAudit((&ProjectUpdatedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) {
|
||||
e := &struct {
|
||||
Project *Project `json:"project"`
|
||||
Doer *auditDoerRef `json:"doer"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionProjectUpdated,
|
||||
Actor: auditActorFromDoerRef(e.Doer),
|
||||
Target: audit.ProjectTarget(e.Project.ID),
|
||||
}, nil
|
||||
})
|
||||
audit.RegisterEventNameForAudit((&ProjectDeletedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) {
|
||||
e := &struct {
|
||||
Project *Project `json:"project"`
|
||||
Doer *auditDoerRef `json:"doer"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionProjectDeleted,
|
||||
Actor: auditActorFromDoerRef(e.Doer),
|
||||
Target: audit.ProjectTarget(e.Project.ID),
|
||||
}, nil
|
||||
})
|
||||
audit.RegisterEventNameForAudit((&ProjectSharedWithUserEvent{}).Name(), func(payload []byte) (*audit.Entry, error) {
|
||||
e := &struct {
|
||||
Project *Project `json:"project"`
|
||||
User *user.User `json:"user"`
|
||||
Doer *auditDoerRef `json:"doer"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionProjectSharedWithUser,
|
||||
Actor: auditActorFromDoerRef(e.Doer),
|
||||
Target: audit.ProjectTarget(e.Project.ID),
|
||||
Metadata: map[string]any{"user_id": e.User.ID},
|
||||
}, nil
|
||||
})
|
||||
audit.RegisterEventNameForAudit((&ProjectSharedWithTeamEvent{}).Name(), func(payload []byte) (*audit.Entry, error) {
|
||||
e := &struct {
|
||||
Project *Project `json:"project"`
|
||||
Team *Team `json:"team"`
|
||||
Doer *auditDoerRef `json:"doer"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionProjectSharedWithTeam,
|
||||
Actor: auditActorFromDoerRef(e.Doer),
|
||||
Target: audit.ProjectTarget(e.Project.ID),
|
||||
Metadata: map[string]any{"team_id": e.Team.ID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// Teams
|
||||
audit.RegisterEventNameForAudit((&TeamCreatedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) {
|
||||
e := &struct {
|
||||
Team *Team `json:"team"`
|
||||
Doer *auditDoerRef `json:"doer"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTeamCreated,
|
||||
Actor: auditActorFromDoerRef(e.Doer),
|
||||
Target: audit.TeamTarget(e.Team.ID),
|
||||
}, nil
|
||||
})
|
||||
audit.RegisterEventNameForAudit((&TeamDeletedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) {
|
||||
e := &struct {
|
||||
Team *Team `json:"team"`
|
||||
Doer *auditDoerRef `json:"doer"`
|
||||
}{}
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTeamDeleted,
|
||||
Actor: auditActorFromDoerRef(e.Doer),
|
||||
Target: audit.TeamTarget(e.Team.ID),
|
||||
}, nil
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TeamMemberAddedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTeamMemberAdded,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TeamTarget(e.Team.ID),
|
||||
Metadata: map[string]any{"member_id": e.Member.ID},
|
||||
}
|
||||
})
|
||||
audit.RegisterEventForAudit(func(e *TeamMemberRemovedEvent) *audit.Entry {
|
||||
return &audit.Entry{
|
||||
Action: audit.ActionTeamMemberRemoved,
|
||||
Actor: auditActorFromUser(e.Doer),
|
||||
Target: audit.TeamTarget(e.Team.ID),
|
||||
Metadata: map[string]any{"member_id": e.Member.ID},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//////
|
||||
|
|
|
|||
Loading…
Reference in New Issue