mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 08:28:52 +00:00
# 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
166 lines
4.2 KiB
Go
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
|
|
}
|