diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index 83ec34c9b..e29bb2369 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -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}, + } + }) } //////