Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.11.0
PLUGIN_PACKAGES += mattermost-plugin-jira-v4.5.0
PLUGIN_PACKAGES += mattermost-plugin-jira-v4.5.1
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.6.1
PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.11.0
Expand Down
76 changes: 76 additions & 0 deletions server/channels/api4/recap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
)

func (api *API) InitRecap() {
Expand All @@ -27,6 +28,19 @@ func requireRecapsEnabled(c *Context) {
}
}

// addRecapChannelIDsToAuditRec extracts channel IDs from a recap and adds them to the audit record.
// This logs which channels' content was accessed through the recap operation.
func addRecapChannelIDsToAuditRec(auditRec *model.AuditRecord, recap *model.Recap) {
if len(recap.Channels) == 0 {
return
}
channelIDs := make([]string, 0, len(recap.Channels))
for _, channel := range recap.Channels {
channelIDs = append(channelIDs, channel.ChannelId)
}
model.AddEventParameterToAuditRec(auditRec, "channel_ids", channelIDs)
}

func createRecap(c *Context, w http.ResponseWriter, r *http.Request) {
requireRecapsEnabled(c)
if c.Err != nil {
Expand Down Expand Up @@ -54,12 +68,22 @@ func createRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec := c.MakeAuditRecord(model.AuditEventCreateRecap, model.AuditStatusFail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
auditRec.AddEventObjectType("recap")
model.AddEventParameterToAuditRec(auditRec, "channel_ids", req.ChannelIds)
model.AddEventParameterToAuditRec(auditRec, "title", req.Title)
model.AddEventParameterToAuditRec(auditRec, "agent_id", req.AgentID)

recap, err := c.App.CreateRecap(c.AppContext, req.Title, req.ChannelIds, req.AgentID)
if err != nil {
c.Err = err
return
}

auditRec.Success()
auditRec.AddEventResultState(recap)

w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(recap); err != nil {
c.Logger.Warn("Error encoding response", mlog.Err(err))
Expand All @@ -77,6 +101,11 @@ func getRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec := c.MakeAuditRecord(model.AuditEventGetRecap, model.AuditStatusFail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
auditRec.AddEventObjectType("recap")
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)

recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
if err != nil {
c.Err = err
Expand All @@ -88,6 +117,12 @@ func getRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

// Log channel IDs accessed through viewing this recap summary
addRecapChannelIDsToAuditRec(auditRec, recap)

auditRec.Success()
auditRec.AddEventResultState(recap)

if err := json.NewEncoder(w).Encode(recap); err != nil {
c.Logger.Warn("Error encoding response", mlog.Err(err))
}
Expand All @@ -99,12 +134,22 @@ func getRecaps(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec := c.MakeAuditRecord(model.AuditEventGetRecaps, model.AuditStatusFail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelAPI)
model.AddEventParameterToAuditRec(auditRec, "page", c.Params.Page)
model.AddEventParameterToAuditRec(auditRec, "per_page", c.Params.PerPage)

recaps, err := c.App.GetRecapsForUser(c.AppContext, c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}

auditRec.Success()
if len(recaps) > 0 {
auditRec.AddMeta("recap_count", len(recaps))
}

if err := json.NewEncoder(w).Encode(recaps); err != nil {
c.Logger.Warn("Error encoding response", mlog.Err(err))
}
Expand All @@ -121,6 +166,11 @@ func markRecapAsRead(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec := c.MakeAuditRecord(model.AuditEventMarkRecapAsRead, model.AuditStatusFail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
auditRec.AddEventObjectType("recap")
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)

// Check permissions
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
if err != nil {
Expand All @@ -133,12 +183,17 @@ func markRecapAsRead(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec.AddEventPriorState(recap)

updatedRecap, err := c.App.MarkRecapAsRead(c.AppContext, recap)
if err != nil {
c.Err = err
return
}

auditRec.Success()
auditRec.AddEventResultState(updatedRecap)

if err := json.NewEncoder(w).Encode(updatedRecap); err != nil {
c.Logger.Warn("Error encoding response", mlog.Err(err))
}
Expand All @@ -155,6 +210,11 @@ func regenerateRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec := c.MakeAuditRecord(model.AuditEventRegenerateRecap, model.AuditStatusFail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
auditRec.AddEventObjectType("recap")
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)

// Check permissions
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
if err != nil {
Expand All @@ -167,12 +227,20 @@ func regenerateRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

// Log channel IDs that will be re-summarized
addRecapChannelIDsToAuditRec(auditRec, recap)

auditRec.AddEventPriorState(recap)

updatedRecap, err := c.App.RegenerateRecap(c.AppContext, c.AppContext.Session().UserId, recap)
if err != nil {
c.Err = err
return
}

auditRec.Success()
auditRec.AddEventResultState(updatedRecap)

if err := json.NewEncoder(w).Encode(updatedRecap); err != nil {
c.Logger.Warn("Error encoding response", mlog.Err(err))
}
Expand All @@ -189,6 +257,11 @@ func deleteRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec := c.MakeAuditRecord(model.AuditEventDeleteRecap, model.AuditStatusFail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
auditRec.AddEventObjectType("recap")
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)

// Check permissions
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
if err != nil {
Expand All @@ -201,10 +274,13 @@ func deleteRecap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}

auditRec.AddEventPriorState(recap)

if err := c.App.DeleteRecap(c.AppContext, c.Params.RecapId); err != nil {
c.Err = err
return
}

auditRec.Success()
ReturnStatusOK(w)
}
10 changes: 10 additions & 0 deletions server/public/model/audit_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,16 @@ const (
AuditEventBurnPost = "burnPost" // burn a post that was hidden due to burn on read
)

// Recaps
const (
AuditEventCreateRecap = "createRecap" // create recap summarizing channel content
AuditEventGetRecap = "getRecap" // view a single recap
AuditEventGetRecaps = "getRecaps" // list user's recaps
AuditEventMarkRecapAsRead = "markRecapAsRead" // mark recap as read
AuditEventRegenerateRecap = "regenerateRecap" // regenerate recap with updated channel content
AuditEventDeleteRecap = "deleteRecap" // delete recap
)

// Preferences
const (
AuditEventDeletePreferences = "deletePreferences" // delete user preferences
Expand Down
21 changes: 21 additions & 0 deletions server/public/model/recap.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,24 @@ const (
RecapStatusCompleted = "completed"
RecapStatusFailed = "failed"
)

// Auditable returns safe-to-log fields for audit logging
func (r *Recap) Auditable() map[string]any {
channelIDs := make([]string, 0, len(r.Channels))
for _, channel := range r.Channels {
channelIDs = append(channelIDs, channel.ChannelId)
}

return map[string]any{
"id": r.Id,
"user_id": r.UserId,
"title": r.Title,
"status": r.Status,
"channel_ids": channelIDs,
"total_message_count": r.TotalMessageCount,
"bot_id": r.BotID,
"create_at": r.CreateAt,
"update_at": r.UpdateAt,
"read_at": r.ReadAt,
}
}
Loading
Loading