2022-10-05 22:53:54 +00:00
package service
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
2023-02-22 17:49:06 +00:00
"errors"
2022-10-05 22:53:54 +00:00
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
2023-02-17 15:28:28 +00:00
"strings"
2023-03-27 18:43:01 +00:00
"sync"
2023-01-23 23:05:24 +00:00
"time"
2022-10-05 22:53:54 +00:00
2023-04-03 18:25:49 +00:00
"github.com/VividCortex/mysqlerr"
2022-10-05 22:53:54 +00:00
"github.com/docker/go-units"
2023-01-23 23:05:24 +00:00
"github.com/fleetdm/fleet/v4/server/authz"
2022-10-05 22:53:54 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2023-04-17 15:08:55 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/license"
2022-10-05 22:53:54 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/logging"
2023-04-03 18:25:49 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
2022-10-05 22:53:54 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
2023-04-07 20:31:02 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest"
2023-03-17 21:52:30 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
2023-04-27 12:43:20 +00:00
"github.com/fleetdm/fleet/v4/server/sso"
2022-10-05 22:53:54 +00:00
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
2023-04-03 18:25:49 +00:00
"github.com/go-sql-driver/mysql"
2022-10-05 22:53:54 +00:00
"github.com/google/uuid"
"github.com/groob/plist"
"github.com/micromdm/nanodep/godep"
"github.com/micromdm/nanomdm/mdm"
)
type createMDMAppleEnrollmentProfileRequest struct {
Type fleet . MDMAppleEnrollmentType ` json:"type" `
DEPProfile * json . RawMessage ` json:"dep_profile" `
}
type createMDMAppleEnrollmentProfileResponse struct {
EnrollmentProfile * fleet . MDMAppleEnrollmentProfile ` json:"enrollment_profile" `
Err error ` json:"error,omitempty" `
}
func ( r createMDMAppleEnrollmentProfileResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func createMDMAppleEnrollmentProfilesEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * createMDMAppleEnrollmentProfileRequest )
enrollmentProfile , err := svc . NewMDMAppleEnrollmentProfile ( ctx , fleet . MDMAppleEnrollmentProfilePayload {
Type : req . Type ,
DEPProfile : req . DEPProfile ,
} )
if err != nil {
return createMDMAppleEnrollmentProfileResponse {
Err : err ,
} , nil
}
return createMDMAppleEnrollmentProfileResponse {
EnrollmentProfile : enrollmentProfile ,
} , nil
}
func ( svc * Service ) NewMDMAppleEnrollmentProfile ( ctx context . Context , enrollmentPayload fleet . MDMAppleEnrollmentProfilePayload ) ( * fleet . MDMAppleEnrollmentProfile , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleEnrollmentProfile { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
// generate a token for the profile
enrollmentPayload . Token = uuid . New ( ) . String ( )
profile , err := svc . ds . NewMDMAppleEnrollmentProfile ( ctx , enrollmentPayload )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
if profile . DEPProfile != nil {
2023-04-27 12:43:20 +00:00
lic , err := svc . License ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get license" )
}
if ! lic . IsPremium ( ) {
return nil , fleet . ErrMissingLicense
}
if err := svc . EnterpriseOverrides . MDMAppleSyncDEPPRofile ( ctx ) ; err != nil {
2022-10-05 22:53:54 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
}
2023-04-27 12:43:20 +00:00
enrollmentURL , err := apple_mdm . EnrollURL ( profile . Token , appConfig )
2022-12-23 17:55:17 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
profile . EnrollmentURL = enrollmentURL
2022-10-05 22:53:54 +00:00
return profile , nil
}
type listMDMAppleEnrollmentProfilesRequest struct { }
type listMDMAppleEnrollmentProfilesResponse struct {
EnrollmentProfiles [ ] * fleet . MDMAppleEnrollmentProfile ` json:"enrollment_profiles" `
Err error ` json:"error,omitempty" `
}
func ( r listMDMAppleEnrollmentProfilesResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func listMDMAppleEnrollmentsEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
enrollmentProfiles , err := svc . ListMDMAppleEnrollmentProfiles ( ctx )
if err != nil {
return listMDMAppleEnrollmentProfilesResponse {
Err : err ,
} , nil
}
return listMDMAppleEnrollmentProfilesResponse {
EnrollmentProfiles : enrollmentProfiles ,
} , nil
}
func ( svc * Service ) ListMDMAppleEnrollmentProfiles ( ctx context . Context ) ( [ ] * fleet . MDMAppleEnrollmentProfile , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleEnrollmentProfile { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
enrollments , err := svc . ds . ListMDMAppleEnrollmentProfiles ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
for i := range enrollments {
2023-04-27 12:43:20 +00:00
enrollURL , err := apple_mdm . EnrollURL ( enrollments [ i ] . Token , appConfig )
2022-12-23 17:55:17 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
enrollments [ i ] . EnrollmentURL = enrollURL
2022-10-05 22:53:54 +00:00
}
return enrollments , nil
}
type getMDMAppleCommandResultsRequest struct {
CommandUUID string ` query:"command_uuid,optional" `
}
type getMDMAppleCommandResultsResponse struct {
2023-04-05 14:50:36 +00:00
Results [ ] * fleet . MDMAppleCommandResult ` json:"results,omitempty" `
Err error ` json:"error,omitempty" `
2022-10-05 22:53:54 +00:00
}
func ( r getMDMAppleCommandResultsResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getMDMAppleCommandResultsEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * getMDMAppleCommandResultsRequest )
results , err := svc . GetMDMAppleCommandResults ( ctx , req . CommandUUID )
if err != nil {
return getMDMAppleCommandResultsResponse {
Err : err ,
} , nil
}
return getMDMAppleCommandResultsResponse {
Results : results ,
} , nil
}
2023-04-05 14:50:36 +00:00
func ( svc * Service ) GetMDMAppleCommandResults ( ctx context . Context , commandUUID string ) ( [ ] * fleet . MDMAppleCommandResult , error ) {
// first, authorize that the user has the right to list hosts
if err := svc . authz . Authorize ( ctx , & fleet . Host { } , fleet . ActionList ) ; err != nil {
2022-10-05 22:53:54 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
2023-04-05 14:50:36 +00:00
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return nil , fleet . ErrNoContext
}
// check that command exists first, to return 404 on invalid commands
// (the command may exist but have no results yet).
if _ , err := svc . ds . GetMDMAppleCommandRequestType ( ctx , commandUUID ) ; err != nil {
return nil , err
}
// next, we need to read the command results before we know what hosts (and
// therefore what teams) we're dealing with.
2022-10-05 22:53:54 +00:00
results , err := svc . ds . GetMDMAppleCommandResults ( ctx , commandUUID )
if err != nil {
return nil , err
}
2023-04-05 14:50:36 +00:00
// now we can load the hosts (lite) corresponding to those command results,
// and do the final authorization check with the proper team(s). Include observers,
// as they are able to view command results for their teams' hosts.
filter := fleet . TeamFilter { User : vc . User , IncludeObserver : true }
hostUUIDs := make ( [ ] string , len ( results ) )
for i , res := range results {
hostUUIDs [ i ] = res . DeviceID
}
hosts , err := svc . ds . ListHostsLiteByUUIDs ( ctx , filter , hostUUIDs )
if err != nil {
return nil , err
}
if len ( hosts ) == 0 {
// do not return 404 here, as it's possible for a command to not have
// results yet
return nil , nil
}
2023-04-17 15:45:16 +00:00
// collect the team IDs and verify that the user has access to view commands
2023-04-05 14:50:36 +00:00
// on all affected teams. Index the hosts by uuid for easly lookup as
// afterwards we'll want to store the hostname on the returned results.
hostsByUUID := make ( map [ string ] * fleet . Host , len ( hosts ) )
teamIDs := make ( map [ uint ] bool )
for _ , h := range hosts {
var id uint
if h . TeamID != nil {
id = * h . TeamID
}
teamIDs [ id ] = true
hostsByUUID [ h . UUID ] = h
}
2023-04-17 15:45:16 +00:00
var commandAuthz fleet . MDMAppleCommandAuthz
2023-04-05 14:50:36 +00:00
for tmID := range teamIDs {
2023-04-17 15:45:16 +00:00
commandAuthz . TeamID = & tmID
2023-04-05 14:50:36 +00:00
if tmID == 0 {
2023-04-17 15:45:16 +00:00
commandAuthz . TeamID = nil
2023-04-05 14:50:36 +00:00
}
2023-04-17 15:45:16 +00:00
if err := svc . authz . Authorize ( ctx , commandAuthz , fleet . ActionRead ) ; err != nil {
2023-04-05 14:50:36 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
}
// add the hostnames to the results
for _ , res := range results {
if h := hostsByUUID [ res . DeviceID ] ; h != nil {
res . Hostname = hostsByUUID [ res . DeviceID ] . Hostname
}
}
2022-10-05 22:53:54 +00:00
return results , nil
}
2023-04-17 15:45:16 +00:00
type listMDMAppleCommandsRequest struct {
ListOptions fleet . ListOptions ` url:"list_options" `
}
type listMDMAppleCommandsResponse struct {
Results [ ] * fleet . MDMAppleCommand ` json:"results" `
Err error ` json:"error,omitempty" `
}
func ( r listMDMAppleCommandsResponse ) error ( ) error { return r . Err }
func listMDMAppleCommandsEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * listMDMAppleCommandsRequest )
results , err := svc . ListMDMAppleCommands ( ctx , & fleet . MDMAppleCommandListOptions {
ListOptions : req . ListOptions ,
} )
if err != nil {
return listMDMAppleCommandsResponse {
Err : err ,
} , nil
}
return listMDMAppleCommandsResponse {
Results : results ,
} , nil
}
func ( svc * Service ) ListMDMAppleCommands ( ctx context . Context , opts * fleet . MDMAppleCommandListOptions ) ( [ ] * fleet . MDMAppleCommand , error ) {
// first, authorize that the user has the right to list hosts
if err := svc . authz . Authorize ( ctx , & fleet . Host { } , fleet . ActionList ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return nil , fleet . ErrNoContext
}
// get the list of commands so we know what hosts (and therefore what teams)
// we're dealing with. Including the observers as they are allowed to view
// MDM Apple commands.
results , err := svc . ds . ListMDMAppleCommands ( ctx , fleet . TeamFilter {
User : vc . User ,
IncludeObserver : true ,
} , opts )
if err != nil {
return nil , err
}
// collect the different team IDs and verify that the user has access to view
// commands on all affected teams, do not assume that ListMDMAppleCommands
// only returned hosts that the user is authorized to view the command
// results of (that is, always verify with our rego authz policy).
teamIDs := make ( map [ uint ] bool )
for _ , res := range results {
var id uint
if res . TeamID != nil {
id = * res . TeamID
}
teamIDs [ id ] = true
}
// instead of returning an authz error if the user is not authorized for a
// team, we remove those commands from the results (as we want to return
// whatever the user is allowed to see). Since this can only be done after
// retrieving the list of commands, this may result in returning less results
// than requested, but it's ok - it's expected that the results retrieved
// from the datastore will all be authorized for the user.
var commandAuthz fleet . MDMAppleCommandAuthz
var authzErr error
for tmID := range teamIDs {
commandAuthz . TeamID = & tmID
if tmID == 0 {
commandAuthz . TeamID = nil
}
if err := svc . authz . Authorize ( ctx , commandAuthz , fleet . ActionRead ) ; err != nil {
if authzErr == nil {
authzErr = err
}
teamIDs [ tmID ] = false
}
}
if authzErr != nil {
level . Error ( svc . logger ) . Log ( "err" , "unauthorized to view some team commands" , "details" , authzErr )
// filter-out the teams that the user is not allowed to view
allowedResults := make ( [ ] * fleet . MDMAppleCommand , 0 , len ( results ) )
for _ , res := range results {
var id uint
if res . TeamID != nil {
id = * res . TeamID
}
if teamIDs [ id ] {
allowedResults = append ( allowedResults , res )
}
}
results = allowedResults
}
return results , nil
}
2023-02-17 15:28:28 +00:00
type newMDMAppleConfigProfileRequest struct {
TeamID uint
Profile * multipart . FileHeader
}
type newMDMAppleConfigProfileResponse struct {
ProfileID uint ` json:"profile_id" `
Err error ` json:"error,omitempty" `
}
// TODO(lucas): We parse the whole body before running svc.authz.Authorize.
// An authenticated but unauthorized user could abuse this.
func ( newMDMAppleConfigProfileRequest ) DecodeRequest ( ctx context . Context , r * http . Request ) ( interface { } , error ) {
decoded := newMDMAppleConfigProfileRequest { }
err := r . ParseMultipartForm ( 512 * units . MiB )
if err != nil {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [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] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , & fleet . BadRequestError {
Message : "failed to parse multipart form" ,
InternalErr : err ,
}
2023-02-17 15:28:28 +00:00
}
val , ok := r . MultipartForm . Value [ "team_id" ]
if ! ok || len ( val ) < 1 {
// default is no team
decoded . TeamID = 0
} else {
teamID , err := strconv . Atoi ( val [ 0 ] )
if err != nil {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [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] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , & fleet . BadRequestError { Message : fmt . Sprintf ( "failed to decode team_id in multipart form: %s" , err . Error ( ) ) }
2023-02-17 15:28:28 +00:00
}
decoded . TeamID = uint ( teamID )
}
fhs , ok := r . MultipartForm . File [ "profile" ]
if ! ok || len ( fhs ) < 1 {
return nil , & fleet . BadRequestError { Message : "no file headers for profile" }
}
decoded . Profile = fhs [ 0 ]
return & decoded , nil
}
func ( r newMDMAppleConfigProfileResponse ) error ( ) error { return r . Err }
func newMDMAppleConfigProfileEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * newMDMAppleConfigProfileRequest )
ff , err := req . Profile . Open ( )
if err != nil {
return & newMDMAppleConfigProfileResponse { Err : err } , nil
}
defer ff . Close ( )
cp , err := svc . NewMDMAppleConfigProfile ( ctx , req . TeamID , ff , req . Profile . Size )
if err != nil {
return & newMDMAppleConfigProfileResponse { Err : err } , nil
}
return & newMDMAppleConfigProfileResponse {
ProfileID : cp . ProfileID ,
} , nil
}
func ( svc * Service ) NewMDMAppleConfigProfile ( ctx context . Context , teamID uint , r io . Reader , size int64 ) ( * fleet . MDMAppleConfigProfile , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleConfigProfile { TeamID : & teamID } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
var teamName string
if teamID >= 1 {
tm , err := svc . EnterpriseOverrides . TeamByIDOrName ( ctx , & teamID , nil )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
teamName = tm . Name
}
b := make ( [ ] byte , size )
_ , err := r . Read ( b )
if err != nil {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [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] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : "failed to read config profile" ,
InternalErr : err ,
} )
2023-02-17 15:28:28 +00:00
}
2023-03-17 21:52:30 +00:00
cp , err := fleet . NewMDMAppleConfigProfile ( b , & teamID )
2023-02-17 15:28:28 +00:00
if err != nil {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [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] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : fmt . Sprintf ( "failed to parse config profile: %s" , err . Error ( ) ) ,
} )
2023-02-17 15:28:28 +00:00
}
2023-03-17 21:52:30 +00:00
if err := cp . ValidateUserProvided ( ) ; err != nil {
2023-02-17 15:28:28 +00:00
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError { Message : err . Error ( ) } )
}
newCP , err := svc . ds . NewMDMAppleConfigProfile ( ctx , * cp )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
2023-03-27 18:43:01 +00:00
if err := svc . ds . BulkSetPendingMDMAppleHostProfiles ( ctx , nil , nil , [ ] uint { newCP . ProfileID } , nil ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "bulk set pending host profiles" )
}
2023-02-17 15:28:28 +00:00
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , & fleet . ActivityTypeCreatedMacosProfile {
2023-03-17 21:16:18 +00:00
TeamID : & teamID ,
TeamName : & teamName ,
ProfileName : newCP . Name ,
ProfileIdentifier : newCP . Identifier ,
2023-02-17 15:28:28 +00:00
} ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "logging activity for create mdm apple config profile" )
}
return newCP , nil
}
type listMDMAppleConfigProfilesRequest struct {
TeamID uint ` query:"team_id,optional" `
}
type listMDMAppleConfigProfilesResponse struct {
ConfigProfiles [ ] * fleet . MDMAppleConfigProfile ` json:"profiles" `
Err error ` json:"error,omitempty" `
}
func ( r listMDMAppleConfigProfilesResponse ) error ( ) error { return r . Err }
func listMDMAppleConfigProfilesEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * listMDMAppleConfigProfilesRequest )
res := listMDMAppleConfigProfilesResponse { }
cps , err := svc . ListMDMAppleConfigProfiles ( ctx , req . TeamID )
if err != nil {
res . Err = err
return & res , err
}
res . ConfigProfiles = cps
return & res , nil
}
func ( svc * Service ) ListMDMAppleConfigProfiles ( ctx context . Context , teamID uint ) ( [ ] * fleet . MDMAppleConfigProfile , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleConfigProfile { TeamID : & teamID } , fleet . ActionRead ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
if teamID >= 1 {
// confirm that team exists
if _ , err := svc . ds . Team ( ctx , teamID ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
}
cps , err := svc . ds . ListMDMAppleConfigProfiles ( ctx , & teamID )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return cps , nil
}
type getMDMAppleConfigProfileRequest struct {
ProfileID uint ` url:"profile_id" `
}
type getMDMAppleConfigProfileResponse struct {
Err error ` json:"error,omitempty" `
// file fields below are used in hijackRender for the response
fileReader io . ReadCloser
fileLength int64
fileName string
}
func ( r getMDMAppleConfigProfileResponse ) error ( ) error { return r . Err }
func ( r getMDMAppleConfigProfileResponse ) hijackRender ( ctx context . Context , w http . ResponseWriter ) {
w . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( r . fileLength , 10 ) )
w . Header ( ) . Set ( "Content-Type" , "application/x-apple-aspen-config" )
w . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( ` attachment;filename="%s.mobileconfig" ` , r . fileName ) )
// OK to just log the error here as writing anything on
// `http.ResponseWriter` sets the status code to 200 (and it can't be
// changed.) Clients should rely on matching content-length with the
// header provided
wl , err := io . Copy ( w , r . fileReader )
if err != nil {
logging . WithExtras ( ctx , "mobileconfig_copy_error" , err , "bytes_copied" , wl )
}
r . fileReader . Close ( )
}
func getMDMAppleConfigProfileEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleConfigProfileRequest )
cp , err := svc . GetMDMAppleConfigProfile ( ctx , req . ProfileID )
if err != nil {
return getMDMAppleConfigProfileResponse { Err : err } , nil
}
reader := bytes . NewReader ( cp . Mobileconfig )
fileName := fmt . Sprintf ( "%s_%s" , time . Now ( ) . Format ( "2006-01-02" ) , strings . ReplaceAll ( cp . Name , " " , "_" ) )
return getMDMAppleConfigProfileResponse { fileReader : io . NopCloser ( reader ) , fileLength : reader . Size ( ) , fileName : fileName } , nil
}
func ( svc * Service ) GetMDMAppleConfigProfile ( ctx context . Context , profileID uint ) ( * fleet . MDMAppleConfigProfile , error ) {
// first we perform a perform basic authz check
if err := svc . authz . Authorize ( ctx , & fleet . Team { } , fleet . ActionRead ) ; err != nil {
return nil , err
}
cp , err := svc . ds . GetMDMAppleConfigProfile ( ctx , profileID )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
// now we can do a specific authz check based on team id of profile before we return the profile
if err := svc . authz . Authorize ( ctx , cp , fleet . ActionRead ) ; err != nil {
return nil , err
}
return cp , nil
}
type deleteMDMAppleConfigProfileRequest struct {
ProfileID uint ` url:"profile_id" `
}
type deleteMDMAppleConfigProfileResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deleteMDMAppleConfigProfileResponse ) error ( ) error { return r . Err }
func deleteMDMAppleConfigProfileEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * deleteMDMAppleConfigProfileRequest )
if err := svc . DeleteMDMAppleConfigProfile ( ctx , req . ProfileID ) ; err != nil {
return & deleteMDMAppleConfigProfileResponse { Err : err } , nil
}
return & deleteMDMAppleConfigProfileResponse { } , nil
}
func ( svc * Service ) DeleteMDMAppleConfigProfile ( ctx context . Context , profileID uint ) error {
// first we perform a perform basic authz check
if err := svc . authz . Authorize ( ctx , & fleet . Team { } , fleet . ActionRead ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
cp , err := svc . ds . GetMDMAppleConfigProfile ( ctx , profileID )
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
var teamName string
teamID := * cp . TeamID
if teamID >= 1 {
tm , err := svc . EnterpriseOverrides . TeamByIDOrName ( ctx , & teamID , nil )
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
teamName = tm . Name
}
// now we can do a specific authz check based on team id of profile before we delete the profile
if err := svc . authz . Authorize ( ctx , cp , fleet . ActionWrite ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
2023-03-17 21:52:30 +00:00
// prevent deleting profiles that are managed by Fleet
if _ , ok := mobileconfig . FleetPayloadIdentifiers ( ) [ cp . Identifier ] ; ok {
return & fleet . BadRequestError {
Message : "profiles managed by Fleet can't be deleted using this endpoint." ,
InternalErr : fmt . Errorf ( "deleting profile %s for team %s not allowed because it's managed by Fleet" , cp . Identifier , teamName ) ,
}
}
2023-02-17 15:28:28 +00:00
if err := svc . ds . DeleteMDMAppleConfigProfile ( ctx , profileID ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
2023-03-27 18:43:01 +00:00
// cannot use the profile ID as it is now deleted
if err := svc . ds . BulkSetPendingMDMAppleHostProfiles ( ctx , nil , [ ] uint { teamID } , nil , nil ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "bulk set pending host profiles" )
}
2023-02-17 15:28:28 +00:00
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , & fleet . ActivityTypeDeletedMacosProfile {
2023-03-17 21:16:18 +00:00
TeamID : & teamID ,
TeamName : & teamName ,
ProfileName : cp . Name ,
ProfileIdentifier : cp . Identifier ,
2023-02-17 15:28:28 +00:00
} ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "logging activity for delete mdm apple config profile" )
}
return nil
}
2023-03-02 00:36:59 +00:00
type getMDMAppleProfilesSummaryRequest struct {
TeamID * uint ` query:"team_id,optional" `
}
type getMDMAppleProfilesSummaryResponse struct {
2023-04-22 15:23:38 +00:00
fleet . MDMAppleConfigProfilesSummary
2023-03-02 00:36:59 +00:00
Err error ` json:"error,omitempty" `
}
func ( r getMDMAppleProfilesSummaryResponse ) error ( ) error { return r . Err }
func getMDMAppleProfilesSummaryEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleProfilesSummaryRequest )
res := getMDMAppleProfilesSummaryResponse { }
ps , err := svc . GetMDMAppleProfilesSummary ( ctx , req . TeamID )
if err != nil {
return & getMDMAppleProfilesSummaryResponse { Err : err } , nil
}
2023-04-24 21:27:15 +00:00
res . Verifying = ps . Verifying
2023-03-02 00:36:59 +00:00
res . Failed = ps . Failed
res . Pending = ps . Pending
return & res , nil
}
2023-04-22 15:23:38 +00:00
func ( svc * Service ) GetMDMAppleProfilesSummary ( ctx context . Context , teamID * uint ) ( * fleet . MDMAppleConfigProfilesSummary , error ) {
2023-03-02 00:36:59 +00:00
if err := svc . authz . Authorize ( ctx , fleet . MDMAppleConfigProfile { TeamID : teamID } , fleet . ActionRead ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
ps , err := svc . ds . GetMDMAppleHostsProfilesSummary ( ctx , teamID )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return ps , nil
}
2023-03-28 14:50:14 +00:00
type getMDMAppleFileVaultSummaryRequest struct {
TeamID * uint ` query:"team_id,optional" `
}
type getMDMAppleFileVauleSummaryResponse struct {
* fleet . MDMAppleFileVaultSummary
Err error ` json:"error,omitempty" `
}
func ( r getMDMAppleFileVauleSummaryResponse ) error ( ) error { return r . Err }
func getMdmAppleFileVaultSummaryEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleFileVaultSummaryRequest )
fvs , err := svc . GetMDMAppleFileVaultSummary ( ctx , req . TeamID )
if err != nil {
return & getMDMAppleFileVauleSummaryResponse { Err : err } , nil
}
return & getMDMAppleFileVauleSummaryResponse {
MDMAppleFileVaultSummary : fvs ,
} , nil
}
// QUESTION: workflow for developing new APIs? whats your setup quickly test code working?
func ( svc * Service ) GetMDMAppleFileVaultSummary ( ctx context . Context , teamID * uint ) ( * fleet . MDMAppleFileVaultSummary , error ) {
if err := svc . authz . Authorize ( ctx , fleet . MDMAppleConfigProfile { TeamID : teamID } , fleet . ActionRead ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
fvs , err := svc . ds . GetMDMAppleFileVaultSummary ( ctx , teamID )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return fvs , nil
}
2022-10-05 22:53:54 +00:00
type uploadAppleInstallerRequest struct {
Installer * multipart . FileHeader
}
type uploadAppleInstallerResponse struct {
ID uint ` json:"installer_id" `
Err error ` json:"error,omitempty" `
}
// TODO(lucas): We parse the whole body before running svc.authz.Authorize.
// An authenticated but unauthorized user could abuse this.
func ( uploadAppleInstallerRequest ) DecodeRequest ( ctx context . Context , r * http . Request ) ( interface { } , error ) {
err := r . ParseMultipartForm ( 512 * units . MiB )
if err != nil {
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [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] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , & fleet . BadRequestError {
Message : "failed to parse multipart form" ,
InternalErr : err ,
}
2022-10-05 22:53:54 +00:00
}
installer := r . MultipartForm . File [ "installer" ] [ 0 ]
return & uploadAppleInstallerRequest {
Installer : installer ,
} , nil
}
func ( r uploadAppleInstallerResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func uploadAppleInstallerEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * uploadAppleInstallerRequest )
ff , err := req . Installer . Open ( )
if err != nil {
return uploadAppleInstallerResponse { Err : err } , nil
}
defer ff . Close ( )
installer , err := svc . UploadMDMAppleInstaller ( ctx , req . Installer . Filename , req . Installer . Size , ff )
if err != nil {
return uploadAppleInstallerResponse { Err : err } , nil
}
return & uploadAppleInstallerResponse {
ID : installer . ID ,
} , nil
}
func ( svc * Service ) UploadMDMAppleInstaller ( ctx context . Context , name string , size int64 , installer io . Reader ) ( * fleet . MDMAppleInstaller , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleInstaller { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
token := uuid . New ( ) . String ( )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
url := svc . installerURL ( token , appConfig )
var installerBuf bytes . Buffer
manifest , err := createManifest ( size , io . TeeReader ( installer , & installerBuf ) , url )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
inst , err := svc . ds . NewMDMAppleInstaller ( ctx , name , size , manifest , installerBuf . Bytes ( ) , token )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return inst , nil
}
func ( svc * Service ) installerURL ( token string , appConfig * fleet . AppConfig ) string {
return fmt . Sprintf ( "%s%s?token=%s" , appConfig . ServerSettings . ServerURL , apple_mdm . InstallerPath , token )
}
func createManifest ( size int64 , installer io . Reader , url string ) ( string , error ) {
2023-04-07 20:31:02 +00:00
manifest , err := appmanifest . New ( & readerWithSize {
2022-10-05 22:53:54 +00:00
Reader : installer ,
size : size ,
} , url )
if err != nil {
return "" , fmt . Errorf ( "create manifest file: %w" , err )
}
var buf bytes . Buffer
enc := plist . NewEncoder ( & buf )
enc . Indent ( " " )
if err := enc . Encode ( manifest ) ; err != nil {
return "" , fmt . Errorf ( "encode manifest: %w" , err )
}
return buf . String ( ) , nil
}
type readerWithSize struct {
io . Reader
size int64
}
func ( r * readerWithSize ) Size ( ) int64 {
return r . size
}
type getAppleInstallerDetailsRequest struct {
ID uint ` url:"installer_id" `
}
type getAppleInstallerDetailsResponse struct {
Installer * fleet . MDMAppleInstaller
Err error ` json:"error,omitempty" `
}
func ( r getAppleInstallerDetailsResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func getAppleInstallerEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * getAppleInstallerDetailsRequest )
installer , err := svc . GetMDMAppleInstallerByID ( ctx , req . ID )
if err != nil {
return getAppleInstallerDetailsResponse { Err : err } , nil
}
return & getAppleInstallerDetailsResponse {
Installer : installer ,
} , nil
}
func ( svc * Service ) GetMDMAppleInstallerByID ( ctx context . Context , id uint ) ( * fleet . MDMAppleInstaller , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleInstaller { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
inst , err := svc . ds . MDMAppleInstallerDetailsByID ( ctx , id )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return inst , nil
}
type deleteAppleInstallerDetailsRequest struct {
ID uint ` url:"installer_id" `
}
type deleteAppleInstallerDetailsResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deleteAppleInstallerDetailsResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func deleteAppleInstallerEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * deleteAppleInstallerDetailsRequest )
if err := svc . DeleteMDMAppleInstaller ( ctx , req . ID ) ; err != nil {
return deleteAppleInstallerDetailsResponse { Err : err } , nil
}
return & deleteAppleInstallerDetailsResponse { } , nil
}
func ( svc * Service ) DeleteMDMAppleInstaller ( ctx context . Context , id uint ) error {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleInstaller { } , fleet . ActionWrite ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
if err := svc . ds . DeleteMDMAppleInstaller ( ctx , id ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
return nil
}
type listMDMAppleDevicesRequest struct { }
type listMDMAppleDevicesResponse struct {
Devices [ ] fleet . MDMAppleDevice ` json:"devices" `
Err error ` json:"error,omitempty" `
}
func ( r listMDMAppleDevicesResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func listMDMAppleDevicesEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
devices , err := svc . ListMDMAppleDevices ( ctx )
if err != nil {
return listMDMAppleDevicesResponse { Err : err } , nil
}
return & listMDMAppleDevicesResponse {
Devices : devices ,
} , nil
}
func ( svc * Service ) ListMDMAppleDevices ( ctx context . Context ) ( [ ] fleet . MDMAppleDevice , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleDevice { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return svc . ds . MDMAppleListDevices ( ctx )
}
type listMDMAppleDEPDevicesRequest struct { }
type listMDMAppleDEPDevicesResponse struct {
Devices [ ] fleet . MDMAppleDEPDevice ` json:"devices" `
Err error ` json:"error,omitempty" `
}
func ( r listMDMAppleDEPDevicesResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func listMDMAppleDEPDevicesEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
devices , err := svc . ListMDMAppleDEPDevices ( ctx )
if err != nil {
return listMDMAppleDEPDevicesResponse { Err : err } , nil
}
return & listMDMAppleDEPDevicesResponse {
Devices : devices ,
} , nil
}
func ( svc * Service ) ListMDMAppleDEPDevices ( ctx context . Context ) ( [ ] fleet . MDMAppleDEPDevice , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleDEPDevice { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
2023-01-31 14:46:01 +00:00
depClient := apple_mdm . NewDEPClient ( svc . depStorage , svc . ds , svc . logger )
2022-10-05 22:53:54 +00:00
// TODO(lucas): Use cursors and limit to fetch in multiple requests.
// This single-request version supports up to 1000 devices (max to return in one call).
fetchDevicesResponse , err := depClient . FetchDevices ( ctx , apple_mdm . DEPName , godep . WithLimit ( 1000 ) )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
devices := make ( [ ] fleet . MDMAppleDEPDevice , len ( fetchDevicesResponse . Devices ) )
for i := range fetchDevicesResponse . Devices {
devices [ i ] = fleet . MDMAppleDEPDevice { Device : fetchDevicesResponse . Devices [ i ] }
}
return devices , nil
}
2022-12-16 18:39:36 +00:00
type newMDMAppleDEPKeyPairResponse struct {
PublicKey [ ] byte ` json:"public_key,omitempty" `
PrivateKey [ ] byte ` json:"private_key,omitempty" `
Err error ` json:"error,omitempty" `
}
func ( r newMDMAppleDEPKeyPairResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func newMDMAppleDEPKeyPairEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-12-16 18:39:36 +00:00
keyPair , err := svc . NewMDMAppleDEPKeyPair ( ctx )
if err != nil {
return newMDMAppleDEPKeyPairResponse {
Err : err ,
} , nil
}
return newMDMAppleDEPKeyPairResponse {
PublicKey : keyPair . PublicKey ,
PrivateKey : keyPair . PrivateKey ,
} , nil
}
func ( svc * Service ) NewMDMAppleDEPKeyPair ( ctx context . Context ) ( * fleet . MDMAppleDEPKeyPair , error ) {
// skipauth: Generating a new key pair does not actually make any changes to fleet, or expose any
// information. The user must configure fleet with the new key pair and restart the server.
svc . authz . SkipAuthorization ( ctx )
publicKeyPEM , privateKeyPEM , err := apple_mdm . NewDEPKeyPairPEM ( )
if err != nil {
return nil , fmt . Errorf ( "generate key pair: %w" , err )
}
return & fleet . MDMAppleDEPKeyPair {
PublicKey : publicKeyPEM ,
PrivateKey : privateKeyPEM ,
} , nil
}
2022-10-05 22:53:54 +00:00
type enqueueMDMAppleCommandRequest struct {
Command string ` json:"command" `
DeviceIDs [ ] string ` json:"device_ids" `
}
type enqueueMDMAppleCommandResponse struct {
2023-04-03 18:25:49 +00:00
* fleet . CommandEnqueueResult
status int ` json:"-" `
Err error ` json:"error,omitempty" `
2022-10-05 22:53:54 +00:00
}
func ( r enqueueMDMAppleCommandResponse ) error ( ) error { return r . Err }
func ( r enqueueMDMAppleCommandResponse ) Status ( ) int { return r . status }
2022-12-27 14:26:59 +00:00
func enqueueMDMAppleCommandEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * enqueueMDMAppleCommandRequest )
2023-04-03 18:25:49 +00:00
status , result , err := svc . EnqueueMDMAppleCommand ( ctx , req . Command , req . DeviceIDs , false )
2022-10-05 22:53:54 +00:00
if err != nil {
return enqueueMDMAppleCommandResponse { Err : err } , nil
}
return enqueueMDMAppleCommandResponse {
2023-04-03 18:25:49 +00:00
status : status ,
CommandEnqueueResult : result ,
2022-10-05 22:53:54 +00:00
} , nil
}
func ( svc * Service ) EnqueueMDMAppleCommand (
ctx context . Context ,
2023-04-03 18:25:49 +00:00
rawBase64Cmd string ,
2022-10-05 22:53:54 +00:00
deviceIDs [ ] string ,
noPush bool ,
) ( status int , result * fleet . CommandEnqueueResult , err error ) {
2023-04-17 15:08:55 +00:00
premiumCommands := map [ string ] bool {
2023-04-03 18:25:49 +00:00
"EraseDevice" : true ,
"DeviceLock" : true ,
}
2023-04-18 10:53:33 +00:00
// load hosts (lite) by uuids, check that the user has the rights to run
2023-04-03 18:25:49 +00:00
// commands for every affected team.
if err := svc . authz . Authorize ( ctx , & fleet . Host { } , fleet . ActionList ) ; err != nil {
2022-10-05 22:53:54 +00:00
return 0 , nil , ctxerr . Wrap ( ctx , err )
}
2023-04-03 18:25:49 +00:00
vc , ok := viewer . FromContext ( ctx )
if ! ok {
return 0 , nil , fleet . ErrNoContext
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
// for the team filter, we don't include observers as we require maintainer
// and up to run commands.
filter := fleet . TeamFilter { User : vc . User , IncludeObserver : false }
hosts , err := svc . ds . ListHostsLiteByUUIDs ( ctx , filter , deviceIDs )
if err != nil {
return 0 , nil , err
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
if len ( hosts ) == 0 {
return 0 , nil , newNotFoundError ( )
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
// collect the team IDs and verify that the user has access to run commands
// on all affected teams.
teamIDs := make ( map [ uint ] bool )
for _ , h := range hosts {
var id uint
if h . TeamID != nil {
id = * h . TeamID
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
teamIDs [ id ] = true
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
2023-04-17 15:45:16 +00:00
var commandAuthz fleet . MDMAppleCommandAuthz
2023-04-03 18:25:49 +00:00
for tmID := range teamIDs {
2023-04-17 15:45:16 +00:00
commandAuthz . TeamID = & tmID
2023-04-03 18:25:49 +00:00
if tmID == 0 {
2023-04-17 15:45:16 +00:00
commandAuthz . TeamID = nil
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
2023-04-17 15:45:16 +00:00
if err := svc . authz . Authorize ( ctx , commandAuthz , fleet . ActionWrite ) ; err != nil {
2023-04-03 18:25:49 +00:00
return 0 , nil , ctxerr . Wrap ( ctx , err )
2022-10-05 22:53:54 +00:00
}
}
2023-04-03 18:25:49 +00:00
rawXMLCmd , err := base64 . RawStdEncoding . DecodeString ( rawBase64Cmd )
if err != nil {
return 0 , nil , ctxerr . Wrap ( ctx , err , "decode base64 command" )
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
cmd , err := mdm . DecodeCommand ( rawXMLCmd )
if err != nil {
return 0 , nil , ctxerr . Wrap ( ctx , err , "decode plist command" )
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
if premiumCommands [ strings . TrimSpace ( cmd . Command . RequestType ) ] {
lic , err := svc . License ( ctx )
if err != nil {
return 0 , nil , ctxerr . Wrap ( ctx , err , "get license" )
}
if ! lic . IsPremium ( ) {
return 0 , nil , fleet . ErrMissingLicense
}
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
2023-04-05 14:50:36 +00:00
if err := svc . mdmAppleCommander . EnqueueCommand ( ctx , deviceIDs , string ( rawXMLCmd ) ) ; err != nil {
// if at least one UUID enqueued properly, return success, otherwise return
// 500
var apnsErr * apple_mdm . APNSDeliveryError
2023-04-03 18:25:49 +00:00
var mysqlErr * mysql . MySQLError
if errors . As ( err , & apnsErr ) {
if len ( apnsErr . FailedUUIDs ) < len ( deviceIDs ) {
// some hosts properly received the command, so return success, with the list
// of failed uuids.
return http . StatusOK , & fleet . CommandEnqueueResult {
CommandUUID : cmd . CommandUUID ,
RequestType : cmd . Command . RequestType ,
FailedUUIDs : apnsErr . FailedUUIDs ,
} , nil
}
} else if errors . As ( err , & mysqlErr ) {
// enqueue may fail with a foreign key constraint error 1452 when one of
// the hosts provided is not enrolled in nano_enrollments. Detect when
// that's the case and add information to the error.
if mysqlErr . Number == mysqlerr . ER_NO_REFERENCED_ROW_2 {
err := fleet . NewInvalidArgumentError ( "device_ids" , fmt . Sprintf ( "at least one of the hosts is not enrolled in MDM: %v" , err ) )
return http . StatusInternalServerError , nil , ctxerr . Wrap ( ctx , err , "enqueue command" )
}
}
return http . StatusInternalServerError , nil , ctxerr . Wrap ( ctx , err , "enqueue command" )
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
return http . StatusOK , & fleet . CommandEnqueueResult {
CommandUUID : cmd . CommandUUID ,
RequestType : cmd . Command . RequestType ,
} , nil
2022-10-05 22:53:54 +00:00
}
type mdmAppleEnrollRequest struct {
Token string ` query:"token" `
}
func ( r mdmAppleEnrollResponse ) error ( ) error { return r . Err }
type mdmAppleEnrollResponse struct {
Err error ` json:"error,omitempty" `
// Profile field is used in hijackRender for the response.
Profile [ ] byte
}
func ( r mdmAppleEnrollResponse ) hijackRender ( ctx context . Context , w http . ResponseWriter ) {
w . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( int64 ( len ( r . Profile ) ) , 10 ) )
w . Header ( ) . Set ( "Content-Type" , "application/x-apple-aspen-config" )
// OK to just log the error here as writing anything on
// `http.ResponseWriter` sets the status code to 200 (and it can't be
// changed.) Clients should rely on matching content-length with the
// header provided.
if n , err := w . Write ( r . Profile ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "written" , n )
}
}
2022-12-27 14:26:59 +00:00
func mdmAppleEnrollEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * mdmAppleEnrollRequest )
profile , err := svc . GetMDMAppleEnrollmentProfileByToken ( ctx , req . Token )
if err != nil {
return mdmAppleEnrollResponse { Err : err } , nil
}
return mdmAppleEnrollResponse {
Profile : profile ,
} , nil
}
func ( svc * Service ) GetMDMAppleEnrollmentProfileByToken ( ctx context . Context , token string ) ( profile [ ] byte , err error ) {
// skipauth: The enroll profile endpoint is unauthenticated.
svc . authz . SkipAuthorization ( ctx )
_ , err = svc . ds . GetMDMAppleEnrollmentProfileByToken ( ctx , token )
if err != nil {
if fleet . IsNotFound ( err ) {
return nil , fleet . NewAuthFailedError ( "enrollment profile not found" )
}
return nil , ctxerr . Wrap ( ctx , err , "get enrollment profile" )
}
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
// TODO(lucas): Actually use enrollment (when we define which configuration we want to define
// on enrollments).
2023-03-13 13:33:32 +00:00
mobileconfig , err := apple_mdm . GenerateEnrollmentProfileMobileconfig (
2022-10-05 22:53:54 +00:00
appConfig . OrgInfo . OrgName ,
appConfig . ServerSettings . ServerURL ,
2023-03-23 10:30:28 +00:00
svc . config . MDM . AppleSCEPChallenge ,
2022-10-05 22:53:54 +00:00
svc . mdmPushCertTopic ,
)
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return mobileconfig , nil
}
2023-01-23 23:05:24 +00:00
type mdmAppleCommandRemoveEnrollmentProfileRequest struct {
HostID uint ` url:"id" `
}
type mdmAppleCommandRemoveEnrollmentProfileResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r mdmAppleCommandRemoveEnrollmentProfileResponse ) error ( ) error { return r . Err }
func mdmAppleCommandRemoveEnrollmentProfileEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * mdmAppleCommandRemoveEnrollmentProfileRequest )
err := svc . EnqueueMDMAppleCommandRemoveEnrollmentProfile ( ctx , req . HostID )
if err != nil {
return mdmAppleCommandRemoveEnrollmentProfileResponse { Err : err } , nil
}
return mdmAppleCommandRemoveEnrollmentProfileResponse { } , nil
}
func ( svc * Service ) EnqueueMDMAppleCommandRemoveEnrollmentProfile ( ctx context . Context , hostID uint ) error {
if err := svc . authz . Authorize ( ctx , & fleet . Host { } , fleet . ActionList ) ; err != nil {
return err
}
h , err := svc . ds . HostLite ( ctx , hostID )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting host info for mdm apple remove profile command" )
}
info , err := svc . ds . GetHostMDMCheckinInfo ( ctx , h . UUID )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting mdm checkin info for mdm apple remove profile command" )
}
2023-04-18 10:53:33 +00:00
// Check authorization again based on host info for team-based permissions.
if err := svc . authz . Authorize ( ctx , fleet . MDMAppleCommandAuthz {
TeamID : h . TeamID ,
} , fleet . ActionWrite ) ; err != nil {
2023-01-23 23:05:24 +00:00
return err
}
2023-03-27 18:43:01 +00:00
nanoEnroll , err := svc . ds . GetNanoMDMEnrollment ( ctx , h . UUID )
2023-01-23 23:05:24 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting mdm enrollment status for mdm apple remove profile command" )
}
2023-03-27 18:43:01 +00:00
if nanoEnroll == nil || ! nanoEnroll . Enabled {
2023-01-23 23:05:24 +00:00
return fleet . NewUserMessageError ( ctxerr . New ( ctx , fmt . Sprintf ( "mdm is not enabled for host %d" , hostID ) ) , http . StatusConflict )
}
2023-02-22 17:49:06 +00:00
cmdUUID := uuid . New ( ) . String ( )
err = svc . mdmAppleCommander . RemoveProfile ( ctx , [ ] string { h . UUID } , apple_mdm . FleetPayloadIdentifier , cmdUUID )
2023-01-23 23:05:24 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "enqueuing mdm apple remove profile command" )
}
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , & fleet . ActivityTypeMDMUnenrolled {
HostSerial : h . HardwareSerial ,
HostDisplayName : h . DisplayName ( ) ,
InstalledFromDEP : info . InstalledFromDEP ,
} ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "logging activity for mdm apple remove profile command" )
}
return svc . pollResultMDMAppleCommandRemoveEnrollmentProfile ( ctx , cmdUUID , h . UUID )
}
func ( svc * Service ) pollResultMDMAppleCommandRemoveEnrollmentProfile ( ctx context . Context , cmdUUID string , deviceID string ) error {
ctx , cancelFn := context . WithDeadline ( ctx , time . Now ( ) . Add ( 5 * time . Second ) )
ticker := time . NewTicker ( 300 * time . Millisecond )
defer func ( ) {
ticker . Stop ( )
cancelFn ( )
} ( )
for {
select {
case <- ctx . Done ( ) :
// time out after 5 seconds
return fleet . MDMAppleCommandTimeoutError { }
case <- ticker . C :
2023-03-27 18:43:01 +00:00
nanoEnroll , err := svc . ds . GetNanoMDMEnrollment ( ctx , deviceID )
2023-01-23 23:05:24 +00:00
if err != nil {
level . Error ( svc . logger ) . Log ( "err" , "get nanomdm enrollment status" , "details" , err , "id" , deviceID , "command_uuid" , cmdUUID )
return err
}
2023-03-27 18:43:01 +00:00
if nanoEnroll != nil && nanoEnroll . Enabled {
2023-01-23 23:05:24 +00:00
// check again on the next tick
continue
}
// success, mdm enrollment is no longer enabled for the device
level . Info ( svc . logger ) . Log ( "msg" , "mdm disabled for device" , "id" , deviceID , "command_uuid" , cmdUUID )
return nil
}
}
}
2022-10-05 22:53:54 +00:00
type mdmAppleGetInstallerRequest struct {
Token string ` query:"token" `
}
func ( r mdmAppleGetInstallerResponse ) error ( ) error { return r . Err }
type mdmAppleGetInstallerResponse struct {
Err error ` json:"error,omitempty" `
// head is used by hijackRender for the response.
head bool
// Name field is used in hijackRender for the response.
name string
// Size field is used in hijackRender for the response.
size int64
// Installer field is used in hijackRender for the response.
installer [ ] byte
}
func ( r mdmAppleGetInstallerResponse ) hijackRender ( ctx context . Context , w http . ResponseWriter ) {
w . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( r . size , 10 ) )
w . Header ( ) . Set ( "Content-Type" , "application/octet-stream" )
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( ` attachment;filename="%s" ` , r . name ) )
if r . head {
w . WriteHeader ( http . StatusOK )
return
}
// OK to just log the error here as writing anything on
// `http.ResponseWriter` sets the status code to 200 (and it can't be
// changed.) Clients should rely on matching content-length with the
// header provided
if n , err := w . Write ( r . installer ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "bytes_copied" , n )
}
}
2022-12-27 14:26:59 +00:00
func mdmAppleGetInstallerEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * mdmAppleGetInstallerRequest )
installer , err := svc . GetMDMAppleInstallerByToken ( ctx , req . Token )
if err != nil {
return mdmAppleGetInstallerResponse { Err : err } , nil
}
return mdmAppleGetInstallerResponse {
head : false ,
name : installer . Name ,
size : installer . Size ,
installer : installer . Installer ,
} , nil
}
func ( svc * Service ) GetMDMAppleInstallerByToken ( ctx context . Context , token string ) ( * fleet . MDMAppleInstaller , error ) {
// skipauth: The installer endpoint uses token authentication.
svc . authz . SkipAuthorization ( ctx )
installer , err := svc . ds . MDMAppleInstaller ( ctx , token )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return installer , nil
}
type mdmAppleHeadInstallerRequest struct {
Token string ` query:"token" `
}
2022-12-27 14:26:59 +00:00
func mdmAppleHeadInstallerEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
req := request . ( * mdmAppleHeadInstallerRequest )
installer , err := svc . GetMDMAppleInstallerDetailsByToken ( ctx , req . Token )
if err != nil {
return mdmAppleGetInstallerResponse { Err : err } , nil
}
return mdmAppleGetInstallerResponse {
head : true ,
name : installer . Name ,
size : installer . Size ,
} , nil
}
func ( svc * Service ) GetMDMAppleInstallerDetailsByToken ( ctx context . Context , token string ) ( * fleet . MDMAppleInstaller , error ) {
// skipauth: The installer endpoint uses token authentication.
svc . authz . SkipAuthorization ( ctx )
installer , err := svc . ds . MDMAppleInstallerDetailsByToken ( ctx , token )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return installer , nil
}
type listMDMAppleInstallersRequest struct { }
type listMDMAppleInstallersResponse struct {
Installers [ ] fleet . MDMAppleInstaller ` json:"installers" `
Err error ` json:"error,omitempty" `
}
func ( r listMDMAppleInstallersResponse ) error ( ) error { return r . Err }
2022-12-27 14:26:59 +00:00
func listMDMAppleInstallersEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
2022-10-05 22:53:54 +00:00
installers , err := svc . ListMDMAppleInstallers ( ctx )
if err != nil {
return listMDMAppleInstallersResponse {
Err : err ,
} , nil
}
return listMDMAppleInstallersResponse {
Installers : installers ,
} , nil
}
func ( svc * Service ) ListMDMAppleInstallers ( ctx context . Context ) ( [ ] fleet . MDMAppleInstaller , error ) {
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleInstaller { } , fleet . ActionWrite ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
installers , err := svc . ds . ListMDMAppleInstallers ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
for i := range installers {
installers [ i ] . URL = svc . installerURL ( installers [ i ] . URLToken , appConfig )
}
return installers , nil
}
2023-01-16 20:06:30 +00:00
2023-02-22 20:11:44 +00:00
////////////////////////////////////////////////////////////////////////////////
// Lock a device
////////////////////////////////////////////////////////////////////////////////
type deviceLockRequest struct {
HostID uint ` url:"id" `
}
type deviceLockResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deviceLockResponse ) error ( ) error { return r . Err }
func ( r deviceLockResponse ) Status ( ) int { return http . StatusNoContent }
func deviceLockEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * deviceLockRequest )
err := svc . MDMAppleDeviceLock ( ctx , req . HostID )
if err != nil {
return deviceLockResponse { Err : err } , nil
}
return deviceLockResponse { } , nil
}
func ( svc * Service ) MDMAppleDeviceLock ( ctx context . Context , hostID uint ) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Wipe a device
////////////////////////////////////////////////////////////////////////////////
type deviceWipeRequest struct {
HostID uint ` url:"id" `
}
type deviceWipeResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deviceWipeResponse ) error ( ) error { return r . Err }
func ( r deviceWipeResponse ) Status ( ) int { return http . StatusNoContent }
func deviceWipeEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * deviceWipeRequest )
err := svc . MDMAppleEraseDevice ( ctx , req . HostID )
if err != nil {
return deviceWipeResponse { Err : err } , nil
}
return deviceWipeResponse { } , nil
}
func ( svc * Service ) MDMAppleEraseDevice ( ctx context . Context , hostID uint ) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
2023-02-15 18:01:44 +00:00
////////////////////////////////////////////////////////////////////////////////
// Batch Replace MDM Apple Profiles
////////////////////////////////////////////////////////////////////////////////
type batchSetMDMAppleProfilesRequest struct {
TeamID * uint ` json:"-" query:"team_id,optional" `
TeamName * string ` json:"-" query:"team_name,optional" `
DryRun bool ` json:"-" query:"dry_run,optional" ` // if true, apply validation but do not save changes
Profiles [ ] [ ] byte ` json:"profiles" `
}
type batchSetMDMAppleProfilesResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r batchSetMDMAppleProfilesResponse ) error ( ) error { return r . Err }
func ( r batchSetMDMAppleProfilesResponse ) Status ( ) int { return http . StatusNoContent }
func batchSetMDMAppleProfilesEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * batchSetMDMAppleProfilesRequest )
if err := svc . BatchSetMDMAppleProfiles ( ctx , req . TeamID , req . TeamName , req . Profiles , req . DryRun ) ; err != nil {
return batchSetMDMAppleProfilesResponse { Err : err } , nil
}
return batchSetMDMAppleProfilesResponse { } , nil
}
func ( svc * Service ) BatchSetMDMAppleProfiles ( ctx context . Context , tmID * uint , tmName * string , profiles [ ] [ ] byte , dryRun bool ) error {
if tmID != nil && tmName != nil {
svc . authz . SkipAuthorization ( ctx ) // so that the error message is not replaced by "forbidden"
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "team_name" , "cannot specify both team_id and team_name" ) )
}
if tmID != nil || tmName != nil {
2023-04-17 15:08:55 +00:00
license , _ := license . FromContext ( ctx )
2023-02-15 18:01:44 +00:00
if ! license . IsPremium ( ) {
field := "team_id"
if tmName != nil {
field = "team_name"
}
svc . authz . SkipAuthorization ( ctx ) // so that the error message is not replaced by "forbidden"
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( field , ErrMissingLicense . Error ( ) ) )
}
}
// if the team name is provided, load the corresponding team to get its id.
2023-02-16 16:53:26 +00:00
// vice-versa, if the id is provided, load it to get the name (required for
// the activity).
if tmName != nil || tmID != nil {
tm , err := svc . EnterpriseOverrides . TeamByIDOrName ( ctx , tmID , tmName )
2023-02-15 18:01:44 +00:00
if err != nil {
return err
}
2023-02-16 16:53:26 +00:00
if tmID == nil {
tmID = & tm . ID
} else {
tmName = & tm . Name
}
2023-02-15 18:01:44 +00:00
}
if err := svc . authz . Authorize ( ctx , & fleet . MDMAppleConfigProfile { TeamID : tmID } , fleet . ActionWrite ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
2023-04-17 15:08:55 +00:00
appCfg , err := svc . ds . AppConfig ( ctx )
2023-03-27 19:30:29 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
if ! appCfg . MDM . EnabledAndConfigured {
2023-03-08 13:31:53 +00:00
// NOTE: in order to prevent an error when Fleet MDM is not enabled but no
// profile is provided, which can happen if a user runs `fleetctl get
// config` and tries to apply that YAML, as it will contain an empty/null
// custom_settings key, we just return a success response in this
// situation.
if len ( profiles ) == 0 {
return nil
}
2023-03-27 19:30:29 +00:00
return ctxerr . Wrap ( ctx , fleet . NewInvalidArgumentError ( "mdm" , "cannot set custom settings: Fleet MDM is not configured" ) )
2023-03-08 13:31:53 +00:00
}
2023-02-15 18:01:44 +00:00
// any duplicate identifier or name in the provided set results in an error
profs := make ( [ ] * fleet . MDMAppleConfigProfile , 0 , len ( profiles ) )
byName , byIdent := make ( map [ string ] bool , len ( profiles ) ) , make ( map [ string ] bool , len ( profiles ) )
for i , prof := range profiles {
2023-03-17 21:52:30 +00:00
mdmProf , err := fleet . NewMDMAppleConfigProfile ( prof , tmID )
2023-02-15 18:01:44 +00:00
if err != nil {
return ctxerr . Wrap ( ctx ,
fleet . NewInvalidArgumentError ( fmt . Sprintf ( "profiles[%d]" , i ) , err . Error ( ) ) ,
"invalid mobileconfig profile" )
}
2023-03-17 21:52:30 +00:00
if err := mdmProf . ValidateUserProvided ( ) ; err != nil {
2023-02-24 20:12:53 +00:00
return ctxerr . Wrap ( ctx ,
fleet . NewInvalidArgumentError ( fmt . Sprintf ( "profiles[%d]" , i ) , err . Error ( ) ) )
}
2023-02-15 18:01:44 +00:00
if byName [ mdmProf . Name ] {
return ctxerr . Wrap ( ctx ,
fleet . NewInvalidArgumentError ( fmt . Sprintf ( "profiles[%d]" , i ) , fmt . Sprintf ( "Couldn’ t edit custom_settings. More than one configuration profile have the same name (PayloadDisplayName): %q" , mdmProf . Name ) ) ,
"duplicate mobileconfig profile by name" )
}
byName [ mdmProf . Name ] = true
if byIdent [ mdmProf . Identifier ] {
return ctxerr . Wrap ( ctx ,
fleet . NewInvalidArgumentError ( fmt . Sprintf ( "profiles[%d]" , i ) , fmt . Sprintf ( "Couldn’ t edit custom_settings. More than one configuration profile have the same identifier (PayloadIdentifier): %q" , mdmProf . Identifier ) ) ,
"duplicate mobileconfig profile by identifier" )
}
byIdent [ mdmProf . Identifier ] = true
profs = append ( profs , mdmProf )
}
if dryRun {
return nil
}
2023-02-16 16:53:26 +00:00
if err := svc . ds . BatchSetMDMAppleProfiles ( ctx , tmID , profs ) ; err != nil {
return err
}
2023-03-27 18:43:01 +00:00
var bulkTeamID uint
if tmID != nil {
bulkTeamID = * tmID
}
if err := svc . ds . BulkSetPendingMDMAppleHostProfiles ( ctx , nil , [ ] uint { bulkTeamID } , nil , nil ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "bulk set pending host profiles" )
}
2023-02-16 16:53:26 +00:00
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , & fleet . ActivityTypeEditedMacosProfile {
TeamID : tmID ,
TeamName : tmName ,
} ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "logging activity for edited macos profile" )
}
return nil
2023-02-15 18:01:44 +00:00
}
2023-03-06 14:54:51 +00:00
////////////////////////////////////////////////////////////////////////////////
// Update MDM Apple Settings
////////////////////////////////////////////////////////////////////////////////
type updateMDMAppleSettingsRequest struct {
fleet . MDMAppleSettingsPayload
}
type updateMDMAppleSettingsResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r updateMDMAppleSettingsResponse ) error ( ) error { return r . Err }
func ( r updateMDMAppleSettingsResponse ) Status ( ) int { return http . StatusNoContent }
// This endpoint is required because the UI must allow maintainers (in addition
// to admins) to update some MDM Apple settings, while the update config/update
// team endpoints only allow write access to admins.
func updateMDMAppleSettingsEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * updateMDMAppleSettingsRequest )
if err := svc . UpdateMDMAppleSettings ( ctx , req . MDMAppleSettingsPayload ) ; err != nil {
return updateMDMAppleSettingsResponse { Err : err } , nil
}
return updateMDMAppleSettingsResponse { } , nil
}
func ( svc * Service ) UpdateMDMAppleSettings ( ctx context . Context , payload fleet . MDMAppleSettingsPayload ) error {
// for now, assume all settings require premium (this is true for the first
// supported setting, enable_disk_encryption. Adjust as needed in the future
// if this is not always the case).
license , err := svc . License ( ctx )
if err != nil {
svc . authz . SkipAuthorization ( ctx ) // so that the error message is not replaced by "forbidden"
return err
}
if ! license . IsPremium ( ) {
svc . authz . SkipAuthorization ( ctx ) // so that the error message is not replaced by "forbidden"
return ErrMissingLicense
}
if err := svc . authz . Authorize ( ctx , payload , fleet . ActionWrite ) ; err != nil {
return ctxerr . Wrap ( ctx , err )
}
if payload . TeamID != nil {
tm , err := svc . EnterpriseOverrides . TeamByIDOrName ( ctx , payload . TeamID , nil )
if err != nil {
return err
}
return svc . EnterpriseOverrides . UpdateTeamMDMAppleSettings ( ctx , tm , payload )
}
return svc . updateAppConfigMDMAppleSettings ( ctx , payload )
}
func ( svc * Service ) updateAppConfigMDMAppleSettings ( ctx context . Context , payload fleet . MDMAppleSettingsPayload ) error {
2023-03-28 18:23:15 +00:00
ac , err := svc . AppConfigObfuscated ( ctx )
2023-03-06 14:54:51 +00:00
if err != nil {
return err
}
2023-03-08 13:31:53 +00:00
var didUpdate , didUpdateMacOSDiskEncryption bool
2023-03-06 14:54:51 +00:00
if payload . EnableDiskEncryption != nil {
if ac . MDM . MacOSSettings . EnableDiskEncryption != * payload . EnableDiskEncryption {
ac . MDM . MacOSSettings . EnableDiskEncryption = * payload . EnableDiskEncryption
didUpdate = true
2023-03-08 13:31:53 +00:00
didUpdateMacOSDiskEncryption = true
2023-03-06 14:54:51 +00:00
}
}
if didUpdate {
if err := svc . ds . SaveAppConfig ( ctx , ac ) ; err != nil {
return err
}
2023-03-08 13:31:53 +00:00
if didUpdateMacOSDiskEncryption {
var act fleet . ActivityDetails
if ac . MDM . MacOSSettings . EnableDiskEncryption {
act = fleet . ActivityTypeEnabledMacosDiskEncryption { }
if err := svc . EnterpriseOverrides . MDMAppleEnableFileVaultAndEscrow ( ctx , nil ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "enable no-team filevault and escrow" )
}
} else {
act = fleet . ActivityTypeDisabledMacosDiskEncryption { }
if err := svc . EnterpriseOverrides . MDMAppleDisableFileVaultAndEscrow ( ctx , nil ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "disable no-team filevault and escrow" )
}
}
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , act ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "create activity for app config macos disk encryption" )
}
}
2023-03-06 14:54:51 +00:00
}
return nil
}
2023-04-07 20:31:02 +00:00
////////////////////////////////////////////////////////////////////////////////
// Upload a bootstrap package
////////////////////////////////////////////////////////////////////////////////
type uploadBootstrapPackageRequest struct {
Package * multipart . FileHeader
TeamID uint
}
type uploadBootstrapPackageResponse struct {
Err error ` json:"error,omitempty" `
}
// TODO: We parse the whole body before running svc.authz.Authorize.
// An authenticated but unauthorized user could abuse this.
func ( uploadBootstrapPackageRequest ) DecodeRequest ( ctx context . Context , r * http . Request ) ( interface { } , error ) {
decoded := uploadBootstrapPackageRequest { }
err := r . ParseMultipartForm ( 512 * units . MiB )
if err != nil {
return nil , & fleet . BadRequestError {
Message : "failed to parse multipart form" ,
InternalErr : err ,
}
}
if r . MultipartForm . File [ "package" ] == nil {
return nil , & fleet . BadRequestError {
Message : "package multipart field is required" ,
InternalErr : err ,
}
}
decoded . Package = r . MultipartForm . File [ "package" ] [ 0 ]
// default is no team
decoded . TeamID = 0
val , ok := r . MultipartForm . Value [ "team_id" ]
if ok && len ( val ) > 0 {
teamID , err := strconv . Atoi ( val [ 0 ] )
if err != nil {
return nil , & fleet . BadRequestError { Message : fmt . Sprintf ( "failed to decode team_id in multipart form: %s" , err . Error ( ) ) }
}
decoded . TeamID = uint ( teamID )
}
return & decoded , nil
}
func ( r uploadBootstrapPackageResponse ) error ( ) error { return r . Err }
func uploadBootstrapPackageEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * uploadBootstrapPackageRequest )
ff , err := req . Package . Open ( )
if err != nil {
return uploadBootstrapPackageResponse { Err : err } , nil
}
defer ff . Close ( )
if err := svc . MDMAppleUploadBootstrapPackage ( ctx , req . Package . Filename , ff , req . TeamID ) ; err != nil {
return uploadBootstrapPackageResponse { Err : err } , nil
}
return & uploadBootstrapPackageResponse { } , nil
}
func ( svc * Service ) MDMAppleUploadBootstrapPackage ( ctx context . Context , name string , pkg io . Reader , teamID uint ) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Download a bootstrap package
////////////////////////////////////////////////////////////////////////////////
type downloadBootstrapPackageRequest struct {
Token string ` query:"token" `
}
type downloadBootstrapPackageResponse struct {
Err error ` json:"error,omitempty" `
// fields used by hijackRender for the response.
pkg * fleet . MDMAppleBootstrapPackage
}
func ( r downloadBootstrapPackageResponse ) error ( ) error { return r . Err }
func ( r downloadBootstrapPackageResponse ) hijackRender ( ctx context . Context , w http . ResponseWriter ) {
w . Header ( ) . Set ( "Content-Length" , strconv . Itoa ( len ( r . pkg . Bytes ) ) )
w . Header ( ) . Set ( "Content-Type" , "application/octet-stream" )
// OK to just log the error here as writing anything on
// `http.ResponseWriter` sets the status code to 200 (and it can't be
// changed.) Clients should rely on matching content-length with the
// header provided
if n , err := w . Write ( r . pkg . Bytes ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "bytes_copied" , n )
}
}
func downloadBootstrapPackageEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * downloadBootstrapPackageRequest )
pkg , err := svc . GetMDMAppleBootstrapPackageBytes ( ctx , req . Token )
if err != nil {
return downloadBootstrapPackageResponse { Err : err } , nil
}
return downloadBootstrapPackageResponse { pkg : pkg } , nil
}
func ( svc * Service ) GetMDMAppleBootstrapPackageBytes ( ctx context . Context , token string ) ( * fleet . MDMAppleBootstrapPackage , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Get metadata about a bootstrap package
////////////////////////////////////////////////////////////////////////////////
type bootstrapPackageMetadataRequest struct {
TeamID uint ` url:"team_id" `
}
type bootstrapPackageMetadataResponse struct {
2023-04-22 15:23:38 +00:00
Err error ` json:"error,omitempty" `
* fleet . MDMAppleBootstrapPackage ` json:",omitempty" `
2023-04-07 20:31:02 +00:00
}
func ( r bootstrapPackageMetadataResponse ) error ( ) error { return r . Err }
func bootstrapPackageMetadataEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * bootstrapPackageMetadataRequest )
meta , err := svc . GetMDMAppleBootstrapPackageMetadata ( ctx , req . TeamID )
if err != nil {
return bootstrapPackageMetadataResponse { Err : err } , nil
}
2023-04-22 15:23:38 +00:00
return bootstrapPackageMetadataResponse { MDMAppleBootstrapPackage : meta } , nil
2023-04-07 20:31:02 +00:00
}
func ( svc * Service ) GetMDMAppleBootstrapPackageMetadata ( ctx context . Context , teamID uint ) ( * fleet . MDMAppleBootstrapPackage , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Delete a bootstrap package
////////////////////////////////////////////////////////////////////////////////
type deleteBootstrapPackageRequest struct {
TeamID uint ` url:"team_id" `
}
type deleteBootstrapPackageResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deleteBootstrapPackageResponse ) error ( ) error { return r . Err }
func deleteBootstrapPackageEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * deleteBootstrapPackageRequest )
2023-04-26 21:09:21 +00:00
if err := svc . DeleteMDMAppleBootstrapPackage ( ctx , & req . TeamID ) ; err != nil {
2023-04-07 20:31:02 +00:00
return deleteBootstrapPackageResponse { Err : err } , nil
}
return deleteBootstrapPackageResponse { } , nil
}
2023-04-26 21:09:21 +00:00
func ( svc * Service ) DeleteMDMAppleBootstrapPackage ( ctx context . Context , teamID * uint ) error {
2023-04-07 20:31:02 +00:00
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
2023-04-22 15:23:38 +00:00
////////////////////////////////////////////////////////////////////////////////
// Get aggregated summary about a team's bootstrap package
////////////////////////////////////////////////////////////////////////////////
type getMDMAppleBootstrapPackageSummaryRequest struct {
TeamID * uint ` query:"team_id,optional" `
}
type getMDMAppleBootstrapPackageSummaryResponse struct {
fleet . MDMAppleBootstrapPackageSummary
Err error ` json:"error,omitempty" `
}
func ( r getMDMAppleBootstrapPackageSummaryResponse ) error ( ) error { return r . Err }
func getMDMAppleBootstrapPackageSummaryEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleBootstrapPackageSummaryRequest )
summary , err := svc . GetMDMAppleBootstrapPackageSummary ( ctx , req . TeamID )
if err != nil {
return getMDMAppleBootstrapPackageSummaryResponse { Err : err } , nil
}
return getMDMAppleBootstrapPackageSummaryResponse { MDMAppleBootstrapPackageSummary : * summary } , nil
}
func ( svc * Service ) GetMDMAppleBootstrapPackageSummary ( ctx context . Context , teamID * uint ) ( * fleet . MDMAppleBootstrapPackageSummary , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return & fleet . MDMAppleBootstrapPackageSummary { } , fleet . ErrMissingLicense
}
2023-04-25 13:36:01 +00:00
////////////////////////////////////////////////////////////////////////////////
// Create or update an MDM Apple Setup Assistant
////////////////////////////////////////////////////////////////////////////////
type createMDMAppleSetupAssistantRequest struct {
TeamID * uint ` json:"team_id" `
Name string ` json:"name" `
EnrollmentProfile json . RawMessage ` json:"enrollment_profile" `
}
type createMDMAppleSetupAssistantResponse struct {
fleet . MDMAppleSetupAssistant
Err error ` json:"error,omitempty" `
}
func ( r createMDMAppleSetupAssistantResponse ) error ( ) error { return r . Err }
func createMDMAppleSetupAssistantEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * createMDMAppleSetupAssistantRequest )
asst , err := svc . SetOrUpdateMDMAppleSetupAssistant ( ctx , & fleet . MDMAppleSetupAssistant {
TeamID : req . TeamID ,
Name : req . Name ,
Profile : req . EnrollmentProfile ,
} )
if err != nil {
return createMDMAppleSetupAssistantResponse { Err : err } , nil
}
return createMDMAppleSetupAssistantResponse { MDMAppleSetupAssistant : * asst } , nil
}
func ( svc * Service ) SetOrUpdateMDMAppleSetupAssistant ( ctx context . Context , asst * fleet . MDMAppleSetupAssistant ) ( * fleet . MDMAppleSetupAssistant , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Get the MDM Apple Setup Assistant
////////////////////////////////////////////////////////////////////////////////
type getMDMAppleSetupAssistantRequest struct {
TeamID * uint ` query:"team_id,optional" `
}
type getMDMAppleSetupAssistantResponse struct {
fleet . MDMAppleSetupAssistant
Err error ` json:"error,omitempty" `
}
func ( r getMDMAppleSetupAssistantResponse ) error ( ) error { return r . Err }
func getMDMAppleSetupAssistantEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleSetupAssistantRequest )
asst , err := svc . GetMDMAppleSetupAssistant ( ctx , req . TeamID )
if err != nil {
return getMDMAppleSetupAssistantResponse { Err : err } , nil
}
return getMDMAppleSetupAssistantResponse { MDMAppleSetupAssistant : * asst } , nil
}
func ( svc * Service ) GetMDMAppleSetupAssistant ( ctx context . Context , teamID * uint ) ( * fleet . MDMAppleSetupAssistant , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Delete an MDM Apple Setup Assistant
////////////////////////////////////////////////////////////////////////////////
type deleteMDMAppleSetupAssistantRequest struct {
TeamID * uint ` query:"team_id,optional" `
}
type deleteMDMAppleSetupAssistantResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r deleteMDMAppleSetupAssistantResponse ) error ( ) error { return r . Err }
func ( r deleteMDMAppleSetupAssistantResponse ) Status ( ) int { return http . StatusNoContent }
func deleteMDMAppleSetupAssistantEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * deleteMDMAppleSetupAssistantRequest )
if err := svc . DeleteMDMAppleSetupAssistant ( ctx , req . TeamID ) ; err != nil {
return deleteMDMAppleSetupAssistantResponse { Err : err } , nil
}
return deleteMDMAppleSetupAssistantResponse { } , nil
}
func ( svc * Service ) DeleteMDMAppleSetupAssistant ( ctx context . Context , teamID * uint ) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
2023-04-27 12:43:20 +00:00
////////////////////////////////////////////////////////////////////////////////
// POST /mdm/sso
////////////////////////////////////////////////////////////////////////////////
type initiateMDMAppleSSORequest struct { }
type initiateMDMAppleSSOResponse struct {
URL string ` json:"url,omitempty" `
Err error ` json:"error,omitempty" `
}
func ( r initiateMDMAppleSSOResponse ) error ( ) error { return r . Err }
func initiateMDMAppleSSOEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
idpProviderURL , err := svc . InitiateMDMAppleSSO ( ctx )
if err != nil {
return initiateMDMAppleSSOResponse { Err : err } , nil
}
return initiateMDMAppleSSOResponse { URL : idpProviderURL } , nil
}
func ( svc * Service ) InitiateMDMAppleSSO ( ctx context . Context ) ( string , error ) {
// skipauth: No authorization check needed due to implementation
// returning only license error.
svc . authz . SkipAuthorization ( ctx )
return "" , fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// POST /mdm/sso/callback
////////////////////////////////////////////////////////////////////////////////
type callbackMDMAppleSSORequest struct { }
func ( callbackMDMAppleSSORequest ) DecodeRequest ( ctx context . Context , r * http . Request ) ( interface { } , error ) {
err := r . ParseForm ( )
if err != nil {
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : "failed to parse form" ,
InternalErr : err ,
} , "decode sso callback" )
}
authResponse , err := sso . DecodeAuthResponse ( r . FormValue ( "SAMLResponse" ) )
if err != nil {
return nil , ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : "failed to decode SAMLResponse" ,
InternalErr : err ,
} , "decoding sso callback" )
}
return authResponse , nil
}
type callbackMDMAppleSSOResponse struct {
Err error ` json:"error,omitempty" `
// used in hijackRender for the response
profile [ ] byte
}
func ( r callbackMDMAppleSSOResponse ) error ( ) error { return r . Err }
func ( r callbackMDMAppleSSOResponse ) hijackRender ( ctx context . Context , w http . ResponseWriter ) {
w . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( int64 ( len ( r . profile ) ) , 10 ) )
w . Header ( ) . Set ( "Content-Type" , "application/x-apple-aspen-config" )
w . Header ( ) . Add ( "Content-Disposition" , ` attachment; filename="fleet-mdm-enrollment-profile.mobileconfig" ` )
w . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
// OK to just log the error here as writing anything on
// `http.ResponseWriter` sets the status code to 200 (and it can't be
// changed.) Clients should rely on matching content-length with the
// header provided.
if n , err := w . Write ( r . profile ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "written" , n )
}
}
func callbackMDMAppleSSOEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
auth := request . ( fleet . Auth )
// validate that the SSO response is valid
profile , err := svc . InitiateMDMAppleSSOCallback ( ctx , auth )
if err != nil {
return callbackMDMAppleSSOResponse { Err : err } , nil
}
return callbackMDMAppleSSOResponse { profile : profile } , nil
}
func ( svc * Service ) InitiateMDMAppleSSOCallback ( ctx context . Context , auth fleet . Auth ) ( [ ] byte , error ) {
// skipauth: No authorization check needed due to implementation
// returning only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , fleet . ErrMissingLicense
}
2023-03-01 13:43:15 +00:00
////////////////////////////////////////////////////////////////////////////////
// FileVault-related free version implementation
////////////////////////////////////////////////////////////////////////////////
2023-03-08 13:31:53 +00:00
func ( svc * Service ) MDMAppleEnableFileVaultAndEscrow ( ctx context . Context , teamID * uint ) error {
2023-03-01 13:43:15 +00:00
return fleet . ErrMissingLicense
}
2023-03-08 13:31:53 +00:00
func ( svc * Service ) MDMAppleDisableFileVaultAndEscrow ( ctx context . Context , teamID * uint ) error {
2023-03-01 13:43:15 +00:00
return fleet . ErrMissingLicense
}
2023-01-16 20:06:30 +00:00
////////////////////////////////////////////////////////////////////////////////
// Implementation of nanomdm's CheckinAndCommandService interface
////////////////////////////////////////////////////////////////////////////////
type MDMAppleCheckinAndCommandService struct {
2023-04-05 23:52:26 +00:00
ds fleet . Datastore
2023-04-07 20:31:02 +00:00
logger kitlog . Logger
2023-04-05 23:52:26 +00:00
commander * apple_mdm . MDMAppleCommander
2023-01-16 20:06:30 +00:00
}
2023-04-07 20:31:02 +00:00
func NewMDMAppleCheckinAndCommandService ( ds fleet . Datastore , commander * apple_mdm . MDMAppleCommander , logger kitlog . Logger ) * MDMAppleCheckinAndCommandService {
return & MDMAppleCheckinAndCommandService { ds : ds , commander : commander , logger : logger }
2023-01-16 20:06:30 +00:00
}
// Authenticate handles MDM [Authenticate][1] requests.
//
// This method is executed after the request has been handled by nanomdm, note
// that at this point you can't send any commands to the device yet because we
// haven't received a token, nor a PushMagic.
//
// We use it to perform post-enrollment tasks such as creating a host record,
// adding activities to the log, etc.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/authenticate
func ( svc * MDMAppleCheckinAndCommandService ) Authenticate ( r * mdm . Request , m * mdm . Authenticate ) error {
host := fleet . MDMAppleHostDetails { }
host . SerialNumber = m . SerialNumber
host . UDID = m . UDID
host . Model = m . Model
if err := svc . ds . IngestMDMAppleDeviceFromCheckin ( r . Context , host ) ; err != nil {
return err
}
info , err := svc . ds . GetHostMDMCheckinInfo ( r . Context , m . Enrollment . UDID )
if err != nil {
return err
}
return svc . ds . NewActivity ( r . Context , nil , & fleet . ActivityTypeMDMEnrolled {
HostSerial : info . HardwareSerial ,
2023-01-23 23:05:24 +00:00
HostDisplayName : info . DisplayName ,
2023-01-16 20:06:30 +00:00
InstalledFromDEP : info . InstalledFromDEP ,
} )
}
// TokenUpdate handles MDM [TokenUpdate][1] requests.
//
// This method is executed after the request has been handled by nanomdm.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/token_update
func ( svc * MDMAppleCheckinAndCommandService ) TokenUpdate ( r * mdm . Request , m * mdm . TokenUpdate ) error {
2023-03-27 18:43:01 +00:00
nanoEnroll , err := svc . ds . GetNanoMDMEnrollment ( r . Context , r . ID )
if err != nil {
return err
}
if nanoEnroll != nil && nanoEnroll . Enabled &&
nanoEnroll . Type == "Device" && nanoEnroll . TokenUpdateTally == 1 {
// device is enrolled for the first time, not a token update
if err := svc . ds . BulkSetPendingMDMAppleHostProfiles ( r . Context , nil , nil , nil , [ ] string { r . ID } ) ; err != nil {
return err
}
2023-04-05 23:52:26 +00:00
info , err := svc . ds . GetHostMDMCheckinInfo ( r . Context , m . Enrollment . UDID )
if err != nil {
return err
}
if info . InstalledFromDEP {
2023-04-07 20:31:02 +00:00
svc . logger . Log ( "info" , "running post-enroll commands in newly enrolled DEP device" , "host_uuid" , r . ID )
cmdUUID := uuid . New ( ) . String ( )
if err := svc . commander . InstallEnterpriseApplication ( r . Context , [ ] string { m . Enrollment . UDID } , cmdUUID , apple_mdm . FleetdPublicManifestURL ) ; err != nil {
return err
}
svc . logger . Log ( "info" , "sent command to install fleetd" , "host_uuid" , r . ID )
meta , err := svc . ds . GetMDMAppleBootstrapPackageMeta ( r . Context , info . TeamID )
if err != nil {
var nfe fleet . NotFoundError
if errors . As ( err , & nfe ) {
svc . logger . Log ( "info" , "unable to find a bootstrap package for DEP enrolled device, skppping installation" , "host_uuid" , r . ID )
return nil
}
return err
}
appCfg , err := svc . ds . AppConfig ( r . Context )
if err != nil {
return err
}
url , err := meta . URL ( appCfg . ServerSettings . ServerURL )
if err != nil {
return err
}
manifest := appmanifest . NewFromSha ( meta . Sha256 , url )
cmdUUID = uuid . New ( ) . String ( )
err = svc . commander . InstallEnterpriseApplicationWithEmbeddedManifest ( r . Context , [ ] string { m . Enrollment . UDID } , cmdUUID , manifest )
if err != nil {
2023-04-05 23:52:26 +00:00
return err
}
2023-04-22 15:23:38 +00:00
err = svc . ds . RecordHostBootstrapPackage ( r . Context , cmdUUID , r . ID )
if err != nil {
return err
}
2023-04-07 20:31:02 +00:00
svc . logger . Log ( "info" , "sent command to install bootstrap package" , "host_uuid" , r . ID )
2023-04-05 23:52:26 +00:00
}
2023-03-27 18:43:01 +00:00
}
2023-01-16 20:06:30 +00:00
return nil
}
// CheckOut handles MDM [CheckOut][1] requests.
//
// This method is executed after the request has been handled by nanomdm, note
// that this message is sent on a best-effort basis, don't rely exclusively on
// it.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/check_out
func ( svc * MDMAppleCheckinAndCommandService ) CheckOut ( r * mdm . Request , m * mdm . CheckOut ) error {
info , err := svc . ds . GetHostMDMCheckinInfo ( r . Context , m . Enrollment . UDID )
if err != nil {
return err
}
2023-04-08 01:02:17 +00:00
2023-01-16 20:06:30 +00:00
if err := svc . ds . UpdateHostTablesOnMDMUnenroll ( r . Context , m . UDID ) ; err != nil {
return err
}
return svc . ds . NewActivity ( r . Context , nil , & fleet . ActivityTypeMDMUnenrolled {
HostSerial : info . HardwareSerial ,
2023-01-23 23:05:24 +00:00
HostDisplayName : info . DisplayName ,
2023-01-16 20:06:30 +00:00
InstalledFromDEP : info . InstalledFromDEP ,
} )
}
// SetBootstrapToken handles MDM [SetBootstrapToken][1] requests.
//
// This method is executed after the request has been handled by nanomdm.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/set_bootstrap_token
func ( svc * MDMAppleCheckinAndCommandService ) SetBootstrapToken ( * mdm . Request , * mdm . SetBootstrapToken ) error {
return nil
}
// GetBootstrapToken handles MDM [GetBootstrapToken][1] requests.
//
// This method is executed after the request has been handled by nanomdm.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/get_bootstrap_token
func ( svc * MDMAppleCheckinAndCommandService ) GetBootstrapToken ( * mdm . Request , * mdm . GetBootstrapToken ) ( * mdm . BootstrapToken , error ) {
return nil , nil
}
// UserAuthenticate handles MDM [UserAuthenticate][1] requests.
//
// This method is executed after the request has been handled by nanomdm.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/userauthenticate
func ( svc * MDMAppleCheckinAndCommandService ) UserAuthenticate ( * mdm . Request , * mdm . UserAuthenticate ) ( [ ] byte , error ) {
return nil , nil
}
// DeclarativeManagement handles MDM [DeclarativeManagement][1] requests.
//
// This method is executed after the request has been handled by nanomdm.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/declarative_management_checkin
func ( svc * MDMAppleCheckinAndCommandService ) DeclarativeManagement ( * mdm . Request , * mdm . DeclarativeManagement ) ( [ ] byte , error ) {
return nil , nil
}
// CommandAndReportResults handles MDM [Commands and Queries][1].
//
// This method is executed after the request has been handled by nanomdm.
//
// [1]: https://developer.apple.com/documentation/devicemanagement/commands_and_queries
2023-02-22 17:49:06 +00:00
func ( svc * MDMAppleCheckinAndCommandService ) CommandAndReportResults ( r * mdm . Request , res * mdm . CommandResults ) ( * mdm . Command , error ) {
// Sometimes we get results with Status == "Idle" which don't contain a command
// UUID and are not actionable anyways.
if res . CommandUUID == "" {
return nil , nil
}
// We explicitly get the request type because it comes empty. There's a
// RequestType field in the struct, but it's used when a mdm.Command is
// issued.
requestType , err := svc . ds . GetMDMAppleCommandRequestType ( r . Context , res . CommandUUID )
if err != nil {
return nil , ctxerr . Wrap ( r . Context , err , "command service" )
}
switch requestType {
case "InstallProfile" :
2023-03-08 20:42:23 +00:00
return nil , svc . ds . UpdateOrDeleteHostMDMAppleProfile ( r . Context , & fleet . HostMDMAppleProfile {
2023-02-22 17:49:06 +00:00
CommandUUID : res . CommandUUID ,
HostUUID : res . UDID ,
Status : fleet . MDMAppleDeliveryStatusFromCommandStatus ( res . Status ) ,
2023-04-22 15:23:38 +00:00
Detail : apple_mdm . FmtErrorChain ( res . ErrorChain ) ,
2023-02-22 17:49:06 +00:00
OperationType : fleet . MDMAppleOperationTypeInstall ,
} )
case "RemoveProfile" :
2023-03-08 20:42:23 +00:00
return nil , svc . ds . UpdateOrDeleteHostMDMAppleProfile ( r . Context , & fleet . HostMDMAppleProfile {
2023-02-22 17:49:06 +00:00
CommandUUID : res . CommandUUID ,
HostUUID : res . UDID ,
Status : fleet . MDMAppleDeliveryStatusFromCommandStatus ( res . Status ) ,
2023-04-22 15:23:38 +00:00
Detail : apple_mdm . FmtErrorChain ( res . ErrorChain ) ,
2023-02-22 17:49:06 +00:00
OperationType : fleet . MDMAppleOperationTypeRemove ,
} )
}
2023-01-16 20:06:30 +00:00
return nil , nil
}
2023-02-17 19:26:51 +00:00
2023-04-04 20:09:20 +00:00
// ensureFleetdConfig ensures there's a fleetd configuration profile in
// mdm_apple_configuration_profiles for each team and for "no team"
//
// We try our best to use each team's secret but we default to creating a
// profile with the global enroll secret if the team doesn't have any enroll
// secrets.
//
2023-04-24 21:27:15 +00:00
// This profile will be installed to all hosts in the team (or "no team",) but it
2023-04-04 20:09:20 +00:00
// will only be used by hosts that have a fleetd installation without an enroll
// secret and fleet URL (mainly DEP enrolled hosts).
func ensureFleetdConfig ( ctx context . Context , ds fleet . Datastore , logger kitlog . Logger ) error {
appCfg , err := ds . AppConfig ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "fetching app config" )
}
enrollSecrets , err := ds . AggregateEnrollSecretPerTeam ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting enroll secrets aggregates" )
}
globalSecret := ""
for _ , es := range enrollSecrets {
if es . TeamID == nil {
globalSecret = es . Secret
}
}
var profiles [ ] * fleet . MDMAppleConfigProfile
for _ , es := range enrollSecrets {
if es . Secret == "" {
if globalSecret == "" {
logger . Log ( "err" , "team_id %d doesn't have an enroll secret, and couldn't find a global enroll secret, skipping the creation of a com.fleetdm.fleetd.config profile" )
continue
}
logger . Log ( "err" , "team_id %d doesn't have an enroll secret, using a global enroll secret" )
es . Secret = globalSecret
}
var contents bytes . Buffer
params := mobileconfig . FleetdProfileOptions {
EnrollSecret : es . Secret ,
ServerURL : appCfg . ServerSettings . ServerURL ,
PayloadType : mobileconfig . FleetdConfigPayloadIdentifier ,
}
if err := mobileconfig . FleetdProfileTemplate . Execute ( & contents , params ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "executing fleetd config template" )
}
cp , err := fleet . NewMDMAppleConfigProfile ( contents . Bytes ( ) , es . TeamID )
if err != nil {
return ctxerr . Wrap ( ctx , err , "building configuration profile" )
}
profiles = append ( profiles , cp )
}
if err := ds . BulkUpsertMDMAppleConfigProfiles ( ctx , profiles ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "bulk-upserting configuration profiles" )
}
return nil
}
2023-02-22 17:49:06 +00:00
func ReconcileProfiles (
ctx context . Context ,
ds fleet . Datastore ,
2023-04-05 14:50:36 +00:00
commander * apple_mdm . MDMAppleCommander ,
2023-02-22 17:49:06 +00:00
logger kitlog . Logger ,
) error {
2023-04-04 20:09:20 +00:00
if err := ensureFleetdConfig ( ctx , ds , logger ) ; err != nil {
logger . Log ( "err" , "unable to ensure a fleetd configuration profiles are in place" , "details" , err )
}
2023-02-22 17:49:06 +00:00
// retrieve the profiles to install/remove.
toInstall , err := ds . ListMDMAppleProfilesToInstall ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting profiles to install" )
}
toRemove , err := ds . ListMDMAppleProfilesToRemove ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting profiles to remove" )
}
// Perform aggregations to support all the operations we need to do
2023-03-27 18:43:01 +00:00
// toGetContents contains the IDs of all the profiles from which we
2023-02-22 17:49:06 +00:00
// need to retrieve contents. Since the previous query returns one row
// per host, it would be too expensive to retrieve the profile contents
2023-03-27 18:43:01 +00:00
// there, so we make another request. Using a map to deduplicate.
toGetContents := make ( map [ uint ] bool )
2023-02-22 17:49:06 +00:00
// hostProfiles tracks each host_mdm_apple_profile we need to upsert
// with the new status, operation_type, etc.
2023-03-27 18:43:01 +00:00
hostProfiles := make ( [ ] * fleet . MDMAppleBulkUpsertHostProfilePayload , 0 , len ( toInstall ) + len ( toRemove ) )
// install/removeTargets are maps from profileID -> command uuid and host
// UUIDs as the underlying MDM services are optimized to send one command to
// multiple hosts at the same time. Note that the same command uuid is used
// for all hosts in a given install/remove target operation.
type cmdTarget struct {
cmdUUID string
profIdent string
hostUUIDs [ ] string
}
installTargets , removeTargets := make ( map [ uint ] * cmdTarget ) , make ( map [ uint ] * cmdTarget )
2023-02-22 17:49:06 +00:00
for _ , p := range toInstall {
2023-03-27 18:43:01 +00:00
toGetContents [ p . ProfileID ] = true
target := installTargets [ p . ProfileID ]
if target == nil {
target = & cmdTarget {
cmdUUID : uuid . New ( ) . String ( ) ,
profIdent : p . ProfileIdentifier ,
}
installTargets [ p . ProfileID ] = target
}
target . hostUUIDs = append ( target . hostUUIDs , p . HostUUID )
hostProfiles = append ( hostProfiles , & fleet . MDMAppleBulkUpsertHostProfilePayload {
ProfileID : p . ProfileID ,
HostUUID : p . HostUUID ,
OperationType : fleet . MDMAppleOperationTypeInstall ,
Status : & fleet . MDMAppleDeliveryPending ,
CommandUUID : target . cmdUUID ,
ProfileIdentifier : p . ProfileIdentifier ,
ProfileName : p . ProfileName ,
2023-04-09 02:23:36 +00:00
Checksum : p . Checksum ,
2023-03-27 18:43:01 +00:00
} )
2023-02-22 17:49:06 +00:00
}
for _ , p := range toRemove {
2023-03-27 18:43:01 +00:00
target := removeTargets [ p . ProfileID ]
if target == nil {
target = & cmdTarget {
cmdUUID : uuid . New ( ) . String ( ) ,
profIdent : p . ProfileIdentifier ,
}
removeTargets [ p . ProfileID ] = target
}
target . hostUUIDs = append ( target . hostUUIDs , p . HostUUID )
hostProfiles = append ( hostProfiles , & fleet . MDMAppleBulkUpsertHostProfilePayload {
ProfileID : p . ProfileID ,
HostUUID : p . HostUUID ,
OperationType : fleet . MDMAppleOperationTypeRemove ,
Status : & fleet . MDMAppleDeliveryPending ,
CommandUUID : target . cmdUUID ,
ProfileIdentifier : p . ProfileIdentifier ,
ProfileName : p . ProfileName ,
2023-04-09 02:23:36 +00:00
Checksum : p . Checksum ,
2023-03-27 18:43:01 +00:00
} )
2023-02-22 17:49:06 +00:00
}
// First update all the profiles in the database before sending the
// commands, this prevents race conditions where we could get a
// response from the device before we set its status as 'pending'
//
// We'll do another pass at the end to revert any changes for failed
// delivieries.
2023-03-27 18:43:01 +00:00
if err := ds . BulkUpsertMDMAppleHostProfiles ( ctx , hostProfiles ) ; err != nil {
2023-02-22 17:49:06 +00:00
return ctxerr . Wrap ( ctx , err , "updating host profiles" )
}
// Grab the contents of all the profiles we need to install
2023-03-27 18:43:01 +00:00
profileIDs := make ( [ ] uint , 0 , len ( toGetContents ) )
for pid := range toGetContents {
profileIDs = append ( profileIDs , pid )
}
profileContents , err := ds . GetMDMAppleProfilesContents ( ctx , profileIDs )
2023-02-22 17:49:06 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "get profile contents" )
}
2023-03-27 18:43:01 +00:00
type remoteResult struct {
Err error
CmdUUID string
}
2023-02-22 17:49:06 +00:00
// Send the install/remove commands for each profile.
2023-03-27 18:43:01 +00:00
var wgProd , wgCons sync . WaitGroup
2023-02-22 17:49:06 +00:00
ch := make ( chan remoteResult )
2023-03-27 18:43:01 +00:00
execCmd := func ( profID uint , target * cmdTarget , op fleet . MDMAppleOperationType ) {
defer wgProd . Done ( )
var err error
switch op {
case fleet . MDMAppleOperationTypeInstall :
err = commander . InstallProfile ( ctx , target . hostUUIDs , profileContents [ profID ] , target . cmdUUID )
case fleet . MDMAppleOperationTypeRemove :
err = commander . RemoveProfile ( ctx , target . hostUUIDs , target . profIdent , target . cmdUUID )
}
2023-04-05 14:50:36 +00:00
var e * apple_mdm . APNSDeliveryError
2023-03-27 18:43:01 +00:00
switch {
case errors . As ( err , & e ) :
level . Debug ( logger ) . Log ( "err" , "sending push notifications, profiles still enqueued" , "details" , err )
case err != nil :
level . Error ( logger ) . Log ( "err" , fmt . Sprintf ( "enqueue command to %s profiles" , op ) , "details" , err )
ch <- remoteResult { err , target . cmdUUID }
}
}
for profID , target := range installTargets {
wgProd . Add ( 1 )
go execCmd ( profID , target , fleet . MDMAppleOperationTypeInstall )
}
for profID , target := range removeTargets {
wgProd . Add ( 1 )
go execCmd ( profID , target , fleet . MDMAppleOperationTypeRemove )
}
2023-02-22 17:49:06 +00:00
2023-03-27 18:43:01 +00:00
// index the host profiles by cmdUUID, for ease of error processing in the
// consumer goroutine below.
hostProfsByCmdUUID := make ( map [ string ] [ ] * fleet . MDMAppleBulkUpsertHostProfilePayload , len ( installTargets ) + len ( removeTargets ) )
for _ , hp := range hostProfiles {
hostProfsByCmdUUID [ hp . CommandUUID ] = append ( hostProfsByCmdUUID [ hp . CommandUUID ] , hp )
2023-02-22 17:49:06 +00:00
}
2023-03-27 18:43:01 +00:00
// Grab all the failed deliveries and update the status so they're picked up
// again in the next run.
2023-02-22 17:49:06 +00:00
//
2023-03-27 18:43:01 +00:00
// Note that if the APNs push failed we won't try again, as the command was
// successfully enqueued, this is only to account for internal errors like DB
// failures.
2023-02-22 17:49:06 +00:00
failed := [ ] * fleet . MDMAppleBulkUpsertHostProfilePayload { }
2023-03-27 18:43:01 +00:00
wgCons . Add ( 1 )
go func ( ) {
defer wgCons . Done ( )
for resp := range ch {
hostProfs := hostProfsByCmdUUID [ resp . CmdUUID ]
for _ , hp := range hostProfs {
// clear the command as it failed to enqueue, will need to emit a new command
hp . CommandUUID = ""
// set status to nil so it is retried on the next cron run
hp . Status = nil
failed = append ( failed , hp )
2023-02-22 17:49:06 +00:00
}
}
2023-03-27 18:43:01 +00:00
} ( )
wgProd . Wait ( )
close ( ch ) // done sending at this point, this triggers end of for loop in consumer
wgCons . Wait ( )
if err := ds . BulkUpsertMDMAppleHostProfiles ( ctx , failed ) ; err != nil {
2023-02-22 17:49:06 +00:00
return ctxerr . Wrap ( ctx , err , "reverting status of failed profiles" )
}
return nil
}