2022-03-09 21:13:56 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-08-24 16:04:27 +00:00
|
|
|
"encoding/json"
|
|
|
|
"io"
|
2022-06-13 19:07:08 +00:00
|
|
|
"net/http"
|
2023-08-24 16:04:27 +00:00
|
|
|
"net/url"
|
2023-01-16 15:22:12 +00:00
|
|
|
"strconv"
|
2022-10-10 20:15:35 +00:00
|
|
|
"time"
|
2022-03-09 21:13:56 +00:00
|
|
|
|
2023-01-16 15:22:12 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/authz"
|
2023-05-23 18:01:04 +00:00
|
|
|
authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
2022-03-09 21:13:56 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
|
|
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
2023-01-16 15:22:12 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
2022-03-09 21:13:56 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2023-03-13 13:33:32 +00:00
|
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
2023-05-23 18:01:04 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
2022-03-09 21:13:56 +00:00
|
|
|
)
|
|
|
|
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Ping device endpoint
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type devicePingRequest struct{}
|
|
|
|
|
|
|
|
type devicePingResponse struct{}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func (r devicePingResponse) error() error { return nil }
|
|
|
|
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
func (r devicePingResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
|
2023-08-24 16:04:27 +00:00
|
|
|
writeCapabilitiesHeader(w, fleet.GetServerDeviceCapabilities())
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2022-12-27 14:26:59 +00:00
|
|
|
func devicePingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-09-15 16:12:50 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
2022-09-12 19:37:38 +00:00
|
|
|
// Fleet Desktop endpoints
|
2022-09-15 16:12:50 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2022-10-12 20:13:43 +00:00
|
|
|
type fleetDesktopResponse struct {
|
2023-05-15 20:00:52 +00:00
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
fleet.DesktopSummary
|
2022-09-12 19:37:38 +00:00
|
|
|
}
|
|
|
|
|
2022-10-26 19:17:11 +00:00
|
|
|
func (r fleetDesktopResponse) error() error { return r.Err }
|
|
|
|
|
2022-09-12 19:37:38 +00:00
|
|
|
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.
|
2022-12-27 14:26:59 +00:00
|
|
|
func getFleetDesktopEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2023-05-15 20:00:52 +00:00
|
|
|
sum, err := svc.GetFleetDesktopSummary(ctx)
|
2022-09-12 19:37:38 +00:00
|
|
|
if err != nil {
|
2022-10-12 20:13:43 +00:00
|
|
|
return fleetDesktopResponse{Err: err}, nil
|
2022-09-12 19:37:38 +00:00
|
|
|
}
|
2023-05-15 20:00:52 +00:00
|
|
|
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)
|
2022-09-12 19:37:38 +00:00
|
|
|
|
2023-05-15 20:00:52 +00:00
|
|
|
return fleet.DesktopSummary{}, fleet.ErrMissingLicense
|
2022-09-12 19:37:38 +00:00
|
|
|
}
|
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Get Current Device's Host
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type getDeviceHostRequest struct {
|
|
|
|
Token string `url:"token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *getDeviceHostRequest) deviceAuthToken() string {
|
|
|
|
return r.Token
|
|
|
|
}
|
|
|
|
|
2022-03-16 14:15:25 +00:00
|
|
|
type getDeviceHostResponse struct {
|
2023-07-13 18:35:25 +00:00
|
|
|
Host *HostDetailResponse `json:"host"`
|
|
|
|
OrgLogoURL string `json:"org_logo_url"`
|
|
|
|
OrgLogoURLLightBackground string `json:"org_logo_url_light_background"`
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
License fleet.LicenseInfo `json:"license"`
|
|
|
|
GlobalConfig fleet.DeviceGlobalConfig `json:"global_config"`
|
2022-03-16 14:15:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r getDeviceHostResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-09 21:13:56 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
2022-03-16 14:15:25 +00:00
|
|
|
return getDeviceHostResponse{Err: err}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// must still load the full host details, as it returns more information
|
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
|
|
|
opts := fleet.HostDetailOptions{
|
|
|
|
IncludeCVEScores: false,
|
|
|
|
IncludePolicies: false,
|
|
|
|
}
|
|
|
|
hostDetails, err := svc.GetHost(ctx, host.ID, opts)
|
2022-03-09 21:13:56 +00:00
|
|
|
if err != nil {
|
2022-03-16 14:15:25 +00:00
|
|
|
return getDeviceHostResponse{Err: err}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := hostDetailResponseForHost(ctx, svc, hostDetails)
|
|
|
|
if err != nil {
|
2022-03-16 14:15:25 +00:00
|
|
|
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).
|
2023-03-28 18:23:15 +00:00
|
|
|
ac, err := svc.AppConfigObfuscated(ctx)
|
2022-03-16 14:15:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return getDeviceHostResponse{Err: err}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
2022-05-19 21:28:49 +00:00
|
|
|
license, err := svc.License(ctx)
|
|
|
|
if err != nil {
|
2022-05-19 23:29:33 +00:00
|
|
|
return getDeviceHostResponse{Err: err}, nil
|
2022-05-19 21:28:49 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 17:47:52 +00:00
|
|
|
deviceGlobalConfig := fleet.DeviceGlobalConfig{
|
|
|
|
MDM: fleet.DeviceGlobalMDMConfig{
|
|
|
|
EnabledAndConfigured: ac.MDM.EnabledAndConfigured,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-05-23 18:01:04 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2022-03-16 14:15:25 +00:00
|
|
|
return getDeviceHostResponse{
|
2023-02-01 17:47:52 +00:00
|
|
|
Host: resp,
|
|
|
|
OrgLogoURL: ac.OrgInfo.OrgLogoURL,
|
|
|
|
License: *license,
|
|
|
|
GlobalConfig: deviceGlobalConfig,
|
2022-03-16 14:15:25 +00:00
|
|
|
}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
2023-05-23 18:01:04 +00:00
|
|
|
func (svc *Service) GetHostDEPAssignment(ctx context.Context, host *fleet.Host) (*fleet.HostDEPAssignment, error) {
|
|
|
|
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken)
|
|
|
|
if !alreadyAuthd {
|
|
|
|
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return svc.ds.GetHostDEPAssignment(ctx, host.ID)
|
|
|
|
}
|
|
|
|
|
2022-03-09 21:13:56 +00:00
|
|
|
// 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) {
|
2022-10-10 20:15:35 +00:00
|
|
|
const deviceAuthTokenTTL = time.Hour
|
2022-03-09 21:13:56 +00:00
|
|
|
// 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"))
|
|
|
|
}
|
|
|
|
|
2022-10-10 20:15:35 +00:00
|
|
|
host, err := svc.ds.LoadHostByDeviceAuthToken(ctx, authToken, deviceAuthTokenTTL)
|
2022-03-09 21:13:56 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func refetchDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-09 21:13:56 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
2022-06-29 12:12:20 +00:00
|
|
|
return refetchHostResponse{Err: err}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func listDeviceHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-09 21:13:56 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
2022-06-29 12:12:20 +00:00
|
|
|
return listHostDeviceMappingResponse{Err: err}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getDeviceMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-03-09 21:13:56 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
2022-06-29 12:12:20 +00:00
|
|
|
return getMacadminsDataResponse{Err: err}, nil
|
2022-03-09 21:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
data, err := svc.MacadminsData(ctx, host.ID)
|
|
|
|
if err != nil {
|
|
|
|
return getMacadminsDataResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return getMacadminsDataResponse{Macadmins: data}, nil
|
|
|
|
}
|
add premium, device authed endpoint to retrieve policies (#5967)
This adds a new device authenticated endpoint, `/api/_version_/fleet/device/{token}/policies` to retrieve the device policies.
An example request / response looks like:
```bash
curl https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a/policies
```
```json
{
"policies": [
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Admin",
"author_email": "admin@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,windows,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-05-23T20:53:36Z",
"response": "fail"
}
]
}
```
Related to [#5685](https://github.com/fleetdm/fleet/issues/5685), in another changeset I will be adding "client" endpoints so we can consume this endpoint from Fleet Desktop
2022-05-31 17:54:43 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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 }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func listDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
add premium, device authed endpoint to retrieve policies (#5967)
This adds a new device authenticated endpoint, `/api/_version_/fleet/device/{token}/policies` to retrieve the device policies.
An example request / response looks like:
```bash
curl https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a/policies
```
```json
{
"policies": [
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Admin",
"author_email": "admin@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,windows,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-05-23T20:53:36Z",
"response": "fail"
}
]
}
```
Related to [#5685](https://github.com/fleetdm/fleet/issues/5685), in another changeset I will be adding "client" endpoints so we can consume this endpoint from Fleet Desktop
2022-05-31 17:54:43 +00:00
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
2022-06-29 12:12:20 +00:00
|
|
|
return listDevicePoliciesResponse{Err: err}, nil
|
add premium, device authed endpoint to retrieve policies (#5967)
This adds a new device authenticated endpoint, `/api/_version_/fleet/device/{token}/policies` to retrieve the device policies.
An example request / response looks like:
```bash
curl https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a/policies
```
```json
{
"policies": [
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Admin",
"author_email": "admin@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,windows,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-05-23T20:53:36Z",
"response": "fail"
}
]
}
```
Related to [#5685](https://github.com/fleetdm/fleet/issues/5685), in another changeset I will be adding "client" endpoints so we can consume this endpoint from Fleet Desktop
2022-05-31 17:54:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2022-06-09 13:17:55 +00:00
|
|
|
|
2022-06-13 19:07:08 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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 }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2023-03-28 18:23:15 +00:00
|
|
|
config, err := svc.AppConfigObfuscated(ctx)
|
2022-06-13 19:07:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return transparencyURLResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
license, err := svc.License(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return transparencyURLResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
transparencyURL := fleet.DefaultTransparencyURL
|
|
|
|
// Fleet Premium license is required for custom transparency url
|
2023-05-15 18:06:09 +00:00
|
|
|
if license.IsPremium() && config.FleetDesktop.TransparencyURL != "" {
|
2022-06-13 19:07:08 +00:00
|
|
|
transparencyURL = config.FleetDesktop.TransparencyURL
|
|
|
|
}
|
|
|
|
|
|
|
|
return transparencyURLResponse{RedirectURL: transparencyURL}, nil
|
|
|
|
}
|
2023-01-16 15:22:12 +00:00
|
|
|
|
2023-08-24 16:04:27 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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) 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 }
|
|
|
|
|
|
|
|
// for now, this endpoint must always return a 500 status code, this
|
|
|
|
// way errors are picked up and reported by any monitoring tool that
|
|
|
|
// looks for 5xx errors.
|
|
|
|
//
|
|
|
|
// since the handler is returning an error, this is redundant, but I'm adding
|
|
|
|
// it as a safeguard.
|
|
|
|
//
|
|
|
|
// See: https://github.com/fleetdm/fleet/issues/13238#issuecomment-1671769460
|
|
|
|
func (r fleetdErrorResponse) Status() int { return http.StatusInternalServerError }
|
|
|
|
|
|
|
|
func fleetdError(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
|
|
|
req := request.(*fleetdErrorRequest)
|
|
|
|
err := svc.ReceiveFleetdError(ctx, req.FleetdError)
|
|
|
|
// return the error as the second parameter to get better logs in the server.
|
|
|
|
return fleetdErrorResponse{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) ReceiveFleetdError(ctx context.Context, fleetdError fleet.FleetdError) error {
|
|
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
|
|
|
|
return ctxerr.Wrap(ctx, fleet.NewPermissionError("forbidden: only device-authenticated hosts can access this endpoint"))
|
|
|
|
}
|
|
|
|
|
|
|
|
return ctxerr.WrapWithData(ctx, fleetdError, "receive fleetd error", fleetdError.ToMap())
|
|
|
|
}
|
|
|
|
|
2023-01-16 15:22:12 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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 {
|
|
|
|
// Profile field is used in hijackRender for the response.
|
|
|
|
Profile []byte
|
|
|
|
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getDeviceMDMManualEnrollProfileResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
|
|
// make the browser download the content to a file
|
|
|
|
w.Header().Add("Content-Disposition", `attachment; filename="fleet-mdm-enrollment-profile.mobileconfig"`)
|
|
|
|
// explicitly set the content length before the write, so the caller can
|
|
|
|
// detect short writes (if it fails to send the full content properly)
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.Profile)), 10))
|
|
|
|
// this content type will make macos open the profile with the proper application
|
|
|
|
w.Header().Set("Content-Type", "application/x-apple-aspen-config; charset=urf-8")
|
|
|
|
// prevent detection of content, obey the provided content-type
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
|
|
|
|
if n, err := w.Write(r.Profile); err != nil {
|
|
|
|
logging.WithExtras(ctx, "err", err, "written", n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getDeviceMDMManualEnrollProfileResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func getDeviceMDMManualEnrollProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (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
|
|
|
|
}
|
|
|
|
|
|
|
|
profile, err := svc.GetDeviceMDMAppleEnrollmentProfile(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return getDeviceMDMManualEnrollProfileResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return getDeviceMDMManualEnrollProfileResponse{Profile: profile}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) GetDeviceMDMAppleEnrollmentProfile(ctx context.Context) ([]byte, error) {
|
|
|
|
// must be device-authenticated, no additional authorization is required
|
|
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
|
|
|
|
return nil, ctxerr.Wrap(ctx, fleet.NewPermissionError("forbidden: only device-authenticated hosts can access this endpoint"))
|
|
|
|
}
|
|
|
|
|
|
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
|
|
}
|
|
|
|
|
2023-03-13 13:33:32 +00:00
|
|
|
mobileConfig, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
2023-01-16 15:22:12 +00:00
|
|
|
appConfig.OrgInfo.OrgName,
|
|
|
|
appConfig.ServerSettings.ServerURL,
|
2023-03-23 10:30:28 +00:00
|
|
|
svc.config.MDM.AppleSCEPChallenge,
|
2023-01-16 15:22:12 +00:00
|
|
|
svc.mdmPushCertTopic,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
|
|
}
|
|
|
|
return mobileConfig, nil
|
|
|
|
}
|
2023-03-20 19:14:07 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Request a disk encryption reset
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type rotateEncryptionKeyRequest struct {
|
|
|
|
Token string `url:"token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *rotateEncryptionKeyRequest) deviceAuthToken() string {
|
|
|
|
return r.Token
|
|
|
|
}
|
|
|
|
|
|
|
|
type rotateEncryptionKeyResponse struct {
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r rotateEncryptionKeyResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func rotateEncryptionKeyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
|
|
|
host, ok := hostctx.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
|
|
|
|
return rotateEncryptionKeyResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := svc.RequestEncryptionKeyRotation(ctx, host.ID); err != nil {
|
|
|
|
return rotateEncryptionKeyResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
return rotateEncryptionKeyResponse{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error {
|
|
|
|
return fleet.ErrMissingLicense
|
|
|
|
}
|
2023-05-17 14:16:26 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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) (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
|
|
|
|
}
|