fleet/server/contexts/viewer/viewer.go
Victor Lyuboslavsky 913a5904c8
Move NewActivity to activity bounded context (#39521)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38536 

This PR moves all logic to create new activities to activity bounded
context.
The old service and ActivityModule methods are not facades that route to
the new activity bounded context. The facades will be removed in a
subsequent PR.

# Checklist for submitter

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

## 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

* **New Features**
* Added webhook support for activity events with configurable endpoint
and enable/disable settings.
* Enhanced automation-initiated activity creation without requiring a
user context.
* Improved activity service architecture with centralized creation and
management.

* **Improvements**
* Refactored activity creation to use a dedicated service layer for
better separation of concerns.
* Added support for host-specific and automation-originated activities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 14:11:03 -06:00

158 lines
4.1 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]
}
// systemUserName is the name used for the synthetic system user. It matches
// the activity bounded context's ActivityAutomationAuthor constant ("Fleet").
const systemUserName = "Fleet"
// systemUser is a synthetic user for internal system operations.
var systemUser = &fleet.User{
Name: systemUserName,
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})
}