fleet/server/contexts/viewer/viewer.go
Victor Lyuboslavsky 7deade8057
Activity bounded context: /api/latest/fleet/activities (2 of 2) (#38478)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #37806 

Removed `ds.ListActivities` from the legacy datastore and updated
code/tests to use the new activity bounded context instead.

The changes to `cron.go` and most changes to `mysql/activities_test.go`
will eventually be migrated to the activity bounded context. The current
changes are an intermediate step.

The issues tracked by https://github.com/fleetdm/fleet/issues/38234 will
be addressed in additional/parallel PRs shortly.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
  - Done in the previous PR

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Migrated activity retrieval from direct datastore calls to a
service-based architecture for improved maintainability and consistency.
* Enhanced system context handling for background automation tasks to
ensure proper authorization during scheduled operations.
* Streamlined activity recording for automated processes with dedicated
system identity tracking.

* **Tests**
* Updated test infrastructure with new helpers for activity service
integration across test suites.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
2026-01-23 07:42:09 -06:00

155 lines
4 KiB
Go

// Package viewer enables setting and reading the current
// user contexts
package viewer
import (
"context"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
type key int
const viewerKey key = 0
// NewContext creates a new context with the current user information.
func NewContext(ctx context.Context, v Viewer) context.Context {
return context.WithValue(ctx, viewerKey, v)
}
// FromContext returns the current user information if present.
func FromContext(ctx context.Context) (Viewer, bool) {
v, ok := ctx.Value(viewerKey).(Viewer)
return v, ok
}
// Viewer holds information about the current
// user and the user's session
type Viewer struct {
User *fleet.User
Session *fleet.Session
}
// UserID is a helper that enables quick access to the user ID of the current
// user.
func (v Viewer) UserID() uint {
if v.User != nil {
return v.User.ID
}
return 0
}
// Email is a helper that enables quick access to the email of the current
// user.
func (v Viewer) Email() string {
if v.User != nil {
return v.User.Email
}
return ""
}
// FullName is a helper that enables quick access to the full name of the
// current user.
func (v Viewer) FullName() string {
if v.User != nil {
return v.User.Name
}
return ""
}
// SessionID returns the current user's session ID
func (v Viewer) SessionID() uint {
if v.Session != nil {
return v.Session.ID
}
return 0
}
// IsUserID returns true if the given user id the same as the user which is
// represented by this ViewerContext
func (v Viewer) IsUserID(id uint) bool {
return v.UserID() == id
}
// IsLoggedIn determines whether or not the current VC is attached to a user
// account
func (v Viewer) IsLoggedIn() bool {
if v.Session != nil {
// Without having access to a service to call GetInfoAboutSession(id),
// we can't synchronously check the database here.
if v.Session.ID != 0 {
return true
}
}
return false
}
// CanPerformActions returns a bool indicating the current user's ability to
// perform the most basic actions on the site
func (v Viewer) CanPerformActions() bool {
if v.User != nil {
return v.IsLoggedIn() && !v.User.IsAdminForcedPasswordReset()
}
return false
}
// CanPerformPasswordReset returns a bool indicating the current user's
// ability to perform a password reset (in the case they have been required by
// the admin).
func (v Viewer) CanPerformPasswordReset() bool {
if v.User != nil {
return v.IsLoggedIn() && v.User.IsAdminForcedPasswordReset()
}
return false
}
// GetDiagnosticContext implements ctxerr.ErrorContextProvider
func (v *Viewer) GetDiagnosticContext() map[string]any {
vdata := map[string]any{
"is_logged_in": v.IsLoggedIn(),
}
if v.User != nil {
vdata["sso_enabled"] = v.User.SSOEnabled
}
return map[string]any{
"viewer": vdata,
}
}
// GetTelemetryContext implements ctxerr.ErrorContextProvider
func (v *Viewer) GetTelemetryContext() map[string]any {
if v.User == nil {
return nil
}
return map[string]any{
"user.id": v.User.ID,
"user.email": maskEmail(v.User.Email),
}
}
// maskEmail anonymizes an email address for telemetry by showing only
// the first character of the local part and the full domain.
// Example: "john.doe@example.com" -> "j***@example.com"
func maskEmail(email string) string {
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 || len(parts[0]) == 0 {
return "***"
}
return string(parts[0][0]) + "***@" + parts[1]
}
// systemUser is a synthetic user for internal system operations.
// The Name uses ActivityAutomationAuthor to align with system-initiated activities.
var systemUser = &fleet.User{
Name: fleet.ActivityAutomationAuthor,
GlobalRole: ptr.String(fleet.RoleAdmin),
}
// NewSystemContext returns a context with system-level (admin) privileges.
// Use this for cron jobs, internal service calls, and other system operations
// that need to bypass user-based authorization.
func NewSystemContext(ctx context.Context) context.Context {
return NewContext(ctx, Viewer{User: systemUser})
}