fleet/server/datastore/mysql/activities.go
Jahziel Villasana-Espinoza 447b9e8b25
feat: don't remove user email from activity feed when user deleted (#14975)
# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2023-11-09 15:50:01 -05:00

166 lines
4.2 KiB
Go

package mysql
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
)
// NewActivity stores an activity item that the user performed
func (ds *Datastore) NewActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
detailsBytes, err := json.Marshal(activity)
if err != nil {
return ctxerr.Wrap(ctx, err, "marshaling activity details")
}
var userID *uint
var userName *string
var userEmail *string
if user != nil {
userID = &user.ID
userName = &user.Name
userEmail = &user.Email
}
cols := []string{"user_id", "user_name", "activity_type", "details"}
args := []any{
userID,
userName,
activity.ActivityName(),
detailsBytes,
}
if userEmail != nil {
args = append(args, userEmail)
cols = append(cols, "user_email")
}
insertStmt := `INSERT INTO activities (%s) VALUES (%s)`
sql := fmt.Sprintf(insertStmt, strings.Join(cols, ","), strings.Repeat("?,", len(cols)-1)+"?")
_, err = ds.writer(ctx).ExecContext(ctx,
sql,
args...,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "new activity")
}
return nil
}
// ListActivities returns a slice of activities performed across the organization
func (ds *Datastore) ListActivities(ctx context.Context, opt fleet.ListActivitiesOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) {
// Fetch activities
activities := []*fleet.Activity{}
activitiesQ := `
SELECT
a.id,
a.user_id,
a.created_at,
a.activity_type,
a.details,
a.user_name as name,
a.streamed,
a.user_email
FROM activities a
WHERE true`
var args []interface{}
if opt.Streamed != nil {
activitiesQ += " AND a.streamed = ?"
args = append(args, *opt.Streamed)
}
opt.ListOptions.IncludeMetadata = !(opt.ListOptions.UsesCursorPagination())
activitiesQ, args = appendListOptionsWithCursorToSQL(activitiesQ, args, &opt.ListOptions)
err := sqlx.SelectContext(ctx, ds.reader(ctx), &activities, activitiesQ, args...)
if err == sql.ErrNoRows {
return nil, nil, ctxerr.Wrap(ctx, notFound("Activity"))
} else if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "select activities")
}
// Fetch users as a stand-alone query (because of performance reasons)
lookup := make(map[uint][]int)
for idx, a := range activities {
if a.ActorID != nil {
lookup[*a.ActorID] = append(lookup[*a.ActorID], idx)
}
}
if len(lookup) != 0 {
usersQ := `
SELECT u.id, u.name, u.gravatar_url, u.email
FROM users u
WHERE id IN (?)
`
userIDs := make([]uint, 0, len(lookup))
for k := range lookup {
userIDs = append(userIDs, k)
}
usersQ, usersArgs, err := sqlx.In(usersQ, userIDs)
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "Error binding usersIDs")
}
var usersR []struct {
ID uint `db:"id"`
Name string `db:"name"`
GravatarUrl string `db:"gravatar_url"`
Email string `db:"email"`
}
err = sqlx.SelectContext(ctx, ds.reader(ctx), &usersR, usersQ, usersArgs...)
if err != nil && err != sql.ErrNoRows {
return nil, nil, ctxerr.Wrap(ctx, err, "selecting users")
}
for _, r := range usersR {
entries, ok := lookup[r.ID]
if !ok {
continue
}
email := r.Email
gravatar := r.GravatarUrl
name := r.Name
for _, idx := range entries {
activities[idx].ActorEmail = &email
activities[idx].ActorGravatar = &gravatar
activities[idx].ActorFullName = &name
}
}
}
var metaData *fleet.PaginationMetadata
if opt.ListOptions.IncludeMetadata {
metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0}
if len(activities) > int(opt.ListOptions.PerPage) {
metaData.HasNextResults = true
activities = activities[:len(activities)-1]
}
}
return activities, metaData, nil
}
func (ds *Datastore) MarkActivitiesAsStreamed(ctx context.Context, activityIDs []uint) error {
stmt := `UPDATE activities SET streamed = true WHERE id IN (?);`
query, args, err := sqlx.In(stmt, activityIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "sqlx.In mark activities as streamed")
}
if _, err := ds.writer(ctx).ExecContext(ctx, query, args...); err != nil {
return ctxerr.Wrap(ctx, err, "exec mark activities as streamed")
}
return nil
}