diff --git a/docs/Using-Fleet/Permissions.md b/docs/Using-Fleet/Permissions.md index 7d3a3f3296..dd4d049de1 100644 --- a/docs/Using-Fleet/Permissions.md +++ b/docs/Using-Fleet/Permissions.md @@ -78,6 +78,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | View results of MDM commands executed on macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ | ✅ | | | Edit [MDM settings](https://fleetdm.com/docs/using-fleet/mdm-macos-settings) | | | | ✅ | ✅ | | Edit [MDM settings for teams](https://fleetdm.com/docs/using-fleet/mdm-macos-settings) | | | | ✅ | ✅ | +| Upload an EULA file for MDM automatic enrollment\* | | | | ✅ | | | View/download MDM macOS setup assistant\* | | | ✅ | ✅ | | | Edit/upload MDM macOS setup assistant\* | | | ✅ | ✅ | | diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 4a3f10411c..69a5d0fc31 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -3572,6 +3572,11 @@ These API endpoints are used to automate MDM features in Fleet. Read more about - [Get metadata about a bootstrap package](#get-metadata-about-a-bootstrap-package) - [Delete a bootstrap package](#delete-a-bootstrap-package) - [Download a bootstrap package](#download-a-bootstrap-package) +- [Get a summary of bootstrap package status](#get-a-summary-of-bootstrap-package-status) +- [Upload an EULA file](#upload-an-eula-file) +- [Get metadata about an EULA file](#get-metadata-about-an-eula-file) +- [Delete an EULA file](#delete-an-eula-file) +- [Download an EULA file](#download-an-eula-file) ### Add custom macOS setting (configuration profile) @@ -4288,6 +4293,125 @@ The summary can optionally be filtered by team id. } ``` +### Upload an EULA file + +_Available in Fleet Premium_ + +Upload an EULA that will be shown during the DEP flow. + +`POST /api/v1/fleet/mdm/apple/setup/eula` + +#### Parameters + +| Name | Type | In | Description | +| ---- | ---- | ---- | ------------------------------------------------- | +| eula | file | form | **Required**. A PDF document containing the EULA. | + +#### Example + +`POST /api/v1/fleet/mdm/apple/setup/eula` + +##### Request headers + +``` +Content-Length: 850 +Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y +``` + +##### Request body + +``` +--------------------------f02md47480und42y +Content-Disposition: form-data; name="eula"; filename="eula.pdf" +Content-Type: application/octet-stream + +--------------------------f02md47480und42y-- +``` + +##### Default response + +`Status: 200` + +### Get metadata about an EULA file + +_Available in Fleet Premium_ + +Get information about the EULA file that was uploaded to Fleet. If no EULA was previously uploaded, this endpoint returns a `404` status code. + +`GET /api/v1/fleet/mdm/apple/setup/eula/metadata` + +#### Example + +`GET /api/v1/fleet/mdm/apple/setup/eula/metadata` + +##### Default response + +`Status: 200` + +```json +{ + "name": "eula.pdf", + "token": "AA598E2A-7952-46E3-B89D-526D45F7E233", + "created_at": "2023-04-20T13:02:05Z" +} +``` + +In the response above: + +- `token` is the value you can use to [download an EULA](#download-an-eula-file) + +### Delete an EULA file + +_Available in Fleet Premium_ + +Delete an EULA file. + +`DELETE /api/v1/fleet/mdm/apple/setup/eula/{token}` + +#### Parameters + +| Name | Type | In | Description | +| ----- | ------ | ----- | ---------------------------------------- | +| token | string | path | **Required** The token of the EULA file. | + +#### Example + +`DELETE /api/v1/fleet/mdm/apple/setup/eula/AA598E2A-7952-46E3-B89D-526D45F7E233` + +##### Default response + +`Status: 200` + +### Download an EULA file + +_Available in Fleet Premium_ + +Download an EULA file + +`GET /api/v1/fleet/mdm/apple/setup/eula/{token}` + +#### Parameters + +| Name | Type | In | Description | +| ----- | ------ | ----- | ---------------------------------------- | +| token | string | path | **Required** The token of the EULA file. | + +#### Example + +`GET /api/v1/fleet/mdm/apple/setup/eula/AA598E2A-7952-46E3-B89D-526D45F7E233` + +##### Default response + +`Status: 200` + +``` +Status: 200 +Content-Type: application/pdf +Content-Disposition: attachment +Content-Length: +Body: +``` + --- ## Policies diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index caca8db7ee..467f86026a 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -293,6 +293,79 @@ func (svc *Service) GetMDMAppleBootstrapPackageSummary(ctx context.Context, team return summary, nil } +func (svc *Service) MDMAppleCreateEULA(ctx context.Context, name string, f io.ReadSeeker) error { + if err := svc.authz.Authorize(ctx, &fleet.MDMAppleEULA{}, fleet.ActionWrite); err != nil { + return err + } + + if err := file.CheckPDF(f); err != nil { + if errors.Is(err, file.ErrInvalidType) { + return &fleet.BadRequestError{ + Message: err.Error(), + InternalErr: err, + } + } + + return ctxerr.Wrap(ctx, err, "checking pdf") + } + + // ensure we read the file from the start + _, err := f.Seek(0, io.SeekStart) + if err != nil { + return ctxerr.Wrap(ctx, err, "seeking start of PDF file") + } + + bytes, err := io.ReadAll(f) + if err != nil { + return ctxerr.Wrap(ctx, err, "reading EULA bytes") + } + + eula := &fleet.MDMAppleEULA{ + Name: name, + Token: uuid.New().String(), + Bytes: bytes, + } + + if err := svc.ds.MDMAppleInsertEULA(ctx, eula); err != nil { + return ctxerr.Wrap(ctx, err, "inserting EULA") + } + + return nil +} + +func (svc *Service) MDMAppleGetEULABytes(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) { + // skipauth: this resource is authorized using the token provided in the + // request. + svc.authz.SkipAuthorization(ctx) + + return svc.ds.MDMAppleGetEULABytes(ctx, token) +} + +func (svc *Service) MDMAppleDeleteEULA(ctx context.Context, token string) error { + if err := svc.authz.Authorize(ctx, &fleet.MDMAppleEULA{}, fleet.ActionWrite); err != nil { + return err + } + + if err := svc.ds.MDMAppleDeleteEULA(ctx, token); err != nil { + return ctxerr.Wrap(ctx, err, "deleting EULA") + } + + return nil +} + +func (svc *Service) MDMAppleGetEULAMetadata(ctx context.Context) (*fleet.MDMAppleEULA, error) { + if err := svc.authz.Authorize(ctx, &fleet.MDMAppleEULA{}, fleet.ActionRead); err != nil { + return nil, err + } + + eula, err := svc.ds.MDMAppleGetEULAMetadata(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting EULA metadata") + } + + return eula, nil +} + func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) { if err := svc.authz.Authorize(ctx, asst, fleet.ActionWrite); err != nil { return nil, err diff --git a/pkg/file/pdf.go b/pkg/file/pdf.go new file mode 100644 index 0000000000..2709d46671 --- /dev/null +++ b/pkg/file/pdf.go @@ -0,0 +1,28 @@ +package file + +import ( + "bytes" + "errors" + "fmt" + "io" +) + +// pdfMagic is the [file signature][1] (or magic bytes) for PDF +// +// [1]: https://en.wikipedia.org/wiki/List_of_file_signatures +var pdfMagic = []byte{0x25, 0x50, 0x44, 0x46} + +// CheckPDF checks if the provided bytes are a PDF file. +func CheckPDF(pdf io.Reader) error { + buf := make([]byte, len(pdfMagic)) + if _, err := io.ReadFull(pdf, buf); err != nil { + if errors.Is(err, io.ErrUnexpectedEOF) { + return ErrInvalidType + } + return fmt.Errorf("reading magic bytes: %w", err) + } + if !bytes.Equal(buf, pdfMagic) { + return ErrInvalidType + } + return nil +} diff --git a/pkg/file/pdf_test.go b/pkg/file/pdf_test.go new file mode 100644 index 0000000000..71d822895c --- /dev/null +++ b/pkg/file/pdf_test.go @@ -0,0 +1,32 @@ +package file + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCheckPDF(t *testing.T) { + testCases := []struct { + in []byte + outErr string + }{ + {[]byte{}, "reading magic bytes: EOF"}, + {[]byte("--"), ErrInvalidType.Error()}, + {[]byte("invalid"), ErrInvalidType.Error()}, + {[]byte("%PDF-"), ""}, + {[]byte("%PDF-1"), ""}, + {[]byte("%PDF-2"), ""}, + } + + for _, c := range testCases { + r := bytes.NewReader(c.in) + err := CheckPDF(r) + if c.outErr != "" { + require.ErrorContains(t, err, c.outErr) + } else { + require.NoError(t, err) + } + } +} diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 08a2c6eb0e..dab4c797cf 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -1954,6 +1954,65 @@ func bulkDeleteHostDiskEncryptionKeysDB(ctx context.Context, tx sqlx.ExtContext, return err } +func (ds *Datastore) MDMAppleGetEULAMetadata(ctx context.Context) (*fleet.MDMAppleEULA, error) { + // Currently, there can only be one EULA in the database, and we're + // hardcoding it's id to be 1 in order to enforce this restriction. + stmt := "SELECT name, created_at, token FROM eulas WHERE id = 1" + var eula fleet.MDMAppleEULA + if err := sqlx.GetContext(ctx, ds.reader, &eula, stmt); err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("MDMAppleEULA")) + } + return nil, ctxerr.Wrap(ctx, err, "get EULA metadata") + } + return &eula, nil +} + +func (ds *Datastore) MDMAppleGetEULABytes(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) { + stmt := "SELECT name, bytes FROM eulas WHERE token = ?" + var eula fleet.MDMAppleEULA + if err := sqlx.GetContext(ctx, ds.reader, &eula, stmt, token); err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("MDMAppleEULA")) + } + return nil, ctxerr.Wrap(ctx, err, "get EULA bytes") + } + return &eula, nil +} + +func (ds *Datastore) MDMAppleInsertEULA(ctx context.Context, eula *fleet.MDMAppleEULA) error { + // We're intentionally hardcoding the id to be 1 because we only want to + // allow one EULA. + stmt := ` + INSERT INTO eulas (id, name, bytes, token) + VALUES (1, ?, ?, ?) + ` + + _, err := ds.writer.ExecContext(ctx, stmt, eula.Name, eula.Bytes, eula.Token) + if err != nil { + if isDuplicate(err) { + return ctxerr.Wrap(ctx, alreadyExists("MDMAppleEULA", eula.Token)) + } + return ctxerr.Wrap(ctx, err, "create EULA") + } + + return nil +} + +func (ds *Datastore) MDMAppleDeleteEULA(ctx context.Context, token string) error { + stmt := "DELETE FROM eulas WHERE token = ?" + res, err := ds.writer.ExecContext(ctx, stmt, token) + if err != nil { + return ctxerr.Wrap(ctx, err, "delete EULA") + } + + deleted, _ := res.RowsAffected() + if deleted != 1 { + return ctxerr.Wrap(ctx, notFound("MDMAppleEULA")) + } + return nil +} + func (ds *Datastore) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) { const stmt = ` INSERT INTO diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index fdcd612cce..d8c2e9ff38 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestMDMAppleConfigProfile(t *testing.T) { +func TestMDMApple(t *testing.T) { ds := CreateMySQLDS(t) cases := []struct { @@ -54,6 +54,7 @@ func TestMDMAppleConfigProfile(t *testing.T) { {"TestBulkUpsertMDMAppleConfigProfiles", testBulkUpsertMDMAppleConfigProfile}, {"TestMDMAppleBootstrapPackageCRUD", testMDMAppleBootstrapPackageCRUD}, {"TestListMDMAppleCommands", testListMDMAppleCommands}, + {"TestMDMAppleEULA", testMDMAppleEULA}, {"TestMDMAppleSetupAssistant", testMDMAppleSetupAssistant}, } @@ -3219,6 +3220,47 @@ func testListMDMAppleCommands(t *testing.T, ds *Datastore) { }) } +func testMDMAppleEULA(t *testing.T, ds *Datastore) { + ctx := context.Background() + eula := &fleet.MDMAppleEULA{ + Token: uuid.New().String(), + Name: "eula.pdf", + Bytes: []byte("contents"), + } + + err := ds.MDMAppleInsertEULA(ctx, eula) + require.NoError(t, err) + + var ae fleet.AlreadyExistsError + err = ds.MDMAppleInsertEULA(ctx, eula) + require.ErrorAs(t, err, &ae) + + gotEULA, err := ds.MDMAppleGetEULAMetadata(ctx) + require.NoError(t, err) + require.NotEmpty(t, gotEULA.CreatedAt) + require.Equal(t, eula.Token, gotEULA.Token) + require.Equal(t, eula.Name, gotEULA.Name) + + gotEULABytes, err := ds.MDMAppleGetEULABytes(ctx, eula.Token) + require.NoError(t, err) + require.EqualValues(t, eula.Bytes, gotEULABytes.Bytes) + require.Equal(t, eula.Name, gotEULABytes.Name) + + err = ds.MDMAppleDeleteEULA(ctx, eula.Token) + require.NoError(t, err) + + var nfe fleet.NotFoundError + _, err = ds.MDMAppleGetEULAMetadata(ctx) + require.ErrorAs(t, err, &nfe) + _, err = ds.MDMAppleGetEULABytes(ctx, eula.Token) + require.ErrorAs(t, err, &nfe) + err = ds.MDMAppleDeleteEULA(ctx, eula.Token) + require.ErrorAs(t, err, &nfe) + + err = ds.MDMAppleInsertEULA(ctx, eula) + require.NoError(t, err) +} + func testMDMAppleSetupAssistant(t *testing.T, ds *Datastore) { ctx := context.Background() diff --git a/server/datastore/mysql/migrations/tables/20230425105727_AddEulasTable.go b/server/datastore/mysql/migrations/tables/20230425105727_AddEulasTable.go new file mode 100644 index 0000000000..2ae04bf2dd --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230425105727_AddEulasTable.go @@ -0,0 +1,32 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20230425105727, Down_20230425105727) +} + +func Up_20230425105727(tx *sql.Tx) error { + _, err := tx.Exec(` + CREATE TABLE eulas ( + id int(10) unsigned NOT NULL, + token varchar(36), + name varchar(255), + bytes longblob, + created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id) + )`) + if err != nil { + return fmt.Errorf("creating eulas table: %w", err) + } + + return nil +} + +func Down_20230425105727(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230425105727_AddEulasTable_test.go b/server/datastore/mysql/migrations/tables/20230425105727_AddEulasTable_test.go new file mode 100644 index 0000000000..7e5f6b3815 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230425105727_AddEulasTable_test.go @@ -0,0 +1,45 @@ +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20230425105727(t *testing.T) { + db := applyUpToPrev(t) + applyNext(t, db) + + insertStmt := ` + INSERT INTO eulas (id, token, name, bytes) + VALUES (?, ?, ?, ?) + ` + + selectStmt := ` + SELECT id, name, bytes, token + FROM eulas + WHERE token = ? + ` + + _, err := db.Exec(insertStmt, 1, "ABC-DEF", "eula.pdf", []byte("eula")) + require.NoError(t, err) + + _, err = db.Exec(insertStmt, 1, "ABC-DEF", "eula_2.pdf", []byte("eula_2")) + require.ErrorContains(t, err, "Error 1062") + + _, err = db.Exec(insertStmt, 2, "ABC-DEF", "eula_2.pdf", []byte("eula_2")) + require.NoError(t, err) + + var ( + token string + name string + bytes []byte + id uint + ) + + err = db.QueryRow(selectStmt, "ABC-DEF").Scan(&id, &name, &bytes, &token) + require.NoError(t, err) + require.Equal(t, "ABC-DEF", token) + require.Equal(t, "eula.pdf", name) + require.Equal(t, []byte("eula"), bytes) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 6403923218..79c66f7151 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -146,6 +146,17 @@ CREATE TABLE `enroll_secrets` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; +CREATE TABLE `eulas` ( + `id` int(10) unsigned NOT NULL, + `token` varchar(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `bytes` longblob, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; CREATE TABLE `host_additional` ( `host_id` int(10) unsigned NOT NULL, `additional` json DEFAULT NULL, @@ -593,9 +604,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=183 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=184 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index bae8279cdb..25e92d87ff 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -886,6 +886,18 @@ type Datastore interface { // GetHostMDMMacOSSetup returns the MDM macOS setup information for the specified host id. GetHostMDMMacOSSetup(ctx context.Context, hostID uint) (*HostMDMMacOSSetup, error) + // MDMAppleGetEULAMetadata returns metadata information about the EULA + // filed stored in the database. + MDMAppleGetEULAMetadata(ctx context.Context) (*MDMAppleEULA, error) + // MDMAppleGetEULABytes returns the bytes of the EULA file stored in + // the database. A token is required since this file is publicly + // accessible by anyone with the token. + MDMAppleGetEULABytes(ctx context.Context, token string) (*MDMAppleEULA, error) + // MDMAppleInsertEULA inserts a new EULA in the database + MDMAppleInsertEULA(ctx context.Context, eula *MDMAppleEULA) error + // MDMAppleDeleteEULA deletes the EULA file from the database + MDMAppleDeleteEULA(ctx context.Context, token string) error + // Create or update the MDM Apple Setup Assistant for a team or no team. SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *MDMAppleSetupAssistant) (*MDMAppleSetupAssistant, error) // Get the MDM Apple Setup Assistant for the provided team or no team. diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 33b8b6e3ba..feb17b2575 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -88,3 +88,15 @@ func (bp *MDMAppleBootstrapPackage) URL(host string) (string, error) { pkgURL.RawQuery = fmt.Sprintf("token=%s", bp.Token) return pkgURL.String(), nil } + +// MDMAppleEULA represents an EULA (End User License Agreement) file. +type MDMAppleEULA struct { + Name string `json:"name"` + Bytes []byte `json:"bytes"` + Token string `json:"token"` + CreatedAt time.Time `json:"created_at" db:"created_at"` +} + +func (e MDMAppleEULA) AuthzType() string { + return "mdm_apple" +} diff --git a/server/fleet/service.go b/server/fleet/service.go index db87319af6..056fc23081 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -700,6 +700,20 @@ type Service interface { GetMDMAppleBootstrapPackageSummary(ctx context.Context, teamID *uint) (*MDMAppleBootstrapPackageSummary, error) + // MDMAppleGetEULABytes returns the contents of the EULA that matches + // the given token. + // + // A token is required as the means of authentication for this resource + // since it can be publicly accessed with anyone with a valid token. + MDMAppleGetEULABytes(ctx context.Context, token string) (*MDMAppleEULA, error) + // MDMAppleGetEULABytes returns metadata about the EULA file that can + // be used by clients to display information. + MDMAppleGetEULAMetadata(ctx context.Context) (*MDMAppleEULA, error) + // MDMAppleCreateEULA adds a new EULA file. + MDMAppleCreateEULA(ctx context.Context, name string, file io.ReadSeeker) error + // MDMAppleDelete EULA removes an EULA entry. + MDMAppleDeleteEULA(ctx context.Context, token string) error + // Create or update the MDM Apple Setup Assistant for a team or no team. SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *MDMAppleSetupAssistant) (*MDMAppleSetupAssistant, error) // Get the MDM Apple Setup Assistant for the provided team or no team. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index b403fa32e3..67be2a1cd9 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -602,6 +602,14 @@ type RecordHostBootstrapPackageFunc func(ctx context.Context, commandUUID string type GetHostMDMMacOSSetupFunc func(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) +type MDMAppleGetEULAMetadataFunc func(ctx context.Context) (*fleet.MDMAppleEULA, error) + +type MDMAppleGetEULABytesFunc func(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) + +type MDMAppleInsertEULAFunc func(ctx context.Context, eula *fleet.MDMAppleEULA) error + +type MDMAppleDeleteEULAFunc func(ctx context.Context, token string) error + type SetOrUpdateMDMAppleSetupAssistantFunc func(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) type GetMDMAppleSetupAssistantFunc func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) @@ -1488,6 +1496,18 @@ type DataStore struct { GetHostMDMMacOSSetupFunc GetHostMDMMacOSSetupFunc GetHostMDMMacOSSetupFuncInvoked bool + MDMAppleGetEULAMetadataFunc MDMAppleGetEULAMetadataFunc + MDMAppleGetEULAMetadataFuncInvoked bool + + MDMAppleGetEULABytesFunc MDMAppleGetEULABytesFunc + MDMAppleGetEULABytesFuncInvoked bool + + MDMAppleInsertEULAFunc MDMAppleInsertEULAFunc + MDMAppleInsertEULAFuncInvoked bool + + MDMAppleDeleteEULAFunc MDMAppleDeleteEULAFunc + MDMAppleDeleteEULAFuncInvoked bool + SetOrUpdateMDMAppleSetupAssistantFunc SetOrUpdateMDMAppleSetupAssistantFunc SetOrUpdateMDMAppleSetupAssistantFuncInvoked bool @@ -3551,6 +3571,34 @@ func (s *DataStore) GetHostMDMMacOSSetup(ctx context.Context, hostID uint) (*fle return s.GetHostMDMMacOSSetupFunc(ctx, hostID) } +func (s *DataStore) MDMAppleGetEULAMetadata(ctx context.Context) (*fleet.MDMAppleEULA, error) { + s.mu.Lock() + s.MDMAppleGetEULAMetadataFuncInvoked = true + s.mu.Unlock() + return s.MDMAppleGetEULAMetadataFunc(ctx) +} + +func (s *DataStore) MDMAppleGetEULABytes(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) { + s.mu.Lock() + s.MDMAppleGetEULABytesFuncInvoked = true + s.mu.Unlock() + return s.MDMAppleGetEULABytesFunc(ctx, token) +} + +func (s *DataStore) MDMAppleInsertEULA(ctx context.Context, eula *fleet.MDMAppleEULA) error { + s.mu.Lock() + s.MDMAppleInsertEULAFuncInvoked = true + s.mu.Unlock() + return s.MDMAppleInsertEULAFunc(ctx, eula) +} + +func (s *DataStore) MDMAppleDeleteEULA(ctx context.Context, token string) error { + s.mu.Lock() + s.MDMAppleDeleteEULAFuncInvoked = true + s.mu.Unlock() + return s.MDMAppleDeleteEULAFunc(ctx, token) +} + func (s *DataStore) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) { s.mu.Lock() s.SetOrUpdateMDMAppleSetupAssistantFuncInvoked = true diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 1a082925a6..c534b64b23 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -158,6 +158,18 @@ func setupAppleMDMService(t *testing.T) (fleet.Service, context.Context, *mock.S ds.GetMDMAppleCommandRequestTypeFunc = func(ctx context.Context, commandUUID string) (string, error) { return "", nil } + ds.MDMAppleGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMAppleEULA, error) { + return &fleet.MDMAppleEULA{}, nil + } + ds.MDMAppleGetEULABytesFunc = func(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) { + return &fleet.MDMAppleEULA{}, nil + } + ds.MDMAppleInsertEULAFunc = func(ctx context.Context, eula *fleet.MDMAppleEULA) error { + return nil + } + ds.MDMAppleDeleteEULAFunc = func(ctx context.Context, token string) error { + return nil + } return svc, ctx, ds } @@ -194,6 +206,14 @@ func TestAppleMDMAuthorization(t *testing.T) { checkAuthErr(t, err, shouldFailWithAuth) _, err = svc.ListMDMAppleDEPDevices(ctx) checkAuthErr(t, err, shouldFailWithAuth) + + // check EULA routes + _, err = svc.MDMAppleGetEULAMetadata(ctx) + checkAuthErr(t, err, shouldFailWithAuth) + err = svc.MDMAppleCreateEULA(ctx, "eula.pdf", bytes.NewReader([]byte("%PDF-"))) + checkAuthErr(t, err, shouldFailWithAuth) + err = svc.MDMAppleDeleteEULA(ctx, "foo") + checkAuthErr(t, err, shouldFailWithAuth) } // Only global admins can access the endpoints. @@ -217,6 +237,8 @@ func TestAppleMDMAuthorization(t *testing.T) { require.NoError(t, err) _, err = svc.GetMDMAppleInstallerDetailsByToken(ctx, "foo") require.NoError(t, err) + _, err = svc.MDMAppleGetEULABytes(ctx, "foo") + require.NoError(t, err) // 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. _, err = svc.NewMDMAppleDEPKeyPair(ctx) diff --git a/server/service/handler.go b/server/service/handler.go index 7df6bd1cd9..32aee8e7ea 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -437,8 +437,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC ue.GET("/api/_version_/fleet/status/live_query", statusLiveQueryEndpoint, nil) // Only Fleet MDM specific endpoints should be within the root /mdm/ path. - // NOTE: remember to update `service.mdmAppleConfigurationRequiredEndpoints` - // when you add an endpoint that's behind the mdmConfiguredMiddleware. + // NOTE: remember to update + // `service.mdmAppleConfigurationRequiredEndpoints` when you add an + // endpoint that's behind the mdmConfiguredMiddleware, this applies + // both to this set of endpoints and to any public/token-authenticated + // endpoints using `neMDM` below in this file. mdmConfiguredMiddleware := mdmconfigured.NewAppleMiddleware(svc) mdm := ue.WithCustomMiddleware(mdmConfiguredMiddleware.Verify()) mdm.POST("/api/_version_/fleet/mdm/apple/enqueue", enqueueMDMAppleCommandEndpoint, enqueueMDMAppleCommandRequest{}) @@ -480,6 +483,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC mdm.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{}) mdm.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil) + mdm.POST("/api/_version_/fleet/mdm/apple/setup/eula", createMDMAppleEULAEndpoint, createMDMAppleEULARequest{}) + mdm.GET("/api/_version_/fleet/mdm/apple/setup/eula/metadata", getMDMAppleEULAMetadataEndpoint, getMDMAppleEULAMetadataRequest{}) + mdm.DELETE("/api/_version_/fleet/mdm/apple/setup/eula/{token}", deleteMDMAppleEULAEndpoint, deleteMDMAppleEULARequest{}) + // the following set of mdm endpoints must always be accessible (even // if MDM is not configured) as it bootstraps the setup of MDM // (generates CSR request for APNs, plus the SCEP and ABM keypairs). @@ -565,11 +572,17 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC POST("/api/osquery/enroll", enrollAgentEndpoint, enrollAgentRequest{}) // These endpoint are token authenticated. + // NOTE: remember to update + // `service.mdmAppleConfigurationRequiredEndpoints` when you add an + // endpoint that's behind the mdmConfiguredMiddleware, this applies + // both to this set of endpoints and to any user authenticated + // endpoints using `mdm.*` above in this file. neMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.Verify()) neMDM.GET(apple_mdm.EnrollPath, mdmAppleEnrollEndpoint, mdmAppleEnrollRequest{}) neMDM.GET(apple_mdm.InstallerPath, mdmAppleGetInstallerEndpoint, mdmAppleGetInstallerRequest{}) neMDM.HEAD(apple_mdm.InstallerPath, mdmAppleHeadInstallerEndpoint, mdmAppleHeadInstallerRequest{}) neMDM.GET("/api/_version_/fleet/mdm/apple/bootstrap", downloadBootstrapPackageEndpoint, downloadBootstrapPackageRequest{}) + neMDM.GET("/api/_version_/fleet/mdm/apple/setup/eula/{token}", getMDMAppleEULAEndpoint, getMDMAppleEULARequest{}) ne.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, EnrollOrbitRequest{}) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 235df73bb2..0bc906759a 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -2972,6 +2972,53 @@ func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() { checkHostAPIs(t, fleet.MDMBootstrapPackageFailed, &team.ID) } +func (s *integrationMDMTestSuite) TestEULA() { + t := s.T() + pdfBytes := []byte("%PDF-1.pdf-contents") + pdfName := "eula.pdf" + + // trying to get metadata about an EULA that hasn't been uploaded yet is an error + metadataResp := getMDMAppleEULAMetadataResponse{} + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/setup/eula/metadata", nil, http.StatusNotFound, &metadataResp) + + // trying to upload a file that is not a PDF fails + s.uploadEULA(&fleet.MDMAppleEULA{Bytes: []byte("should-fail"), Name: "should-fail.pdf"}, http.StatusBadRequest, "") + + // admin is able to upload a new EULA + s.uploadEULA(&fleet.MDMAppleEULA{Bytes: pdfBytes, Name: pdfName}, http.StatusOK, "") + + // get EULA metadata + metadataResp = getMDMAppleEULAMetadataResponse{} + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/setup/eula/metadata", nil, http.StatusOK, &metadataResp) + require.NotEmpty(t, metadataResp.MDMAppleEULA.Token) + require.NotEmpty(t, metadataResp.MDMAppleEULA.CreatedAt) + require.Equal(t, pdfName, metadataResp.MDMAppleEULA.Name) + eulaToken := metadataResp.Token + + // download EULA + resp := s.DoRaw("GET", fmt.Sprintf("/api/latest/fleet/mdm/apple/setup/eula/%s", eulaToken), nil, http.StatusOK) + require.EqualValues(t, len(pdfBytes), resp.ContentLength) + require.Equal(t, "application/pdf", resp.Header.Get("content-type")) + respBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.EqualValues(t, pdfBytes, respBytes) + + // try to download EULA with a bad token + var downloadResp downloadBootstrapPackageResponse + s.DoJSON("GET", "/api/latest/fleet/mdm/apple/setup/eula/bad-token", nil, http.StatusNotFound, &downloadResp) + + // trying to upload any EULA without deleting the previous one first results in an error + s.uploadEULA(&fleet.MDMAppleEULA{Bytes: pdfBytes, Name: "should-fail.pdf"}, http.StatusConflict, "") + + // delete EULA + var deleteResp deleteMDMAppleEULAResponse + s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/apple/setup/eula/%s", eulaToken), nil, http.StatusOK, &deleteResp) + metadataResp = getMDMAppleEULAMetadataResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/apple/setup/eula/%s", eulaToken), nil, http.StatusNotFound, &metadataResp) + // trying to delete again is a bad request + s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/apple/setup/eula/%s", eulaToken), nil, http.StatusNotFound, &deleteResp) +} + func (s *integrationMDMTestSuite) TestMacosSetupAssistant() { ctx := context.Background() t := s.T() @@ -3299,6 +3346,37 @@ func (s *integrationMDMTestSuite) uploadBootstrapPackage( } } +func (s *integrationMDMTestSuite) uploadEULA( + eula *fleet.MDMAppleEULA, + expectedStatus int, + wantErr string, +) { + t := s.T() + + var b bytes.Buffer + w := multipart.NewWriter(&b) + + // add the eula field + fw, err := w.CreateFormFile("eula", eula.Name) + require.NoError(t, err) + _, err = io.Copy(fw, bytes.NewBuffer(eula.Bytes)) + require.NoError(t, err) + w.Close() + + headers := map[string]string{ + "Content-Type": w.FormDataContentType(), + "Accept": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", s.token), + } + + res := s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/setup/eula", b.Bytes(), expectedStatus, headers) + + if wantErr != "" { + errMsg := extractServerErrorText(res.Body) + assert.Contains(t, errMsg, wantErr) + } +} + type device struct { uuid string serial string diff --git a/server/service/mdm.go b/server/service/mdm.go index 2fdd8ff31a..49ec86d892 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -3,12 +3,17 @@ package service import ( "context" "fmt" + "io" + "mime/multipart" "net/http" + "strconv" "strings" "time" + "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" ) @@ -208,3 +213,174 @@ func (svc *Service) VerifyMDMAppleConfigured(ctx context.Context) error { return nil } + +//////////////////////////////////////////////////////////////////////////////// +// POST /mdm/apple/setup/eula +//////////////////////////////////////////////////////////////////////////////// + +type createMDMAppleEULARequest struct { + EULA *multipart.FileHeader +} + +// TODO: We parse the whole body before running svc.authz.Authorize. +// An authenticated but unauthorized user could abuse this. +func (createMDMAppleEULARequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { + 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["eula"] == nil { + return nil, &fleet.BadRequestError{ + Message: "eula multipart field is required", + InternalErr: err, + } + } + + return &createMDMAppleEULARequest{ + EULA: r.MultipartForm.File["eula"][0], + }, nil +} + +type createMDMAppleEULAResponse struct { + Err error `json:"error,omitempty"` +} + +func (r createMDMAppleEULAResponse) error() error { return r.Err } + +func createMDMAppleEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*createMDMAppleEULARequest) + ff, err := req.EULA.Open() + if err != nil { + return createMDMAppleEULAResponse{Err: err}, nil + } + defer ff.Close() + + if err := svc.MDMAppleCreateEULA(ctx, req.EULA.Filename, ff); err != nil { + return createMDMAppleEULAResponse{Err: err}, nil + } + + return createMDMAppleEULAResponse{}, nil +} + +func (svc *Service) MDMAppleCreateEULA(ctx context.Context, name string, file io.ReadSeeker) error { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////////////////////////////////// +// GET /mdm/apple/setup/eula?token={token} +//////////////////////////////////////////////////////////////////////////////// + +type getMDMAppleEULARequest struct { + Token string `url:"token"` +} + +type getMDMAppleEULAResponse struct { + Err error `json:"error,omitempty"` + + // fields used in hijackRender to build the response + eula *fleet.MDMAppleEULA +} + +func (r getMDMAppleEULAResponse) error() error { return r.Err } + +func (r getMDMAppleEULAResponse) hijackRender(ctx context.Context, w http.ResponseWriter) { + w.Header().Set("Content-Length", strconv.Itoa(len(r.eula.Bytes))) + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s.pdf"`, r.eula.Name)) + + // 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.eula.Bytes); err != nil { + logging.WithExtras(ctx, "err", err, "bytes_copied", n) + } +} + +func getMDMAppleEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*getMDMAppleEULARequest) + + eula, err := svc.MDMAppleGetEULABytes(ctx, req.Token) + if err != nil { + return getMDMAppleEULAResponse{Err: err}, nil + } + + return getMDMAppleEULAResponse{eula: eula}, nil +} + +func (svc *Service) MDMAppleGetEULABytes(ctx context.Context, token string) (*fleet.MDMAppleEULA, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////////////////////////////////// +// GET /mdm/apple/setup/eula/{token}/metadata +//////////////////////////////////////////////////////////////////////////////// + +type getMDMAppleEULAMetadataRequest struct{} + +type getMDMAppleEULAMetadataResponse struct { + *fleet.MDMAppleEULA + Err error `json:"error,omitempty"` +} + +func (r getMDMAppleEULAMetadataResponse) error() error { return r.Err } + +func getMDMAppleEULAMetadataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + eula, err := svc.MDMAppleGetEULAMetadata(ctx) + if err != nil { + return getMDMAppleEULAMetadataResponse{Err: err}, nil + } + + return getMDMAppleEULAMetadataResponse{MDMAppleEULA: eula}, nil +} + +func (svc *Service) MDMAppleGetEULAMetadata(ctx context.Context) (*fleet.MDMAppleEULA, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + +//////////////////////////////////////////////////////////////////////////////// +// DELETE /mdm/apple/setup/eula +//////////////////////////////////////////////////////////////////////////////// + +type deleteMDMAppleEULARequest struct { + Token string `url:"token"` +} + +type deleteMDMAppleEULAResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteMDMAppleEULAResponse) error() error { return r.Err } + +func deleteMDMAppleEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*deleteMDMAppleEULARequest) + if err := svc.MDMAppleDeleteEULA(ctx, req.Token); err != nil { + return deleteMDMAppleEULAResponse{Err: err}, nil + } + return deleteMDMAppleEULAResponse{}, nil +} + +func (svc *Service) MDMAppleDeleteEULA(ctx context.Context, token string) error { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return fleet.ErrMissingLicense +} diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 44ef5afd5d..0d1cfea04e 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -578,6 +578,14 @@ func mdmAppleConfigurationRequiredEndpoints() [][2]string { {"GET", "/api/latest/fleet/mdm/apple"}, {"GET", apple_mdm.EnrollPath + "?token=test"}, {"GET", apple_mdm.InstallerPath + "?token=test"}, + {"GET", "/api/latest/fleet/mdm/apple/setup/eula/token"}, + {"DELETE", "/api/latest/fleet/mdm/apple/setup/eula/token"}, + {"GET", "/api/latest/fleet/mdm/apple/setup/eula/metadata"}, + // TODO: this endpoint accepts multipart/form data that gets + // parsed before the MDM check, we need to refactor this + // function to return more information to the caller, or find a + // better way to test these endpoints. + // {"POST", "/api/latest/fleet/mdm/apple/setup/eula"}, {"GET", "/api/latest/fleet/mdm/apple/enrollment_profile"}, {"POST", "/api/latest/fleet/mdm/apple/enrollment_profile"}, {"DELETE", "/api/latest/fleet/mdm/apple/enrollment_profile"},