diff --git a/changes/16252-manual-enrollment-prof b/changes/16252-manual-enrollment-prof new file mode 100644 index 0000000000..c977f0ca50 --- /dev/null +++ b/changes/16252-manual-enrollment-prof @@ -0,0 +1,2 @@ +- Adds a new endpoint `GET /api/v1/fleet/mdm/manual_enrollment_profile` that returns the Apple MDM manual + enrollment profile for the organization. \ No newline at end of file diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 6723af5b53..32328ca7d1 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -1074,3 +1074,26 @@ func (svc *Service) mdmWindowsDisableOSUpdates(ctx context.Context, teamID *uint err := svc.ds.DeleteMDMWindowsConfigProfileByTeamAndName(ctx, teamID, mdm.FleetWindowsOSUpdatesProfileName) return ctxerr.Wrap(ctx, err, "delete Windows OS updates profile") } + +func (svc *Service) GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error) { + if err := svc.authz.Authorize(ctx, &fleet.MDMAppleManualEnrollmentProfile{}, fleet.ActionRead); err != nil { + return nil, err + } + + appConfig, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err) + } + + mobileConfig, err := apple_mdm.GenerateEnrollmentProfileMobileconfig( + appConfig.OrgInfo.OrgName, + appConfig.ServerSettings.ServerURL, + svc.config.MDM.AppleSCEPChallenge, + svc.mdmPushCertTopic, + ) + if err != nil { + return nil, ctxerr.Wrap(ctx, err) + } + + return mobileConfig, nil +} diff --git a/server/authz/policy.rego b/server/authz/policy.rego index 44ed0107c8..e68208c287 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -808,6 +808,13 @@ allow { action == write } +# Any logged in user can read the manual enrollment profile data. +allow { + object.type == "mdm_apple_manual_enrollment_profile" + not is_null(subject) + action == read +} + ## # Cron schedules ## diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index c82f89b878..7590071727 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -82,6 +82,15 @@ func (m MDMAppleEnrollmentProfile) AuthzType() string { return "mdm_apple_enrollment_profile" } +// MDMAppleManualEnrollmentProfile is used for authorization checks to get the standard Fleet manual +// enrollment profile. The actual data is returned as raw bytes. +type MDMAppleManualEnrollmentProfile struct{} + +// AuthzType implements authz.AuthzTyper +func (m MDMAppleManualEnrollmentProfile) AuthzType() string { + return "mdm_apple_manual_enrollment_profile" +} + // MDMAppleDEPKeyPair contains the DEP public key certificate and private key pair. Both are PEM encoded. type MDMAppleDEPKeyPair struct { PublicKey []byte `json:"public_key"` diff --git a/server/fleet/service.go b/server/fleet/service.go index fa81887d27..dab1465d92 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -791,6 +791,8 @@ type Service interface { // for MDM macOS migration. TriggerMigrateMDMDevice(ctx context.Context, host *Host) error + GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error) + /////////////////////////////////////////////////////////////////////////////// // CronSchedulesService diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 8b635ab6f1..805886d68f 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2159,6 +2159,30 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet. return apple_mdm.FleetUISSOCallbackPath + "?error=true" } +//////////////////////////////////////////////////////////////////////////////// +// GET /mdm/manual_enrollment_profile +//////////////////////////////////////////////////////////////////////////////// + +type getManualEnrollmentProfileRequest struct{} + +func getManualEnrollmentProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + profile, err := svc.GetMDMManualEnrollmentProfile(ctx) + if err != nil { + return getDeviceMDMManualEnrollProfileResponse{Err: err}, nil + } + + // Using this type to keep code DRY as it already has all the functionality we need. + return getDeviceMDMManualEnrollProfileResponse{Profile: profile}, nil +} + +func (svc *Service) GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + //////////////////////////////////////////////////////////////////////////////// // FileVault-related free version implementation //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 08c9125bd9..c65a9773bd 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -252,6 +252,24 @@ func TestAppleMDMAuthorization(t *testing.T) { _, err = svc.NewMDMAppleDEPKeyPair(ctx) require.NoError(t, err) + // Should work for all user types + for _, user := range []*fleet.User{ + test.UserAdmin, + test.UserMaintainer, + test.UserObserver, + test.UserObserverPlus, + test.UserTeamAdminTeam1, + test.UserTeamGitOpsTeam1, + test.UserGitOps, + test.UserTeamMaintainerTeam1, + test.UserTeamObserverTeam1, + test.UserTeamObserverPlusTeam1, + } { + usrctx := test.UserContext(ctx, user) + _, err = svc.GetMDMManualEnrollmentProfile(usrctx) + require.NoError(t, err) + } + // Must be device-authenticated, should fail _, err = svc.GetDeviceMDMAppleEnrollmentProfile(ctx) checkAuthErr(t, err, true) diff --git a/server/service/handler.go b/server/service/handler.go index c2f026f8e7..08b9c23c9c 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -519,6 +519,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{}) + mdmAppleMW.GET("/api/_version_/fleet/mdm/manual_enrollment_profile", getManualEnrollmentProfileEndpoint, getManualEnrollmentProfileRequest{}) // bootstrap-package routes mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/bootstrap", uploadBootstrapPackageEndpoint, uploadBootstrapPackageRequest{}) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 95ccb14de9..00a9cda198 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -7094,9 +7094,11 @@ func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string for _, p := range profile.PayloadContent { switch p.PayloadType { case "com.apple.security.scep": - require.NotEmpty(t, p.PayloadContent.URL) + require.Equal(t, s.getConfig().ServerSettings.ServerURL+apple_mdm.SCEPPath, p.PayloadContent.URL) + require.Equal(t, s.fleetCfg.MDM.AppleSCEPChallenge, p.PayloadContent.Challenge) case "com.apple.mdm": - require.NotEmpty(t, p.ServerURL) + // Use Contains as the url may have query params + require.Contains(t, p.ServerURL, s.getConfig().ServerSettings.ServerURL+apple_mdm.MDMPath) default: require.Failf(t, "unrecognized payload type in enrollment profile: %s", p.PayloadType) } @@ -11221,3 +11223,7 @@ func (s *integrationMDMTestSuite) TestZCustomConfigurationWebURL() { applyResp = applyTeamSpecsResponse{} s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity, &applyResp) } + +func (s *integrationMDMTestSuite) TestGetManualEnrollmentProfile() { + s.downloadAndVerifyEnrollmentProfile("/api/latest/fleet/mdm/manual_enrollment_profile") +}