mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
1048 lines
36 KiB
Go
1048 lines
36 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/go-kit/log/level"
|
|
)
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Ping device endpoint
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type devicePingRequest struct{}
|
|
|
|
type deviceAuthPingRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *deviceAuthPingRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type devicePingResponse struct{}
|
|
|
|
func (r devicePingResponse) Error() error { return nil }
|
|
|
|
func (r devicePingResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
writeCapabilitiesHeader(w, fleet.GetServerDeviceCapabilities())
|
|
}
|
|
|
|
// NOTE: we're intentionally not reading the capabilities header in this
|
|
// endpoint as is unauthenticated and we don't want to trust whatever comes in
|
|
// there.
|
|
func devicePingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
svc.DisableAuthForPing(ctx)
|
|
return devicePingResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) DisableAuthForPing(ctx context.Context) {
|
|
// skipauth: this endpoint is intentionally public to allow devices to ping
|
|
// the server and among other things, get the fleet.Capabilities header to
|
|
// determine which capabilities are enabled in the server.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Fleet Desktop endpoints
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type fleetDesktopResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
fleet.DesktopSummary
|
|
}
|
|
|
|
func (r fleetDesktopResponse) Error() error { return r.Err }
|
|
|
|
type getFleetDesktopRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *getFleetDesktopRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
// getFleetDesktopEndpoint is meant to be the only API endpoint used by Fleet Desktop. This
|
|
// endpoint should not include any kind of identifying information about the host.
|
|
func getFleetDesktopEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
sum, err := svc.GetFleetDesktopSummary(ctx)
|
|
if err != nil {
|
|
return fleetDesktopResponse{Err: err}, nil
|
|
}
|
|
return fleetDesktopResponse{DesktopSummary: sum}, nil
|
|
}
|
|
|
|
func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSummary, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return fleet.DesktopSummary{}, fleet.ErrMissingLicense
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get Current Device's Host
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getDeviceHostRequest struct {
|
|
Token string `url:"token"`
|
|
ExcludeSoftware bool `query:"exclude_software,optional"`
|
|
}
|
|
|
|
func (r *getDeviceHostRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type getDeviceHostResponse struct {
|
|
Host *HostDetailResponse `json:"host"`
|
|
SelfService bool `json:"self_service"`
|
|
OrgLogoURL string `json:"org_logo_url"`
|
|
OrgLogoURLLightBackground string `json:"org_logo_url_light_background"`
|
|
OrgContactURL string `json:"org_contact_url"`
|
|
Err error `json:"error,omitempty"`
|
|
License fleet.LicenseInfo `json:"license"`
|
|
GlobalConfig fleet.DeviceGlobalConfig `json:"global_config"`
|
|
}
|
|
|
|
func (r getDeviceHostResponse) Error() error { return r.Err }
|
|
|
|
func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getDeviceHostRequest)
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
|
|
// must still load the full host details, as it returns more information
|
|
opts := fleet.HostDetailOptions{
|
|
IncludeCVEScores: false,
|
|
IncludePolicies: false,
|
|
ExcludeSoftware: req.ExcludeSoftware,
|
|
}
|
|
hostDetails, err := svc.GetHost(ctx, host.ID, opts)
|
|
if err != nil {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
|
|
resp, err := hostDetailResponseForHost(ctx, svc, hostDetails)
|
|
if err != nil {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
|
|
// the org logo URL config is required by the frontend to render the page;
|
|
// we need to be careful with what we return from AppConfig in the response
|
|
// as this is a weakly authenticated endpoint (with the device auth token).
|
|
ac, err := svc.AppConfigObfuscated(ctx)
|
|
if err != nil {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
|
|
license, err := svc.License(ctx)
|
|
if err != nil {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
|
|
// Scrub sensitive data from the host response for iOS and iPadOS devices
|
|
if authzCtx, ok := authz.FromContext(ctx); ok && authzCtx.AuthnMethod() == authz.AuthnDeviceURL {
|
|
if host.Platform == "ios" || host.Platform == "ipados" {
|
|
resp.HardwareSerial = ""
|
|
resp.UUID = ""
|
|
resp.PrimaryMac = ""
|
|
resp.TeamName = nil
|
|
resp.MDM.Profiles = nil
|
|
resp.Labels = nil
|
|
resp.Hostname = ""
|
|
resp.ComputerName = ""
|
|
resp.DisplayText = ""
|
|
resp.DisplayName = ""
|
|
|
|
// Scrub sensitive data from the license response
|
|
scrubbedLicense := *license
|
|
scrubbedLicense.Organization = ""
|
|
scrubbedLicense.DeviceCount = 0
|
|
scrubbedLicense.Expiration = time.Time{}
|
|
license = &scrubbedLicense
|
|
}
|
|
}
|
|
|
|
resp.DEPAssignedToFleet = ptr.Bool(false)
|
|
if ac.MDM.EnabledAndConfigured && license.IsPremium() {
|
|
hdep, err := svc.GetHostDEPAssignment(ctx, host)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
resp.DEPAssignedToFleet = ptr.Bool(hdep.IsDEPAssignedToFleet())
|
|
}
|
|
|
|
softwareInventoryEnabled := ac.Features.EnableSoftwareInventory
|
|
requireAllSoftware := ac.MDM.MacOSSetup.RequireAllSoftware
|
|
if resp.TeamID != nil {
|
|
// load the team to get the device's team's software inventory config.
|
|
tm, err := svc.GetTeam(ctx, *resp.TeamID)
|
|
if err != nil && !fleet.IsNotFound(err) {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
if tm != nil {
|
|
softwareInventoryEnabled = tm.Config.Features.EnableSoftwareInventory // TODO: We should look for opportunities to fix the confusing name of the `global_config` object in the API response. Also, how can we better clarify/document the expected order of precedence for team and global feature flags?
|
|
requireAllSoftware = tm.Config.MDM.MacOSSetup.RequireAllSoftware
|
|
}
|
|
}
|
|
|
|
hasSelfService := false
|
|
if softwareInventoryEnabled {
|
|
hasSelfService, err = svc.HasSelfServiceSoftwareInstallers(ctx, host)
|
|
if err != nil {
|
|
return getDeviceHostResponse{Err: err}, nil
|
|
}
|
|
}
|
|
|
|
deviceGlobalConfig := fleet.DeviceGlobalConfig{
|
|
MDM: fleet.DeviceGlobalMDMConfig{
|
|
// TODO(mna): It currently only returns the Apple enabled and configured,
|
|
// regardless of the platform of the device. See
|
|
// https://github.com/fleetdm/fleet/pull/19304#discussion_r1618792410.
|
|
EnabledAndConfigured: ac.MDM.EnabledAndConfigured,
|
|
RequireAllSoftware: requireAllSoftware,
|
|
},
|
|
Features: fleet.DeviceFeatures{
|
|
EnableSoftwareInventory: softwareInventoryEnabled,
|
|
},
|
|
}
|
|
|
|
return getDeviceHostResponse{
|
|
Host: resp,
|
|
OrgLogoURL: ac.OrgInfo.OrgLogoURL,
|
|
OrgLogoURLLightBackground: ac.OrgInfo.OrgLogoURLLightBackground,
|
|
OrgContactURL: ac.OrgInfo.ContactURL,
|
|
License: *license,
|
|
GlobalConfig: deviceGlobalConfig,
|
|
SelfService: hasSelfService,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) GetHostDEPAssignment(ctx context.Context, host *fleet.Host) (*fleet.HostDEPAssignment, error) {
|
|
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) ||
|
|
svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) ||
|
|
svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceURL)
|
|
if !alreadyAuthd {
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return svc.ds.GetHostDEPAssignment(ctx, host.ID)
|
|
}
|
|
|
|
// AuthenticateDevice returns the host identified by the device authentication
|
|
// token, along with a boolean indicating if debug logging is enabled for that
|
|
// host.
|
|
func (svc *Service) AuthenticateDevice(ctx context.Context, authToken string) (*fleet.Host, bool, error) {
|
|
const deviceAuthTokenTTL = time.Hour
|
|
// skipauth: Authorization is currently for user endpoints only.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
if authToken == "" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing device authentication token"))
|
|
}
|
|
|
|
host, err := svc.ds.LoadHostByDeviceAuthToken(ctx, authToken, deviceAuthTokenTTL)
|
|
switch {
|
|
case err == nil:
|
|
// OK
|
|
case fleet.IsNotFound(err):
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: invalid device authentication token"))
|
|
default:
|
|
return nil, false, ctxerr.Wrap(ctx, err, "authenticate device")
|
|
}
|
|
|
|
// iOS/iPadOS must use certificate authentication.
|
|
if host.Platform == "ios" || host.Platform == "ipados" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: iOS and iPadOS devices must use certificate authentication"))
|
|
}
|
|
|
|
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
|
}
|
|
|
|
// AuthenticateDeviceByCertificate returns the host identified by the certificate
|
|
// serial number and host UUID. This is used for iOS/iPadOS devices accessing the
|
|
// My Device page via client certificate authentication. The certificate must match
|
|
// the host's identity certificate, and the host must be iOS or iPadOS.
|
|
func (svc *Service) AuthenticateDeviceByCertificate(ctx context.Context, certSerial uint64, hostUUID string) (*fleet.Host, bool, error) {
|
|
// skipauth: Authorization is currently for user endpoints only.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
if certSerial == 0 {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing certificate serial"))
|
|
}
|
|
|
|
if hostUUID == "" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing host UUID"))
|
|
}
|
|
|
|
// Look up the MDM SCEP certificate by serial number to get the device UUID
|
|
certDeviceUUID, err := svc.ds.GetMDMSCEPCertBySerial(ctx, certSerial)
|
|
switch {
|
|
case err == nil:
|
|
// OK
|
|
case fleet.IsNotFound(err):
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: invalid or missing certificate"))
|
|
default:
|
|
return nil, false, ctxerr.Wrap(ctx, err, "lookup certificate by serial")
|
|
}
|
|
|
|
// Verify certificate's device UUID matches the requested host UUID
|
|
if certDeviceUUID != hostUUID {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: certificate does not match host"))
|
|
}
|
|
|
|
// Look up the host by UUID
|
|
host, err := svc.ds.HostByIdentifier(ctx, hostUUID)
|
|
switch {
|
|
case err == nil:
|
|
// OK
|
|
case fleet.IsNotFound(err):
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: host not found"))
|
|
default:
|
|
return nil, false, ctxerr.Wrap(ctx, err, "lookup host by UUID")
|
|
}
|
|
|
|
// Verify host platform is iOS or iPadOS
|
|
if host.Platform != "ios" && host.Platform != "ipados" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: certificate authentication only supported for iOS and iPadOS devices"))
|
|
}
|
|
|
|
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
|
}
|
|
|
|
// AuthenticateIDeviceByURL returns the host identified by the URL UUID.
|
|
// This is used for iOS/iPadOS devices (iDevices) accessing endpoints via a unique URL parameter.
|
|
// Returns an error if the UUID doesn't exist or if the host is not iOS/iPadOS.
|
|
func (svc *Service) AuthenticateIDeviceByURL(ctx context.Context, urlUUID string) (*fleet.Host, bool, error) {
|
|
// skipauth: Authorization is currently for user endpoints only.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
if urlUUID == "" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing host UUID"))
|
|
}
|
|
|
|
// Look up the host by UUID
|
|
host, err := svc.ds.HostByIdentifier(ctx, urlUUID)
|
|
switch {
|
|
case err == nil:
|
|
// OK
|
|
case fleet.IsNotFound(err):
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: host not found"))
|
|
default:
|
|
return nil, false, ctxerr.Wrap(ctx, err, "lookup host by UUID")
|
|
}
|
|
|
|
// Verify host platform is iOS or iPadOS
|
|
if host.Platform != "ios" && host.Platform != "ipados" {
|
|
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: URL authentication only supported for iOS and iPadOS devices"))
|
|
}
|
|
|
|
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Refetch Current Device's Host
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type refetchDeviceHostRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *refetchDeviceHostRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
func refetchDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return refetchHostResponse{Err: err}, nil
|
|
}
|
|
|
|
err := svc.RefetchHost(ctx, host.ID)
|
|
if err != nil {
|
|
return refetchHostResponse{Err: err}, nil
|
|
}
|
|
return refetchHostResponse{}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Current Device's Host Device Mappings
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listDeviceHostDeviceMappingRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *listDeviceHostDeviceMappingRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
func listDeviceHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return listHostDeviceMappingResponse{Err: err}, nil
|
|
}
|
|
|
|
dms, err := svc.ListHostDeviceMapping(ctx, host.ID)
|
|
if err != nil {
|
|
return listHostDeviceMappingResponse{Err: err}, nil
|
|
}
|
|
return listHostDeviceMappingResponse{HostID: host.ID, DeviceMapping: dms}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Current Device's Macadmins
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getDeviceMacadminsDataRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *getDeviceMacadminsDataRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
func getDeviceMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return getMacadminsDataResponse{Err: err}, nil
|
|
}
|
|
|
|
data, err := svc.MacadminsData(ctx, host.ID)
|
|
if err != nil {
|
|
return getMacadminsDataResponse{Err: err}, nil
|
|
}
|
|
return getMacadminsDataResponse{Macadmins: data}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Current Device's Policies
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listDevicePoliciesRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *listDevicePoliciesRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type listDevicePoliciesResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
Policies []*fleet.HostPolicy `json:"policies"`
|
|
}
|
|
|
|
func (r listDevicePoliciesResponse) Error() error { return r.Err }
|
|
|
|
func listDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return listDevicePoliciesResponse{Err: err}, nil
|
|
}
|
|
|
|
data, err := svc.ListDevicePolicies(ctx, host)
|
|
if err != nil {
|
|
return listDevicePoliciesResponse{Err: err}, nil
|
|
}
|
|
|
|
return listDevicePoliciesResponse{Policies: data}, nil
|
|
}
|
|
|
|
func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Resend configuration profile
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type resendDeviceConfigurationProfileRequest struct {
|
|
Token string `url:"token"`
|
|
ProfileUUID string `url:"profile_uuid"`
|
|
}
|
|
|
|
func (r *resendDeviceConfigurationProfileRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type resendDeviceConfigurationProfileResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r resendDeviceConfigurationProfileResponse) Error() error { return r.Err }
|
|
|
|
func (r resendDeviceConfigurationProfileResponse) Status() int { return http.StatusAccepted }
|
|
|
|
func resendDeviceConfigurationProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return resendDeviceConfigurationProfileResponse{Err: err}, nil
|
|
}
|
|
|
|
req := request.(*resendDeviceConfigurationProfileRequest)
|
|
err := svc.ResendDeviceHostMDMProfile(ctx, host, req.ProfileUUID)
|
|
if err != nil {
|
|
return resendDeviceConfigurationProfileResponse{
|
|
Err: err,
|
|
}, nil
|
|
}
|
|
|
|
return resendDeviceConfigurationProfileResponse{}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get software MDM command results
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getDeviceMDMCommandResultsRequest struct {
|
|
Token string `url:"token"`
|
|
CommandUUID string `url:"command_uuid"`
|
|
}
|
|
|
|
func (r *getDeviceMDMCommandResultsRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
func getDeviceMDMCommandResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
_, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return getMDMCommandResultsResponse{Err: err}, nil
|
|
}
|
|
|
|
req := request.(*getDeviceMDMCommandResultsRequest)
|
|
results, err := svc.GetMDMCommandResults(ctx, req.CommandUUID, "")
|
|
if err != nil {
|
|
return getMDMCommandResultsResponse{
|
|
Err: err,
|
|
}, nil
|
|
}
|
|
|
|
return getMDMCommandResultsResponse{
|
|
Results: results,
|
|
}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Transparency URL Redirect
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type transparencyURLRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *transparencyURLRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type transparencyURLResponse struct {
|
|
RedirectURL string `json:"-"` // used to control the redirect, see HijackRender method
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r transparencyURLResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
w.Header().Set("Location", r.RedirectURL)
|
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
func (r transparencyURLResponse) Error() error { return r.Err }
|
|
|
|
func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
transparencyURL, err := svc.GetTransparencyURL(ctx)
|
|
|
|
return transparencyURLResponse{RedirectURL: transparencyURL, Err: err}, nil
|
|
}
|
|
|
|
func (svc *Service) GetTransparencyURL(ctx context.Context) (string, error) {
|
|
config, err := svc.AppConfigObfuscated(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
license, err := svc.License(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
transparencyURL := fleet.DefaultTransparencyURL
|
|
// See #27309; overridden if on Fleet Premium and custom transparency URL is set
|
|
if svc.config.Partnerships.EnableSecureframe {
|
|
transparencyURL = fleet.SecureframeTransparencyURL
|
|
}
|
|
|
|
// Fleet Premium license is required for custom transparency URL
|
|
if license.IsPremium() && config.FleetDesktop.TransparencyURL != "" {
|
|
transparencyURL = config.FleetDesktop.TransparencyURL
|
|
}
|
|
|
|
return transparencyURL, nil
|
|
}
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////
|
|
// Software title icons
|
|
// ///////////////////////////////////////////////////////////////////////////////
|
|
type getDeviceSoftwareIconRequest struct {
|
|
Token string `url:"token"`
|
|
SoftwareTitleID uint `url:"software_title_id"`
|
|
}
|
|
|
|
func (r *getDeviceSoftwareIconRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type getDeviceSoftwareIconResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
ImageData []byte `json:"-"`
|
|
ContentType string `json:"-"`
|
|
Filename string `json:"-"`
|
|
Size int64 `json:"-"`
|
|
}
|
|
|
|
func (r getDeviceSoftwareIconResponse) Error() error { return r.Err }
|
|
|
|
type getDeviceSoftwareIconRedirectResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
RedirectURL string `json:"-"`
|
|
}
|
|
|
|
func (r getDeviceSoftwareIconRedirectResponse) Error() error { return r.Err }
|
|
|
|
func (r getDeviceSoftwareIconRedirectResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
if r.Err != nil {
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Location", r.RedirectURL)
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
func (r getDeviceSoftwareIconResponse) HijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", r.ContentType)
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, r.Filename))
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", r.Size))
|
|
|
|
_, _ = w.Write(r.ImageData)
|
|
}
|
|
|
|
func getDeviceSoftwareIconEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return getDeviceSoftwareIconResponse{Err: err}, nil
|
|
}
|
|
|
|
req := request.(*getDeviceSoftwareIconRequest)
|
|
var teamID uint
|
|
if host.TeamID != nil {
|
|
teamID = *host.TeamID
|
|
}
|
|
iconData, size, filename, err := svc.GetDeviceSoftwareIconsTitleIcon(ctx, teamID, req.SoftwareTitleID)
|
|
if err != nil {
|
|
var vppErr *fleet.VPPIconAvailable
|
|
if errors.As(err, &vppErr) {
|
|
// 302 redirect to vpp app IconURL
|
|
return getDeviceSoftwareIconRedirectResponse{RedirectURL: vppErr.IconURL}, nil
|
|
}
|
|
return getDeviceSoftwareIconResponse{Err: err}, nil
|
|
}
|
|
|
|
return getDeviceSoftwareIconResponse{
|
|
ImageData: iconData,
|
|
ContentType: "image/png", // only type of icon we currently allow
|
|
Filename: filename,
|
|
Size: size,
|
|
}, nil
|
|
}
|
|
|
|
func (svc *Service) GetDeviceSoftwareIconsTitleIcon(ctx context.Context, teamID uint, titleID uint) ([]byte, int64, string, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, 0, "", fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Receive errors from the client
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type fleetdErrorRequest struct {
|
|
Token string `url:"token"`
|
|
fleet.FleetdError
|
|
}
|
|
|
|
func (f *fleetdErrorRequest) deviceAuthToken() string {
|
|
return f.Token
|
|
}
|
|
|
|
// Since we're directly storing what we get in Redis, limit the request size to
|
|
// 5MB, this combined with the rate limit of this endpoint should be enough to
|
|
// prevent a malicious actor.
|
|
const maxFleetdErrorReportSize int64 = 5 * 1024 * 1024
|
|
|
|
func (f *fleetdErrorRequest) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error {
|
|
limitedReader := io.LimitReader(r, maxFleetdErrorReportSize+1)
|
|
decoder := json.NewDecoder(limitedReader)
|
|
|
|
for {
|
|
if err := decoder.Decode(&f.FleetdError); err == io.EOF {
|
|
break
|
|
} else if err == io.ErrUnexpectedEOF {
|
|
return &fleet.BadRequestError{Message: "payload exceeds maximum accepted size"}
|
|
} else if err != nil {
|
|
return &fleet.BadRequestError{Message: "invalid payload"}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type fleetdErrorResponse struct{}
|
|
|
|
func (r fleetdErrorResponse) Error() error { return nil }
|
|
|
|
func fleetdError(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*fleetdErrorRequest)
|
|
err := svc.LogFleetdError(ctx, req.FleetdError)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fleetdErrorResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) LogFleetdError(ctx context.Context, fleetdError fleet.FleetdError) error {
|
|
// iOS/iPadOS devices don't have fleetd, so URL auth is not allowed here.
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) &&
|
|
!svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) {
|
|
return ctxerr.Wrap(ctx, fleet.NewPermissionError("forbidden: only device-authenticated hosts can access this endpoint"))
|
|
}
|
|
|
|
err := ctxerr.WrapWithData(ctx, fleetdError, "receive fleetd error", fleetdError.ToMap())
|
|
level.Warn(svc.logger).Log(
|
|
"msg",
|
|
"fleetd error",
|
|
"error",
|
|
err,
|
|
)
|
|
// Send to Redis/telemetry (if enabled)
|
|
ctxerr.Handle(ctx, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Current Device's MDM Apple Enrollment Profile
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getDeviceMDMManualEnrollProfileRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *getDeviceMDMManualEnrollProfileRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type getDeviceMDMManualEnrollProfileResponse struct {
|
|
// EnrollURL field is used in HijackRender for the response.
|
|
EnrollURL string `json:"enroll_url,omitempty"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getDeviceMDMManualEnrollProfileResponse) Error() error { return r.Err }
|
|
|
|
func getDeviceMDMManualEnrollProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
// this call ensures that the authentication was done, no need to actually
|
|
// use the host
|
|
if _, ok := hostctx.FromContext(ctx); !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return getDeviceMDMManualEnrollProfileResponse{Err: err}, nil
|
|
}
|
|
|
|
enrollURL, err := svc.GetDeviceMDMAppleEnrollmentProfile(ctx)
|
|
if err != nil {
|
|
return getDeviceMDMManualEnrollProfileResponse{Err: err}, nil
|
|
}
|
|
return getDeviceMDMManualEnrollProfileResponse{EnrollURL: enrollURL.String()}, nil
|
|
}
|
|
|
|
func (svc *Service) GetDeviceMDMAppleEnrollmentProfile(ctx context.Context) (*url.URL, error) {
|
|
// must be device-authenticated, no additional authorization is required
|
|
// iOS/iPadOS devices are enrolled via MDM profile or ABM, so URL auth is not allowed here.
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) &&
|
|
!svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) {
|
|
return nil, ctxerr.Wrap(ctx, fleet.NewPermissionError("forbidden: only device-authenticated hosts can access this endpoint"))
|
|
}
|
|
|
|
cfg, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "fetching app config")
|
|
}
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
return nil, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
}
|
|
|
|
tmSecrets, err := svc.ds.GetEnrollSecrets(ctx, host.TeamID)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting host team enroll secrets")
|
|
}
|
|
if len(tmSecrets) == 0 && host.TeamID != nil {
|
|
tmSecrets, err = svc.ds.GetEnrollSecrets(ctx, nil)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting no team enroll secrets")
|
|
}
|
|
}
|
|
if len(tmSecrets) == 0 {
|
|
return nil, &fleet.BadRequestError{Message: "unable to find an enroll secret to generate enrollment profile"}
|
|
}
|
|
var enrollSecret fleet.EnrollSecret
|
|
for _, s := range tmSecrets {
|
|
if s.CreatedAt.After(enrollSecret.CreatedAt) {
|
|
enrollSecret = *s
|
|
}
|
|
}
|
|
url, err := url.Parse(cfg.MDMUrl())
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "parsing MDM URL from config")
|
|
}
|
|
url.Path = path.Join(url.Path, "enroll")
|
|
q := url.Query()
|
|
q.Set("enroll_secret", enrollSecret.Secret)
|
|
url.RawQuery = q.Encode()
|
|
|
|
return url, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Signal start of mdm migration on a device
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type deviceMigrateMDMRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *deviceMigrateMDMRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type deviceMigrateMDMResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r deviceMigrateMDMResponse) Error() error { return r.Err }
|
|
|
|
func (r deviceMigrateMDMResponse) Status() int { return http.StatusNoContent }
|
|
|
|
func migrateMDMDeviceEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return deviceMigrateMDMResponse{Err: err}, nil
|
|
}
|
|
|
|
if err := svc.TriggerMigrateMDMDevice(ctx, host); err != nil {
|
|
return deviceMigrateMDMResponse{Err: err}, nil
|
|
}
|
|
return deviceMigrateMDMResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Host) error {
|
|
return fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Trigger linux key escrow
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type triggerLinuxDiskEncryptionEscrowRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *triggerLinuxDiskEncryptionEscrowRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type triggerLinuxDiskEncryptionEscrowResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r triggerLinuxDiskEncryptionEscrowResponse) Error() error { return r.Err }
|
|
|
|
func (r triggerLinuxDiskEncryptionEscrowResponse) Status() int { return http.StatusNoContent }
|
|
|
|
func triggerLinuxDiskEncryptionEscrowEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil
|
|
}
|
|
|
|
if err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host); err != nil {
|
|
return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil
|
|
}
|
|
return triggerLinuxDiskEncryptionEscrowResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error {
|
|
return fleet.ErrMissingLicense
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Current Device's Software
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getDeviceSoftwareRequest struct {
|
|
Token string `url:"token"`
|
|
fleet.HostSoftwareTitleListOptions
|
|
}
|
|
|
|
func (r *getDeviceSoftwareRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type getDeviceSoftwareResponse struct {
|
|
Software []*fleet.HostSoftwareWithInstaller `json:"software"`
|
|
Count int `json:"count"`
|
|
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getDeviceSoftwareResponse) Error() error { return r.Err }
|
|
|
|
func getDeviceSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return getDeviceSoftwareResponse{Err: err}, nil
|
|
}
|
|
|
|
req := request.(*getDeviceSoftwareRequest)
|
|
res, meta, err := svc.ListHostSoftware(ctx, host.ID, req.HostSoftwareTitleListOptions)
|
|
for _, s := range res {
|
|
// mutate HostSoftwareWithInstaller records for my device page
|
|
s.ForMyDevicePage(req.Token)
|
|
}
|
|
|
|
if err != nil {
|
|
return getDeviceSoftwareResponse{Err: err}, nil
|
|
}
|
|
if res == nil {
|
|
res = []*fleet.HostSoftwareWithInstaller{}
|
|
}
|
|
return getDeviceSoftwareResponse{Software: res, Meta: meta, Count: int(meta.TotalResults)}, nil //nolint:gosec // dismiss G115
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// List Current Device's Certificates
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listDeviceCertificatesRequest struct {
|
|
Token string `url:"token"`
|
|
fleet.ListOptions
|
|
}
|
|
|
|
func (r *listDeviceCertificatesRequest) ValidateRequest() error {
|
|
if r.ListOptions.OrderKey != "" && !listHostCertificatesSortCols[r.ListOptions.OrderKey] {
|
|
return badRequest("invalid order key")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *listDeviceCertificatesRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type listDeviceCertificatesResponse struct {
|
|
Certificates []*fleet.HostCertificatePayload `json:"certificates"`
|
|
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
|
|
Count uint `json:"count"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listDeviceCertificatesResponse) Error() error { return r.Err }
|
|
|
|
func listDeviceCertificatesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
host, ok := hostctx.FromContext(ctx)
|
|
if !ok {
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
return listDevicePoliciesResponse{Err: err}, nil
|
|
}
|
|
|
|
req := request.(*listDeviceCertificatesRequest)
|
|
res, meta, err := svc.ListHostCertificates(ctx, host.ID, req.ListOptions)
|
|
if err != nil {
|
|
return listDeviceCertificatesResponse{Err: err}, nil
|
|
}
|
|
if res == nil {
|
|
res = []*fleet.HostCertificatePayload{}
|
|
}
|
|
return listDeviceCertificatesResponse{Certificates: res, Meta: meta, Count: meta.TotalResults}, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get "Setup experience" status.
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getDeviceSetupExperienceStatusRequest struct {
|
|
Token string `url:"token"`
|
|
}
|
|
|
|
func (r *getDeviceSetupExperienceStatusRequest) deviceAuthToken() string {
|
|
return r.Token
|
|
}
|
|
|
|
type getDeviceSetupExperienceStatusResponse struct {
|
|
Results *fleet.DeviceSetupExperienceStatusPayload `json:"setup_experience_results,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getDeviceSetupExperienceStatusResponse) Error() error { return r.Err }
|
|
|
|
func getDeviceSetupExperienceStatusEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
if _, ok := request.(*getDeviceSetupExperienceStatusRequest); !ok {
|
|
return nil, fmt.Errorf("internal error: invalid request type: %T", request)
|
|
}
|
|
results, err := svc.GetDeviceSetupExperienceStatus(ctx)
|
|
if err != nil {
|
|
return &getDeviceSetupExperienceStatusResponse{Err: err}, nil
|
|
}
|
|
return &getDeviceSetupExperienceStatusResponse{Results: results}, nil
|
|
}
|
|
|
|
func (svc *Service) GetDeviceSetupExperienceStatus(ctx context.Context) (*fleet.DeviceSetupExperienceStatusPayload, error) {
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
// only license error.
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
}
|