2022-10-05 22:53:54 +00:00
package service
import (
"bytes"
"context"
"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"
2023-05-18 15:50:00 +00:00
"net/url"
2022-10-05 22:53:54 +00:00
"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
"github.com/docker/go-units"
2023-05-11 13:36:28 +00:00
"github.com/fleetdm/fleet/v4/pkg/file"
2023-10-06 22:04:33 +00:00
"github.com/fleetdm/fleet/v4/pkg/optjson"
2023-08-24 18:17:05 +00:00
"github.com/fleetdm/fleet/v4/server"
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"
2023-06-05 15:58:23 +00:00
"github.com/fleetdm/fleet/v4/server/worker"
2022-10-05 22:53:54 +00:00
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/google/uuid"
"github.com/groob/plist"
"github.com/micromdm/nanodep/godep"
"github.com/micromdm/nanomdm/mdm"
)
type getMDMAppleCommandResultsRequest struct {
CommandUUID string ` query:"command_uuid,optional" `
}
type getMDMAppleCommandResultsResponse struct {
2023-11-01 14:13:12 +00:00
Results [ ] * fleet . MDMCommandResult ` 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-11-01 14:13:12 +00:00
func ( svc * Service ) GetMDMAppleCommandResults ( ctx context . Context , commandUUID string ) ( [ ] * fleet . MDMCommandResult , error ) {
2023-04-05 14:50:36 +00:00
// 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 {
2023-11-01 14:13:12 +00:00
hostUUIDs [ i ] = res . HostUUID
2023-04-05 14:50:36 +00:00
}
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-11-01 14:13:12 +00:00
var commandAuthz fleet . MDMCommandAuthz
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 {
2023-11-01 14:13:12 +00:00
if h := hostsByUUID [ res . HostUUID ] ; h != nil {
res . Hostname = hostsByUUID [ res . HostUUID ] . Hostname
2023-04-05 14:50:36 +00:00
}
}
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 )
2023-11-01 14:13:12 +00:00
results , err := svc . ListMDMAppleCommands ( ctx , & fleet . MDMCommandListOptions {
2023-04-17 15:45:16 +00:00
ListOptions : req . ListOptions ,
} )
if err != nil {
return listMDMAppleCommandsResponse {
Err : err ,
} , nil
}
return listMDMAppleCommandsResponse {
Results : results ,
} , nil
}
2023-11-01 14:13:12 +00:00
func ( svc * Service ) ListMDMAppleCommands ( ctx context . Context , opts * fleet . MDMCommandListOptions ) ( [ ] * fleet . MDMAppleCommand , error ) {
2023-04-17 15:45:16 +00:00
// 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.
2023-11-01 14:13:12 +00:00
var commandAuthz fleet . MDMCommandAuthz
2023-04-17 15:45:16 +00:00
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 ( )
2023-11-15 15:58:59 +00:00
cp , err := svc . NewMDMAppleConfigProfile ( ctx , req . TeamID , ff )
2023-02-17 15:28:28 +00:00
if err != nil {
return & newMDMAppleConfigProfileResponse { Err : err } , nil
}
return & newMDMAppleConfigProfileResponse {
ProfileID : cp . ProfileID ,
} , nil
}
2023-11-15 15:58:59 +00:00
func ( svc * Service ) NewMDMAppleConfigProfile ( ctx context . Context , teamID uint , r io . Reader ) ( * fleet . MDMAppleConfigProfile , error ) {
2023-11-08 16:36:57 +00:00
if err := svc . authz . Authorize ( ctx , & fleet . MDMConfigProfileAuthz { TeamID : & teamID } , fleet . ActionWrite ) ; err != nil {
2023-02-17 15:28:28 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
2023-11-15 15:58:59 +00:00
// check that Apple MDM is enabled - the middleware of that endpoint checks
// only that any MDM is enabled, maybe it's just Windows
if err := svc . VerifyMDMAppleConfigured ( ctx ) ; err != nil {
err := fleet . NewInvalidArgumentError ( "profile" , fleet . AppleMDMNotConfiguredMessage ) . WithStatus ( http . StatusBadRequest )
return nil , ctxerr . Wrap ( ctx , err , "check macOS MDM enabled" )
}
2023-02-17 15:28:28 +00:00
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
}
2023-11-15 15:58:59 +00:00
b , err := io . ReadAll ( r )
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 {
2023-11-15 15:58:59 +00:00
Message : "failed to read Apple config profile" ,
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
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 {
2023-11-15 15:58:59 +00:00
var existsErr existsErrorInterface
if errors . As ( err , & existsErr ) {
err = fleet . NewInvalidArgumentError ( "profile" , "Couldn't upload. A configuration profile with this name already exists." ) .
WithStatus ( http . StatusConflict )
}
2023-02-17 15:28:28 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
2023-11-20 14:16:02 +00:00
if err := svc . ds . BulkSetPendingMDMHostProfiles ( ctx , nil , nil , [ ] uint { newCP . ProfileID } , nil , nil ) ; err != nil {
2023-03-27 18:43:01 +00:00
return nil , ctxerr . Wrap ( ctx , err , "bulk set pending host profiles" )
}
2023-02-17 15:28:28 +00:00
2023-11-15 15:58:59 +00:00
var (
actTeamID * uint
actTeamName * string
)
if teamID > 0 {
actTeamID = & teamID
actTeamName = & teamName
}
2023-02-17 15:28:28 +00:00
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , & fleet . ActivityTypeCreatedMacosProfile {
2023-11-15 15:58:59 +00:00
TeamID : actTeamID ,
TeamName : actTeamName ,
2023-03-17 21:16:18 +00:00
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 )
cps , err := svc . ListMDMAppleConfigProfiles ( ctx , req . TeamID )
if err != nil {
2023-05-24 23:56:39 +00:00
return & listMDMAppleConfigProfilesResponse { Err : err } , nil
2023-02-17 15:28:28 +00:00
}
2023-05-24 23:56:39 +00:00
res := listMDMAppleConfigProfilesResponse { ConfigProfiles : cps }
if cps == nil {
res . ConfigProfiles = [ ] * fleet . MDMAppleConfigProfile { } // return empty json array instead of json null
}
2023-02-17 15:28:28 +00:00
return & res , nil
}
func ( svc * Service ) ListMDMAppleConfigProfiles ( ctx context . Context , teamID uint ) ( [ ] * fleet . MDMAppleConfigProfile , error ) {
2023-11-08 16:36:57 +00:00
if err := svc . authz . Authorize ( ctx , & fleet . MDMConfigProfileAuthz { TeamID : & teamID } , fleet . ActionRead ) ; err != nil {
2023-02-17 15:28:28 +00:00
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
2023-11-08 16:36:57 +00:00
if err := svc . authz . Authorize ( ctx , & fleet . MDMConfigProfileAuthz { TeamID : cp . TeamID } , fleet . ActionRead ) ; err != nil {
2023-02-17 15:28:28 +00:00
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 )
}
2023-11-09 17:59:14 +00:00
// check that Apple MDM is enabled - the middleware of that endpoint checks
// only that any MDM is enabled, maybe it's just Windows
if err := svc . VerifyMDMAppleConfigured ( ctx ) ; err != nil {
err := fleet . NewInvalidArgumentError ( "profile_id" , fleet . AppleMDMNotConfiguredMessage ) . WithStatus ( http . StatusBadRequest )
return ctxerr . Wrap ( ctx , err , "check macOS MDM enabled" )
}
2023-02-17 15:28:28 +00:00
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
2023-11-08 16:36:57 +00:00
if err := svc . authz . Authorize ( ctx , & fleet . MDMConfigProfileAuthz { TeamID : cp . TeamID } , fleet . ActionWrite ) ; err != nil {
2023-02-17 15:28:28 +00:00
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
2023-11-20 14:16:02 +00:00
if err := svc . ds . BulkSetPendingMDMHostProfiles ( ctx , nil , [ ] uint { teamID } , nil , nil , nil ) ; err != nil {
2023-03-27 18:43:01 +00:00
return ctxerr . Wrap ( ctx , err , "bulk set pending host profiles" )
}
2023-02-17 15:28:28 +00:00
2023-11-09 17:59:14 +00:00
var (
actTeamID * uint
actTeamName * string
)
if teamID > 0 {
actTeamID = & teamID
actTeamName = & teamName
}
2023-02-17 15:28:28 +00:00
if err := svc . ds . NewActivity ( ctx , authz . UserFromContext ( ctx ) , & fleet . ActivityTypeDeletedMacosProfile {
2023-11-09 17:59:14 +00:00
TeamID : actTeamID ,
TeamName : actTeamName ,
2023-03-17 21:16:18 +00:00
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-11-17 16:49:30 +00:00
type getMDMAppleFileVaultSummaryRequest struct {
2023-03-02 00:36:59 +00:00
TeamID * uint ` query:"team_id,optional" `
}
2023-11-17 16:49:30 +00:00
type getMDMAppleFileVaultSummaryResponse struct {
* fleet . MDMAppleFileVaultSummary
2023-03-02 00:36:59 +00:00
Err error ` json:"error,omitempty" `
}
2023-11-17 16:49:30 +00:00
func ( r getMDMAppleFileVaultSummaryResponse ) error ( ) error { return r . Err }
2023-03-02 00:36:59 +00:00
2023-11-17 16:49:30 +00:00
func getMdmAppleFileVaultSummaryEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleFileVaultSummaryRequest )
2023-03-02 00:36:59 +00:00
2023-11-17 16:49:30 +00:00
fvs , err := svc . GetMDMAppleFileVaultSummary ( ctx , req . TeamID )
2023-03-02 00:36:59 +00:00
if err != nil {
2023-11-17 16:49:30 +00:00
return & getMDMAppleFileVaultSummaryResponse { Err : err } , nil
2023-03-02 00:36:59 +00:00
}
2023-11-17 16:49:30 +00:00
return & getMDMAppleFileVaultSummaryResponse {
MDMAppleFileVaultSummary : fvs ,
} , nil
2023-03-02 00:36:59 +00:00
}
2023-11-17 16:49:30 +00:00
func ( svc * Service ) GetMDMAppleFileVaultSummary ( ctx context . Context , teamID * uint ) ( * fleet . MDMAppleFileVaultSummary , error ) {
2023-11-08 16:36:57 +00:00
if err := svc . authz . Authorize ( ctx , fleet . MDMConfigProfileAuthz { TeamID : teamID } , fleet . ActionRead ) ; err != nil {
2023-03-02 00:36:59 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
2023-11-17 16:49:30 +00:00
fvs , err := svc . ds . GetMDMAppleFileVaultSummary ( ctx , teamID )
2023-03-02 00:36:59 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
2023-11-17 16:49:30 +00:00
return fvs , nil
2023-03-02 00:36:59 +00:00
}
2023-11-17 16:49:30 +00:00
type getMDMAppleProfilesSummaryRequest struct {
2023-03-28 14:50:14 +00:00
TeamID * uint ` query:"team_id,optional" `
}
2023-11-17 16:49:30 +00:00
type getMDMAppleProfilesSummaryResponse struct {
fleet . MDMProfilesSummary
2023-03-28 14:50:14 +00:00
Err error ` json:"error,omitempty" `
}
2023-11-17 16:49:30 +00:00
func ( r getMDMAppleProfilesSummaryResponse ) error ( ) error { return r . Err }
2023-03-28 14:50:14 +00:00
2023-11-17 16:49:30 +00:00
func getMDMAppleProfilesSummaryEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getMDMAppleProfilesSummaryRequest )
res := getMDMAppleProfilesSummaryResponse { }
2023-03-28 14:50:14 +00:00
2023-11-17 16:49:30 +00:00
ps , err := svc . GetMDMAppleProfilesSummary ( ctx , req . TeamID )
2023-03-28 14:50:14 +00:00
if err != nil {
2023-11-17 16:49:30 +00:00
return & getMDMAppleProfilesSummaryResponse { Err : err } , nil
2023-03-28 14:50:14 +00:00
}
2023-11-17 16:49:30 +00:00
res . Verified = ps . Verified
res . Verifying = ps . Verifying
res . Failed = ps . Failed
res . Pending = ps . Pending
return & res , nil
2023-03-28 14:50:14 +00:00
}
2023-11-17 16:49:30 +00:00
func ( svc * Service ) GetMDMAppleProfilesSummary ( ctx context . Context , teamID * uint ) ( * fleet . MDMProfilesSummary , error ) {
2023-11-08 16:36:57 +00:00
if err := svc . authz . Authorize ( ctx , fleet . MDMConfigProfileAuthz { TeamID : teamID } , fleet . ActionRead ) ; err != nil {
2023-03-28 14:50:14 +00:00
return nil , ctxerr . Wrap ( ctx , err )
}
2023-11-17 16:49:30 +00:00
if err := svc . VerifyMDMAppleConfigured ( ctx ) ; err != nil {
return & fleet . MDMProfilesSummary { } , nil
}
ps , err := svc . ds . GetMDMAppleProfilesSummary ( ctx , teamID )
2023-03-28 14:50:14 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
2023-11-17 16:49:30 +00:00
return ps , nil
2023-03-28 14:50:14 +00:00
}
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
2023-11-01 14:13:12 +00:00
Err error ` json:"error,omitempty" `
2022-10-05 22:53:54 +00:00
}
func ( r enqueueMDMAppleCommandResponse ) error ( ) error { return r . Err }
2023-11-01 14:13:12 +00:00
// Deprecated: enqueueMDMAppleCommandEndpoint is now deprecated, replaced by
// the platform-agnostic runMDMCommandEndpoint. It is still supported
// indefinitely for backwards compatibility.
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-11-01 14:13:12 +00:00
result , err := svc . EnqueueMDMAppleCommand ( ctx , req . Command , req . DeviceIDs )
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
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 ,
2023-11-01 14:13:12 +00:00
) ( result * fleet . CommandEnqueueResult , err error ) {
hosts , err := svc . authorizeAllHostsTeams ( ctx , deviceIDs , fleet . ActionWrite , & fleet . MDMCommandAuthz { } )
2023-04-03 18:25:49 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return nil , err
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
if len ( hosts ) == 0 {
2023-11-01 14:13:12 +00:00
return nil , newNotFoundError ( )
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
2023-08-24 18:17:05 +00:00
// using a padding agnostic decoder because we released this using
// base64.RawStdEncoding, but it was causing problems as many standard
// libraries default to padded strings. We're now supporting both for
// backwards compatibility.
rawXMLCmd , err := server . Base64DecodePaddingAgnostic ( rawBase64Cmd )
2023-04-03 18:25:49 +00:00
if err != nil {
2023-05-03 15:56:25 +00:00
err = fleet . NewInvalidArgumentError ( "command" , "unable to decode base64 command" ) . WithStatus ( http . StatusBadRequest )
2023-11-01 14:13:12 +00:00
return nil , ctxerr . Wrap ( ctx , err , "decode base64 command" )
2022-10-05 22:53:54 +00:00
}
2023-04-03 18:25:49 +00:00
2023-11-01 14:13:12 +00:00
return svc . enqueueAppleMDMCommand ( ctx , rawXMLCmd , deviceIDs )
2022-10-05 22:53:54 +00:00
}
type mdmAppleEnrollRequest struct {
2023-05-18 15:50:00 +00:00
Token string ` query:"token" `
EnrollmentReference string ` query:"enrollment_reference,optional" `
2022-10-05 22:53:54 +00:00
}
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" )
2023-05-05 17:36:13 +00:00
w . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
w . Header ( ) . Set ( "Content-Disposition" , "attachment;fleet-enrollment-profile.mobileconfig" )
2022-10-05 22:53:54 +00:00
// 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 )
2023-05-18 15:50:00 +00:00
profile , err := svc . GetMDMAppleEnrollmentProfileByToken ( ctx , req . Token , req . EnrollmentReference )
2022-10-05 22:53:54 +00:00
if err != nil {
return mdmAppleEnrollResponse { Err : err } , nil
}
return mdmAppleEnrollResponse {
Profile : profile ,
} , nil
}
2023-05-18 15:50:00 +00:00
func ( svc * Service ) GetMDMAppleEnrollmentProfileByToken ( ctx context . Context , token string , ref string ) ( profile [ ] byte , err error ) {
2022-10-05 22:53:54 +00:00
// 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 )
}
2023-05-18 15:50:00 +00:00
enrollURL := appConfig . ServerSettings . ServerURL
if ref != "" {
u , err := url . Parse ( enrollURL )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "parsing configured server URL" )
}
q := u . Query ( )
q . Add ( "enroll_reference" , ref )
u . RawQuery = q . Encode ( )
enrollURL = u . String ( )
}
2023-03-13 13:33:32 +00:00
mobileconfig , err := apple_mdm . GenerateEnrollmentProfileMobileconfig (
2022-10-05 22:53:54 +00:00
appConfig . OrgInfo . OrgName ,
2023-05-18 15:50:00 +00:00
enrollURL ,
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.
2023-11-01 14:13:12 +00:00
if err := svc . authz . Authorize ( ctx , fleet . MDMCommandAuthz {
2023-04-18 10:53:33 +00:00
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-07-14 15:53:03 +00:00
////////////////////////////////////////////////////////////////////////////////
// Get profiles assigned to a host
////////////////////////////////////////////////////////////////////////////////
type getHostProfilesRequest struct {
ID uint ` url:"id" `
}
type getHostProfilesResponse struct {
HostID uint ` json:"host_id" `
Profiles [ ] * fleet . MDMAppleConfigProfile ` json:"profiles" `
Err error ` json:"error,omitempty" `
}
func ( r getHostProfilesResponse ) error ( ) error { return r . Err }
func getHostProfilesEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * getHostProfilesRequest )
sums , err := svc . MDMListHostConfigurationProfiles ( ctx , req . ID )
if err != nil {
return getHostProfilesResponse { Err : err } , nil
}
res := getHostProfilesResponse { Profiles : sums , HostID : req . ID }
if res . Profiles == nil {
res . Profiles = [ ] * fleet . MDMAppleConfigProfile { } // return empty json array instead of json null
}
return res , nil
}
func ( svc * Service ) MDMListHostConfigurationProfiles ( ctx context . Context , hostID uint ) ( [ ] * fleet . MDMAppleConfigProfile , error ) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return nil , 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 )
2023-10-13 11:49:11 +00:00
if err := svc . BatchSetMDMAppleProfiles ( ctx , req . TeamID , req . TeamName , req . Profiles , req . DryRun , false ) ; err != nil {
2023-02-15 18:01:44 +00:00
return batchSetMDMAppleProfilesResponse { Err : err } , nil
}
return batchSetMDMAppleProfilesResponse { } , nil
}
2023-10-13 11:49:11 +00:00
func ( svc * Service ) BatchSetMDMAppleProfiles ( ctx context . Context , tmID * uint , tmName * string , profiles [ ] [ ] byte , dryRun , skipBulkPending bool ) error {
2023-11-15 12:37:19 +00:00
var err error
tmID , tmName , err = svc . authorizeBatchProfiles ( ctx , tmID , tmName )
if err != nil {
return err
2023-02-15 18:01:44 +00:00
}
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
}
2023-10-13 11:49:11 +00:00
if ! skipBulkPending {
2023-11-20 14:16:02 +00:00
if err := svc . ds . BulkSetPendingMDMHostProfiles ( ctx , nil , [ ] uint { bulkTeamID } , nil , nil , nil ) ; err != nil {
2023-10-13 11:49:11 +00:00
return ctxerr . Wrap ( ctx , err , "bulk set pending host profiles" )
}
2023-03-27 18:43:01 +00:00
}
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-05-31 13:24:22 +00:00
////////////////////////////////////////////////////////////////////////////////
// Preassign a profile to a host
////////////////////////////////////////////////////////////////////////////////
type preassignMDMAppleProfileRequest struct {
fleet . MDMApplePreassignProfilePayload
}
type preassignMDMAppleProfileResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r preassignMDMAppleProfileResponse ) error ( ) error { return r . Err }
func ( r preassignMDMAppleProfileResponse ) Status ( ) int { return http . StatusNoContent }
func preassignMDMAppleProfileEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * preassignMDMAppleProfileRequest )
if err := svc . MDMApplePreassignProfile ( ctx , req . MDMApplePreassignProfilePayload ) ; err != nil {
return preassignMDMAppleProfileResponse { Err : err } , nil
}
return preassignMDMAppleProfileResponse { } , nil
}
func ( svc * Service ) MDMApplePreassignProfile ( ctx context . Context , payload fleet . MDMApplePreassignProfilePayload ) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Match a set of pre-assigned profiles with a team
////////////////////////////////////////////////////////////////////////////////
type matchMDMApplePreassignmentRequest struct {
ExternalHostIdentifier string ` json:"external_host_identifier" `
}
type matchMDMApplePreassignmentResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r matchMDMApplePreassignmentResponse ) error ( ) error { return r . Err }
func ( r matchMDMApplePreassignmentResponse ) Status ( ) int { return http . StatusNoContent }
func matchMDMApplePreassignmentEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * matchMDMApplePreassignmentRequest )
if err := svc . MDMAppleMatchPreassignment ( ctx , req . ExternalHostIdentifier ) ; err != nil {
return matchMDMApplePreassignmentResponse { Err : err } , nil
}
return matchMDMApplePreassignmentResponse { } , nil
}
func ( svc * Service ) MDMAppleMatchPreassignment ( ctx context . Context , ref string ) error {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc . authz . SkipAuthorization ( ctx )
return fleet . ErrMissingLicense
}
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).
2023-08-07 17:51:11 +00:00
lic , _ := license . FromContext ( ctx )
if lic == nil || ! lic . IsPremium ( ) {
2023-03-06 14:54:51 +00:00
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-08-07 17:51:11 +00:00
// appconfig is only used internally, it's fine to read it unobfuscated
// (svc.AppConfigObfuscated must not be used because the write-only users
// such as gitops will fail to access it).
ac , err := svc . ds . AppConfig ( 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 {
2023-10-06 22:04:33 +00:00
if ac . MDM . EnableDiskEncryption . Value != * payload . EnableDiskEncryption {
ac . MDM . EnableDiskEncryption = optjson . SetBool ( * payload . EnableDiskEncryption )
2023-03-06 14:54:51 +00:00
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
2023-10-06 22:04:33 +00:00
if ac . MDM . EnableDiskEncryption . Value {
2023-03-08 13:31:53 +00:00
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 ]
2023-05-11 13:36:28 +00:00
if ! file . IsValidMacOSName ( decoded . Package . Filename ) {
return nil , & fleet . BadRequestError {
Message : "package name contains invalid characters" ,
InternalErr : ctxerr . New ( ctx , "package name contains invalid characters" ) ,
}
}
2023-04-07 20:31:02 +00:00
// 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" )
2023-04-27 15:10:41 +00:00
w . Header ( ) . Set ( "Content-Disposition" , fmt . Sprintf ( ` attachment;filename="%s" ` , r . pkg . Name ) )
2023-04-07 20:31:02 +00:00
// 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" `
2023-06-07 17:29:36 +00:00
// ForUpdate is used to indicate that the authorization should be for a
// "write" instead of a "read", this is needed specifically for the gitops
// user which is a write-only user, but needs to call this endpoint to check
// if it needs to upload the bootstrap package (if the hashes are different).
ForUpdate bool ` query:"for_update,optional" `
2023-04-07 20:31:02 +00:00
}
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 )
2023-06-07 17:29:36 +00:00
meta , err := svc . GetMDMAppleBootstrapPackageMetadata ( ctx , req . TeamID , req . ForUpdate )
2023-04-07 20:31:02 +00:00
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
}
2023-06-07 17:29:36 +00:00
func ( svc * Service ) GetMDMAppleBootstrapPackageMetadata ( ctx context . Context , teamID uint , forUpdate bool ) ( * fleet . MDMAppleBootstrapPackage , 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 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-05-10 20:22:08 +00:00
////////////////////////////////////////////////////////////////////////////////
// Update MDM Apple Setup
////////////////////////////////////////////////////////////////////////////////
type updateMDMAppleSetupRequest struct {
fleet . MDMAppleSetupPayload
}
type updateMDMAppleSetupResponse struct {
Err error ` json:"error,omitempty" `
}
func ( r updateMDMAppleSetupResponse ) error ( ) error { return r . Err }
func ( r updateMDMAppleSetupResponse ) 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 updateMDMAppleSetupEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
req := request . ( * updateMDMAppleSetupRequest )
if err := svc . UpdateMDMAppleSetup ( ctx , req . MDMAppleSetupPayload ) ; err != nil {
return updateMDMAppleSetupResponse { Err : err } , nil
}
return updateMDMAppleSetupResponse { } , nil
}
func ( svc * Service ) UpdateMDMAppleSetup ( ctx context . Context , payload fleet . MDMAppleSetupPayload ) 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 { }
2023-07-26 17:20:36 +00:00
// TODO: these errors will result in JSON being returned, but we should
// redirect to the UI and let the UI display an error instead. The errors are
// rare enough (malformed data coming from the SSO provider) so they shouldn't
// affect many users.
2023-04-27 12:43:20 +00:00
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 {
2023-05-05 17:36:13 +00:00
redirectURL string
2023-04-27 12:43:20 +00:00
}
func ( r callbackMDMAppleSSOResponse ) hijackRender ( ctx context . Context , w http . ResponseWriter ) {
2023-05-05 17:36:13 +00:00
w . Header ( ) . Set ( "Location" , r . redirectURL )
w . WriteHeader ( http . StatusTemporaryRedirect )
2023-04-27 12:43:20 +00:00
}
2023-07-26 17:20:36 +00:00
// Error will always be nil because errors are handled by sending a query
// parameter in the URL response, this way the UI is able to display an erorr
// message.
func ( r callbackMDMAppleSSOResponse ) error ( ) error { return nil }
2023-05-05 17:36:13 +00:00
2023-04-27 12:43:20 +00:00
func callbackMDMAppleSSOEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( errorer , error ) {
auth := request . ( fleet . Auth )
2023-07-26 17:20:36 +00:00
redirectURL := svc . InitiateMDMAppleSSOCallback ( ctx , auth )
2023-05-05 17:36:13 +00:00
return callbackMDMAppleSSOResponse { redirectURL : redirectURL } , nil
2023-04-27 12:43:20 +00:00
}
2023-07-26 17:20:36 +00:00
func ( svc * Service ) InitiateMDMAppleSSOCallback ( ctx context . Context , auth fleet . Auth ) string {
2023-04-27 12:43:20 +00:00
// skipauth: No authorization check needed due to implementation
// returning only license error.
svc . authz . SkipAuthorization ( ctx )
2023-07-26 17:20:36 +00:00
return apple_mdm . FleetUISSOCallbackPath + "?error=true"
2023-04-27 12:43:20 +00:00
}
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 {
2023-06-06 23:18:14 +00:00
return ctxerr . Wrap ( r . Context , err , "ingesting device in Authenticate message" )
}
2023-06-30 15:30:49 +00:00
if err := svc . ds . ResetMDMAppleEnrollment ( r . Context , host . UDID ) ; err != nil {
2023-06-06 23:18:14 +00:00
return ctxerr . Wrap ( r . Context , err , "resetting nano enrollment info in Authenticate message" )
2023-01-16 20:06:30 +00:00
}
info , err := svc . ds . GetHostMDMCheckinInfo ( r . Context , m . Enrollment . UDID )
if err != nil {
2023-06-06 23:18:14 +00:00
return ctxerr . Wrap ( r . Context , err , "getting checkin info in Authenticate message" )
2023-01-16 20:06:30 +00:00
}
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 ,
2023-07-06 18:33:40 +00:00
MDMPlatform : fleet . MDMPlatformApple ,
2023-01-16 20:06:30 +00:00
} )
}
// 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
2023-11-20 14:16:02 +00:00
if err := svc . ds . BulkSetPendingMDMHostProfiles ( r . Context , nil , nil , nil , nil , [ ] string { r . ID } ) ; err != nil {
2023-03-27 18:43:01 +00:00
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
}
2023-09-23 00:54:45 +00:00
// TODO: improve this to not enqueue the job if a host that is
// assigned in ABM is manually enrolling for some reason.
if info . DEPAssignedToFleet || info . InstalledFromDEP {
2023-06-05 15:58:23 +00:00
svc . logger . Log ( "info" , "queueing post-enroll task for newly enrolled DEP device" , "host_uuid" , r . ID )
2023-04-07 20:31:02 +00:00
2023-06-05 15:58:23 +00:00
var tmID * uint
if info . TeamID != 0 {
tmID = & info . TeamID
}
if err := worker . QueueAppleMDMJob (
r . Context ,
svc . ds ,
svc . logger ,
worker . AppleMDMPostDEPEnrollmentTask ,
r . ID ,
tmID ,
r . Params [ "enroll_reference" ] ,
) ; err != nil {
return ctxerr . Wrap ( r . Context , err , "queue DEP post-enroll task" )
2023-04-22 15:23:38 +00:00
}
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-09-12 14:59:47 +00:00
return nil , apple_mdm . HandleHostMDMProfileInstallResult (
r . Context ,
svc . ds ,
res . UDID ,
res . CommandUUID ,
mdmAppleDeliveryStatusFromCommandStatus ( res . Status ) ,
apple_mdm . FmtErrorChain ( res . ErrorChain ) ,
)
2023-02-22 17:49:06 +00:00
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 ,
2023-06-05 17:05:28 +00:00
Status : mdmAppleDeliveryStatusFromCommandStatus ( res . Status ) ,
2023-04-22 15:23:38 +00:00
Detail : apple_mdm . FmtErrorChain ( res . ErrorChain ) ,
2023-11-07 21:03:03 +00:00
OperationType : fleet . MDMOperationTypeRemove ,
2023-02-22 17:49:06 +00:00
} )
}
2023-01-16 20:06:30 +00:00
return nil , nil
}
2023-02-17 19:26:51 +00:00
2023-06-05 17:05:28 +00:00
// mdmAppleDeliveryStatusFromCommandStatus converts a MDM command status to a
// fleet.MDMAppleDeliveryStatus.
//
// NOTE: this mapping does not include all
// possible delivery statuses (e.g., verified status is not included) is intended to
// only be used in the context of CommandAndReportResults in the MDMAppleCheckinAndCommandService.
// Extra care should be taken before using this function in other contexts.
2023-11-07 21:03:03 +00:00
func mdmAppleDeliveryStatusFromCommandStatus ( cmdStatus string ) * fleet . MDMDeliveryStatus {
2023-06-05 17:05:28 +00:00
switch cmdStatus {
case fleet . MDMAppleStatusAcknowledged :
2023-11-07 21:03:03 +00:00
return & fleet . MDMDeliveryVerifying
2023-06-05 17:05:28 +00:00
case fleet . MDMAppleStatusError , fleet . MDMAppleStatusCommandFormatError :
2023-11-07 21:03:03 +00:00
return & fleet . MDMDeliveryFailed
2023-06-05 17:05:28 +00:00
case fleet . MDMAppleStatusIdle , fleet . MDMAppleStatusNotNow :
2023-11-07 21:03:03 +00:00
return & fleet . MDMDeliveryPending
2023-06-05 17:05:28 +00:00
default :
return nil
}
}
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 == "" {
2023-05-12 14:54:23 +00:00
var msg string
if es . TeamID != nil {
msg += fmt . Sprintf ( "team_id %d doesn't have an enroll secret, " , * es . TeamID )
}
2023-04-04 20:09:20 +00:00
if globalSecret == "" {
2023-05-12 14:54:23 +00:00
logger . Log ( "err" , msg + "no global enroll secret found, skipping the creation of a com.fleetdm.fleetd.config profile" )
2023-04-04 20:09:20 +00:00
continue
}
2023-05-12 14:54:23 +00:00
logger . Log ( "err" , msg + "using a global enroll secret for com.fleetdm.fleetd.config profile" )
2023-04-04 20:09:20 +00:00
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-11-10 14:05:10 +00:00
func ReconcileAppleProfiles (
2023-02-22 17:49:06 +00:00
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-11-30 12:17:07 +00:00
appConfig , err := ds . AppConfig ( ctx )
if err != nil {
return fmt . Errorf ( "reading app config: %w" , err )
}
if ! appConfig . MDM . EnabledAndConfigured {
return nil
}
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 ) )
2023-07-20 21:11:45 +00:00
// profileIntersection tracks profilesToAdd ∩ profilesToRemove, this is used to avoid:
//
// - Sending a RemoveProfile followed by an InstallProfile for a
// profile with an identifier that's already installed, which can cause
// racy behaviors.
// - Sending a InstallProfile command for a profile that's exactly the
// same as the one installed. Customers have reported that sending the
// command causes unwanted behavior.
profileIntersection := apple_mdm . NewProfileBimap ( )
profileIntersection . IntersectByIdentifierAndHostUUID ( toInstall , toRemove )
// hostProfilesToCleanup is used to track profiles that should be removed
// from the database directly without having to issue a RemoveProfile
// command.
hostProfilesToCleanup := [ ] * fleet . MDMAppleProfilePayload { }
2023-03-27 18:43:01 +00:00
// 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-07-20 21:11:45 +00:00
if pp , ok := profileIntersection . GetMatchingProfileInCurrentState ( p ) ; ok {
// if the profile was in any other status than `failed`
// and the checksums match (the profiles are exactly
// the same) we don't send another InstallProfile
// command.
2023-11-07 21:03:03 +00:00
if pp . Status != & fleet . MDMDeliveryFailed && bytes . Equal ( pp . Checksum , p . Checksum ) {
2023-07-20 21:11:45 +00:00
hostProfiles = append ( hostProfiles , & fleet . MDMAppleBulkUpsertHostProfilePayload {
ProfileID : p . ProfileID ,
HostUUID : p . HostUUID ,
ProfileIdentifier : p . ProfileIdentifier ,
ProfileName : p . ProfileName ,
Checksum : p . Checksum ,
OperationType : pp . OperationType ,
Status : pp . Status ,
CommandUUID : pp . CommandUUID ,
Detail : pp . Detail ,
} )
continue
}
}
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 ,
2023-11-07 21:03:03 +00:00
OperationType : fleet . MDMOperationTypeInstall ,
Status : & fleet . MDMDeliveryPending ,
2023-03-27 18:43:01 +00:00
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-07-20 21:11:45 +00:00
if _ , ok := profileIntersection . GetMatchingProfileInDesiredState ( p ) ; ok {
hostProfilesToCleanup = append ( hostProfilesToCleanup , p )
continue
}
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 ,
2023-11-07 21:03:03 +00:00
OperationType : fleet . MDMOperationTypeRemove ,
Status : & fleet . MDMDeliveryPending ,
2023-03-27 18:43:01 +00:00
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
}
2023-07-20 21:11:45 +00:00
// delete all profiles that have a matching identifier to be installed.
// This is to prevent sending both a `RemoveProfile` and an
// `InstallProfile` for the same identifier, which can cause race
// conditions. It's better to "update" the profile by sending a single
// `InstallProfile` command.
if err := ds . BulkDeleteMDMAppleHostsConfigProfiles ( ctx , hostProfilesToCleanup ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "deleting profiles that didn't change" )
}
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-11-07 21:03:03 +00:00
execCmd := func ( profID uint , target * cmdTarget , op fleet . MDMOperationType ) {
2023-03-27 18:43:01 +00:00
defer wgProd . Done ( )
var err error
switch op {
2023-11-07 21:03:03 +00:00
case fleet . MDMOperationTypeInstall :
2023-03-27 18:43:01 +00:00
err = commander . InstallProfile ( ctx , target . hostUUIDs , profileContents [ profID ] , target . cmdUUID )
2023-11-07 21:03:03 +00:00
case fleet . MDMOperationTypeRemove :
2023-03-27 18:43:01 +00:00
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 )
2023-11-07 21:03:03 +00:00
go execCmd ( profID , target , fleet . MDMOperationTypeInstall )
2023-03-27 18:43:01 +00:00
}
for profID , target := range removeTargets {
wgProd . Add ( 1 )
2023-11-07 21:03:03 +00:00
go execCmd ( profID , target , fleet . MDMOperationTypeRemove )
2023-03-27 18:43:01 +00:00
}
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" )
}
2023-05-12 16:50:20 +00:00
2023-02-22 17:49:06 +00:00
return nil
}
2023-09-22 21:50:43 +00:00
func ( svc * Service ) maybeRestorePendingDEPHost ( ctx context . Context , host * fleet . Host ) error {
if host . Platform != "darwin" {
return nil
}
license , ok := license . FromContext ( ctx )
if ! ok {
return ctxerr . New ( ctx , "maybe restore pending DEP host: missing license" )
} else if license . Tier != fleet . TierPremium {
// only premium tier supports DEP so nothing more to do
return nil
}
ac , err := svc . ds . AppConfig ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "maybe restore pending DEP host: get app config" )
} else if ! ac . MDM . AppleBMEnabledAndConfigured {
// if ABM is not enabled and configured, nothing more to do
return nil
}
dep , err := svc . ds . GetHostDEPAssignment ( ctx , host . ID )
switch {
case err != nil && ! fleet . IsNotFound ( err ) :
return ctxerr . Wrap ( ctx , err , "maybe restore pending DEP host: get host dep assignment" )
case dep != nil && dep . DeletedAt == nil :
return svc . restorePendingDEPHost ( ctx , host , ac )
default :
// no DEP assignment was found or the DEP assignment was deleted in ABM
// so nothing more to do
}
return nil
}
func ( svc * Service ) restorePendingDEPHost ( ctx context . Context , host * fleet . Host , appCfg * fleet . AppConfig ) error {
tmID , err := svc . getConfigAppleBMDefaultTeamID ( ctx , appCfg )
if err != nil {
return ctxerr . Wrap ( ctx , err , "restore pending dep host" )
}
host . TeamID = tmID
if err := svc . ds . RestoreMDMApplePendingDEPHost ( ctx , host ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "restore pending dep host" )
}
if err := worker . QueueMacosSetupAssistantJob ( ctx , svc . ds , svc . logger ,
worker . MacosSetupAssistantHostsTransferred , tmID , host . HardwareSerial ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "restore pending dep host" )
}
return nil
}
func ( svc * Service ) getConfigAppleBMDefaultTeamID ( ctx context . Context , appCfg * fleet . AppConfig ) ( * uint , error ) {
var tmID * uint
if name := appCfg . MDM . AppleBMDefaultTeam ; name != "" {
team , err := svc . ds . TeamByName ( ctx , name )
switch {
case fleet . IsNotFound ( err ) :
level . Debug ( svc . logger ) . Log (
"msg" ,
"unable to find default team assigned in config, mdm devices won't be assigned to a team" ,
"team_name" ,
name ,
)
return nil , nil
case err != nil :
return nil , ctxerr . Wrap ( ctx , err , "get default team for mdm devices" )
case team != nil :
tmID = & team . ID
}
}
return tmID , nil
}