add support for displaying EULA during ADE/DEP (#11532)
Related to #11350 and the sub-tasks for stuff that happens in setup assistant: #11477 and #11479 This adds back-end and UI logic to show an EULA during DEP enrollment if one was uploaded via the UI, if an EULA wasn't uploaded, we just proceed to enroll the device right after authentication. https://user-images.githubusercontent.com/4419992/236316655-282ee74a-5f79-4095-a950-82b77b80a5c0.mov
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added support to add an EULA as part of the AEP/DEP unboxing flow.
|
||||
@@ -527,7 +527,8 @@ The MDM endpoints exist to support the related command-line interface sub-comman
|
||||
- [Generate Apple DEP Key Pair](#generate-apple-dep-key-pair)
|
||||
- [Request Certificate Signing Request (CSR)](#request-certificate-signing-request-csr)
|
||||
- [Batch-apply Apple MDM custom settings](#batch-apply-apple-mdm-custom-settings)
|
||||
- [Download an enrollment profile using IdP authentication](#download-an-enrollment-profile-using-idp-authentication)
|
||||
- [Initiate SSO during DEP enrollment](#initiate-sso-during-dep-enrollment)
|
||||
- [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment)
|
||||
|
||||
### Generate Apple DEP Key Pair
|
||||
|
||||
@@ -602,6 +603,61 @@ If no team (id or name) is provided, the profiles are applied for all hosts (for
|
||||
|
||||
`204`
|
||||
|
||||
### Initiate SSO during DEP enrollment
|
||||
|
||||
This endpoint initiates the SSO flow, the response contains an URL that the client can use to redirect the user to initiate the SSO flow in the configured IdP.
|
||||
|
||||
`POST /api/v1/fleet/mdm/sso`
|
||||
|
||||
#### Parameters
|
||||
|
||||
None.
|
||||
|
||||
#### Example
|
||||
|
||||
`POST /api/v1/fleet/mdm/sso`
|
||||
|
||||
##### Default response
|
||||
|
||||
```
|
||||
{
|
||||
"url": "https://idp-provider.com/saml?SAMLRequest=...",
|
||||
}
|
||||
```
|
||||
|
||||
### Complete SSO during DEP enrollment
|
||||
|
||||
This is the callback endpoint that the identity provider will use to send security assertions to Fleet. This is where Fleet receives and processes the response from the identify provider.
|
||||
|
||||
`POST /api/v1/fleet/mdm/sso/callback`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ------------ | ------ | ---- | ----------------------------------------------------------- |
|
||||
| SAMLResponse | string | body | **Required**. The SAML response from the identity provider. |
|
||||
|
||||
#### Example
|
||||
|
||||
`POST /api/v1/fleet/mdm/sso/callback`
|
||||
|
||||
##### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"SAMLResponse": "<SAML response from IdP>"
|
||||
}
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 302`
|
||||
|
||||
If the credentials are valid, the server redirects the client to the Fleet UI. The URL contains the following query parameters that can be used to complete the DEP enrollment flow:
|
||||
|
||||
- `profile_token` is a token that can be used to download an enrollment profile (.mobileconfig).
|
||||
- `eula_token` (optional) if an EULA was uploaded, this contains a token that can be used to view the EULA document.
|
||||
|
||||
## Get or apply configuration files
|
||||
|
||||
These API routes are used by the `fleetctl` CLI tool. Users can manage Fleet with `fleetctl` and [configuration files in YAML syntax](https://fleetdm.com/docs/using-fleet/configuration-files/).
|
||||
|
||||
+50
-26
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
@@ -508,7 +509,7 @@ func (svc *Service) InitiateMDMAppleSSO(ctx context.Context) (string, error) {
|
||||
|
||||
}
|
||||
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) ([]byte, error) {
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) (string, error) {
|
||||
// skipauth: User context does not yet exist. Unauthenticated users may
|
||||
// hit the SSO callback.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
@@ -517,12 +518,12 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
|
||||
|
||||
appConfig, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get config for sso")
|
||||
return "", ctxerr.Wrap(ctx, err, "get config for sso")
|
||||
}
|
||||
|
||||
_, metadata, err := svc.ssoSessionStore.Fullfill(auth.RequestID())
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validate request in session")
|
||||
return "", ctxerr.Wrap(ctx, err, "validate request in session")
|
||||
}
|
||||
|
||||
err = sso.ValidateAudiences(
|
||||
@@ -534,35 +535,35 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating sso response")
|
||||
return "", ctxerr.Wrap(ctx, err, "validating sso response")
|
||||
}
|
||||
|
||||
return apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
||||
appConfig.OrgInfo.OrgName,
|
||||
appConfig.ServerSettings.ServerURL,
|
||||
svc.config.MDM.AppleSCEPChallenge,
|
||||
svc.mdmPushCertTopic,
|
||||
)
|
||||
eula, err := svc.ds.MDMAppleGetEULAMetadata(ctx)
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return "", ctxerr.Wrap(ctx, err, "getting EULA metadata")
|
||||
}
|
||||
|
||||
depProf, err := svc.getAutomaticEnrollmentProfile(ctx)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
}
|
||||
|
||||
if depProf == nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "missing profile")
|
||||
}
|
||||
|
||||
q := url.Values{"profile_token": {depProf.Token}}
|
||||
if eula != nil {
|
||||
q.Add("eula_token", eula.Token)
|
||||
}
|
||||
|
||||
return appConfig.ServerSettings.ServerURL + "/mdm/sso/callback?" + q.Encode(), nil
|
||||
}
|
||||
|
||||
func (svc *Service) mdmAppleSyncDEPProfile(ctx context.Context) error {
|
||||
profiles, err := svc.ds.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
depProf, err := svc.getAutomaticEnrollmentProfile(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
}
|
||||
|
||||
// Grab the first automatic enrollment profile we find, the current
|
||||
// behavior is that the last enrollment profile that was uploaded is
|
||||
// the one assigned to newly enrolled devices.
|
||||
//
|
||||
// TODO: this will change after #10995 where there can be a DEP profile
|
||||
// per team.
|
||||
var depProf *fleet.MDMAppleEnrollmentProfile
|
||||
for _, prof := range profiles {
|
||||
if prof.Type == "automatic" {
|
||||
depProf = prof
|
||||
break
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "fetching enrollment profile")
|
||||
}
|
||||
|
||||
if depProf == nil {
|
||||
@@ -586,3 +587,26 @@ func (svc *Service) mdmAppleSyncDEPProfile(ctx context.Context) error {
|
||||
|
||||
return svc.depService.RegisterProfileWithAppleDEPServer(ctx, jsonProf, enrollURL)
|
||||
}
|
||||
|
||||
func (svc *Service) getAutomaticEnrollmentProfile(ctx context.Context) (*fleet.MDMAppleEnrollmentProfile, error) {
|
||||
profiles, err := svc.ds.ListMDMAppleEnrollmentProfiles(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing profiles")
|
||||
}
|
||||
|
||||
// Grab the first automatic enrollment profile we find, the current
|
||||
// behavior is that the last enrollment profile that was uploaded is
|
||||
// the one assigned to newly enrolled devices.
|
||||
//
|
||||
// TODO: this will change after #10995 where there can be a DEP profile
|
||||
// per team.
|
||||
var depProf *fleet.MDMAppleEnrollmentProfile
|
||||
for _, prof := range profiles {
|
||||
if prof.Type == "automatic" {
|
||||
depProf = prof
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return depProf, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import DataError from "components/DataError";
|
||||
|
||||
const baseClass = "mdm-sso-error";
|
||||
|
||||
interface ISSOErrorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SSOError = ({ className }: ISSOErrorProps) => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
return (
|
||||
<DataError className={classNames}>
|
||||
<p>Please contact your IT admin at +1-(415)-651-2575.</p>
|
||||
</DataError>
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOError;
|
||||
@@ -0,0 +1,6 @@
|
||||
.mdm-sso-error {
|
||||
p {
|
||||
font-size: $x-small;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SSOError";
|
||||
@@ -30,6 +30,7 @@ export default {
|
||||
"unstyled-modal-query",
|
||||
"contextual-nav-item",
|
||||
"small-text-icon",
|
||||
"oversized",
|
||||
],
|
||||
control: "select",
|
||||
},
|
||||
|
||||
@@ -21,7 +21,8 @@ export type ButtonVariant =
|
||||
| "unstyled"
|
||||
| "unstyled-modal-query"
|
||||
| "contextual-nav-item"
|
||||
| "small-text-icon";
|
||||
| "small-text-icon"
|
||||
| "oversized";
|
||||
|
||||
export interface IButtonProps {
|
||||
autofocus?: boolean;
|
||||
|
||||
@@ -377,4 +377,11 @@ $base-class: "button";
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--oversized {
|
||||
background-color: $core-fleet-black;
|
||||
padding: $pad-large $pad-small;
|
||||
font-size: $medium;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useState } from "react";
|
||||
import { WithRouterProps } from "react-router";
|
||||
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
import Spinner from "components/Spinner/Spinner";
|
||||
import SSOError from "components/MDM/SSOError";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
const baseClass = "mdm-apple-sso-callback-page";
|
||||
|
||||
const RedirectTo = ({ url }: { url: string }) => {
|
||||
window.location.href = url;
|
||||
return <Spinner />;
|
||||
};
|
||||
|
||||
interface IEnrollmentGateProps {
|
||||
profileToken?: string;
|
||||
eulaToken?: string;
|
||||
}
|
||||
|
||||
const EnrollmentGate = ({ profileToken, eulaToken }: IEnrollmentGateProps) => {
|
||||
const [showEULA, setShowEULA] = useState(Boolean(eulaToken));
|
||||
|
||||
if (!profileToken) {
|
||||
return <SSOError />;
|
||||
}
|
||||
|
||||
if (showEULA && eulaToken) {
|
||||
return (
|
||||
<div className={`${baseClass}__eula-wrapper`}>
|
||||
<h3>Terms and conditions</h3>
|
||||
<iframe
|
||||
src={`/api/${endpoints.MDM_APPLE_EULA_FILE(eulaToken)}`}
|
||||
width="100%"
|
||||
title="eula"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowEULA(false)}
|
||||
variant="oversized"
|
||||
className={`${baseClass}__agree-btn`}
|
||||
>
|
||||
Agree and continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RedirectTo url={endpoints.MDM_APPLE_ENROLLMENT_PROFILE(profileToken)} />
|
||||
);
|
||||
};
|
||||
|
||||
interface IMDMSSOCallbackQuery {
|
||||
eula_token?: string;
|
||||
profile_token?: string;
|
||||
}
|
||||
|
||||
const MDMAppleSSOCallbackPage = (
|
||||
props: WithRouterProps<object, IMDMSSOCallbackQuery>
|
||||
) => {
|
||||
const { eula_token, profile_token } = props.location.query;
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<EnrollmentGate eulaToken={eula_token} profileToken={profile_token} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MDMAppleSSOCallbackPage;
|
||||
@@ -0,0 +1,20 @@
|
||||
.mdm-apple-sso-callback-page {
|
||||
height: 100vh; // expend height to make entire viewport a white background
|
||||
background-color: $core-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__eula-wrapper {
|
||||
width: 80vw;
|
||||
text-align: center;
|
||||
|
||||
iframe {
|
||||
height: 65vh;
|
||||
}
|
||||
}
|
||||
|
||||
&__agree-btn {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MDMAppleSSOCallbackPage";
|
||||
@@ -4,20 +4,12 @@ import { AxiosError } from "axios";
|
||||
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import DataError from "components/DataError";
|
||||
import SSOError from "components/MDM/SSOError";
|
||||
import Spinner from "components/Spinner/Spinner";
|
||||
import { IMdmSSOReponse } from "interfaces/mdm";
|
||||
|
||||
const baseClass = "mdm-apple-sso-page";
|
||||
|
||||
const SSOError = () => {
|
||||
return (
|
||||
<DataError className={`${baseClass}__sso-error`}>
|
||||
<p>Please contact your IT admin at +1-(415)-651-2575.</p>
|
||||
</DataError>
|
||||
);
|
||||
};
|
||||
|
||||
const DEPSSOLoginPage = () => {
|
||||
const { error } = useQuery<void, AxiosError, IMdmSSOReponse>(
|
||||
["dep_sso"],
|
||||
|
||||
@@ -4,11 +4,4 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__sso-error {
|
||||
p {
|
||||
font-size: $x-small;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import QueryPage from "pages/queries/QueryPage";
|
||||
import RegistrationPage from "pages/RegistrationPage";
|
||||
import ResetPasswordPage from "pages/ResetPasswordPage";
|
||||
import MDMAppleSSOPage from "pages/MDMAppleSSOPage";
|
||||
import MDMAppleSSOCallbackPage from "pages/MDMAppleSSOCallbackPage";
|
||||
import SoftwareDetailsPage from "pages/software/SoftwareDetailsPage";
|
||||
import ApiOnlyUser from "pages/ApiOnlyUser";
|
||||
import Fleet403 from "pages/errors/Fleet403";
|
||||
@@ -98,6 +99,7 @@ const routes = (
|
||||
/>
|
||||
<Route path="login/forgot" component={ForgotPasswordPage} />
|
||||
<Route path="login/reset" component={ResetPasswordPage} />
|
||||
<Route path="mdm/sso/callback" component={MDMAppleSSOCallbackPage} />
|
||||
<Route path="mdm/sso" component={MDMAppleSSOPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -48,6 +48,10 @@ export default {
|
||||
MDM_PROFILES_AGGREGATE_STATUSES: `/${API_VERSION}/fleet/mdm/apple/profiles/summary`,
|
||||
MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: `/${API_VERSION}/fleet/mdm/apple/filevault/summary`,
|
||||
MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`,
|
||||
MDM_APPLE_EULA_FILE: (token: string) =>
|
||||
`/${API_VERSION}/fleet/mdm/apple/setup/eula/${token}`,
|
||||
MDM_APPLE_ENROLLMENT_PROFILE: (token: string) =>
|
||||
`/api/mdm/apple/enroll?token=${token}`,
|
||||
MDM_BOOTSTRAP_PACKAGE_METADATA: (teamId: number) =>
|
||||
`/${API_VERSION}/fleet/mdm/apple/bootstrap/${teamId}/metadata`,
|
||||
MDM_BOOTSTRAP_PACKAGE: `/${API_VERSION}/fleet/mdm/apple/bootstrap`,
|
||||
|
||||
@@ -168,8 +168,7 @@ type Service interface {
|
||||
|
||||
// InitSSOCallback handles the IDP response and ensures the credentials
|
||||
// are valid, then responds with an enrollment profile.
|
||||
// TODO: add support for EULAs too
|
||||
InitiateMDMAppleSSOCallback(ctx context.Context, auth Auth) ([]byte, error)
|
||||
InitiateMDMAppleSSOCallback(ctx context.Context, auth Auth) (string, error)
|
||||
|
||||
// GetSSOUser handles retrieval of an user that is trying to authenticate
|
||||
// via SSO
|
||||
|
||||
+12
-21
@@ -1161,6 +1161,8 @@ type mdmAppleEnrollResponse struct {
|
||||
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")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Disposition", "attachment;fleet-enrollment-profile.mobileconfig")
|
||||
|
||||
// OK to just log the error here as writing anything on
|
||||
// `http.ResponseWriter` sets the status code to 200 (and it can't be
|
||||
@@ -2112,44 +2114,33 @@ func (callbackMDMAppleSSORequest) DecodeRequest(ctx context.Context, r *http.Req
|
||||
type callbackMDMAppleSSOResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
|
||||
// used in hijackRender for the response
|
||||
profile []byte
|
||||
redirectURL string
|
||||
}
|
||||
|
||||
func (r callbackMDMAppleSSOResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
|
||||
w.Header().Set("Location", r.redirectURL)
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (r callbackMDMAppleSSOResponse) error() error { return r.Err }
|
||||
|
||||
func (r callbackMDMAppleSSOResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.profile)), 10))
|
||||
w.Header().Set("Content-Type", "application/x-apple-aspen-config")
|
||||
w.Header().Add("Content-Disposition", `attachment; filename="fleet-mdm-enrollment-profile.mobileconfig"`)
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// OK to just log the error here as writing anything on
|
||||
// `http.ResponseWriter` sets the status code to 200 (and it can't be
|
||||
// changed.) Clients should rely on matching content-length with the
|
||||
// header provided.
|
||||
if n, err := w.Write(r.profile); err != nil {
|
||||
logging.WithExtras(ctx, "err", err, "written", n)
|
||||
}
|
||||
}
|
||||
|
||||
func callbackMDMAppleSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
auth := request.(fleet.Auth)
|
||||
|
||||
// validate that the SSO response is valid
|
||||
profile, err := svc.InitiateMDMAppleSSOCallback(ctx, auth)
|
||||
redirectURL, err := svc.InitiateMDMAppleSSOCallback(ctx, auth)
|
||||
if err != nil {
|
||||
return callbackMDMAppleSSOResponse{Err: err}, nil
|
||||
}
|
||||
return callbackMDMAppleSSOResponse{profile: profile}, nil
|
||||
return callbackMDMAppleSSOResponse{redirectURL: redirectURL}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) ([]byte, error) {
|
||||
func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet.Auth) (string, error) {
|
||||
// skipauth: No authorization check needed due to implementation
|
||||
// returning only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
return "", fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -611,25 +612,7 @@ func (s *integrationMDMTestSuite) TestDeviceMDMManualEnroll() {
|
||||
s.DoRaw("GET", "/api/latest/fleet/device/invalid_token/mdm/apple/manual_enrollment_profile", nil, http.StatusUnauthorized)
|
||||
|
||||
// valid token downloads the profile
|
||||
resp := s.DoRaw("GET", "/api/latest/fleet/device/"+token+"/mdm/apple/manual_enrollment_profile", nil, http.StatusOK)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, resp.Header, "Content-Disposition")
|
||||
require.Contains(t, resp.Header, "Content-Type")
|
||||
require.Contains(t, resp.Header, "X-Content-Type-Options")
|
||||
require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
|
||||
require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
|
||||
require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
|
||||
headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(body), headerLen)
|
||||
|
||||
var profile struct {
|
||||
PayloadIdentifier string `plist:"PayloadIdentifier"`
|
||||
}
|
||||
require.NoError(t, plist.Unmarshal(body, &profile))
|
||||
require.Equal(t, apple_mdm.FleetPayloadIdentifier, profile.PayloadIdentifier)
|
||||
s.downloadAndVerifyEnrollmentProfile("/api/latest/fleet/device/" + token + "/mdm/apple/manual_enrollment_profile")
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
|
||||
@@ -3875,25 +3858,41 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
||||
require.Equal(t, acResp.ServerSettings.ServerURL+"/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)
|
||||
|
||||
res := s.LoginMDMSSOUser("sso_user", "user123#")
|
||||
require.NotEmpty(t, res.Header.Get("Location"))
|
||||
require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
u, err := url.Parse(res.Header.Get("Location"))
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Contains(t, res.Header, "Content-Disposition")
|
||||
require.Contains(t, res.Header, "Content-Type")
|
||||
require.Contains(t, res.Header, "X-Content-Type-Options")
|
||||
require.Contains(t, res.Header.Get("Content-Disposition"), "attachment;")
|
||||
require.Contains(t, res.Header.Get("Content-Type"), "application/x-apple-aspen-config")
|
||||
require.Contains(t, res.Header.Get("X-Content-Type-Options"), "nosniff")
|
||||
headerLen, err := strconv.Atoi(res.Header.Get("Content-Length"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(body), headerLen)
|
||||
q := u.Query()
|
||||
// without an EULA uploaded, only the profile token is provided
|
||||
require.False(t, q.Has("eula_token"))
|
||||
require.True(t, q.Has("profile_token"))
|
||||
// the url retrieves a valid profile
|
||||
s.downloadAndVerifyEnrollmentProfile("/api/mdm/apple/enroll?token=" + q.Get("profile_token"))
|
||||
|
||||
var profile struct {
|
||||
PayloadIdentifier string `plist:"PayloadIdentifier"`
|
||||
}
|
||||
require.NoError(t, plist.Unmarshal(body, &profile))
|
||||
require.Equal(t, apple_mdm.FleetPayloadIdentifier, profile.PayloadIdentifier)
|
||||
// upload an EULA
|
||||
pdfBytes := []byte("%PDF-1.pdf-contents")
|
||||
pdfName := "eula.pdf"
|
||||
s.uploadEULA(&fleet.MDMAppleEULA{Bytes: pdfBytes, Name: pdfName}, http.StatusOK, "")
|
||||
|
||||
res = s.LoginMDMSSOUser("sso_user", "user123#")
|
||||
require.NotEmpty(t, res.Header.Get("Location"))
|
||||
require.Equal(t, http.StatusTemporaryRedirect, res.StatusCode)
|
||||
u, err = url.Parse(res.Header.Get("Location"))
|
||||
require.NoError(t, err)
|
||||
q = u.Query()
|
||||
// with an EULA uploaded, both values are present
|
||||
require.True(t, q.Has("eula_token"))
|
||||
require.True(t, q.Has("profile_token"))
|
||||
// the url retrieves a valid profile
|
||||
s.downloadAndVerifyEnrollmentProfile("/api/mdm/apple/enroll?token=" + q.Get("profile_token"))
|
||||
// the url retrieves a valid EULA
|
||||
resp := s.DoRaw("GET", "/api/latest/fleet/mdm/apple/setup/eula/"+q.Get("eula_token"), 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)
|
||||
|
||||
// changing the server URL also updates the remote DEP profile
|
||||
acResp = appConfigResponse{}
|
||||
@@ -3903,3 +3902,26 @@ func (s *integrationMDMTestSuite) TestSSO() {
|
||||
require.Contains(t, lastSubmittedProfile.URL, "https://example.com/api/mdm/apple/enroll?token=")
|
||||
require.Equal(t, "https://example.com/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string) {
|
||||
t := s.T()
|
||||
|
||||
resp := s.DoRaw("GET", path, nil, http.StatusOK)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, resp.Header, "Content-Disposition")
|
||||
require.Contains(t, resp.Header, "Content-Type")
|
||||
require.Contains(t, resp.Header, "X-Content-Type-Options")
|
||||
require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
|
||||
require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
|
||||
require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
|
||||
headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(body), headerLen)
|
||||
var profile struct {
|
||||
PayloadIdentifier string `plist:"PayloadIdentifier"`
|
||||
}
|
||||
require.NoError(t, plist.Unmarshal(body, &profile))
|
||||
require.Equal(t, apple_mdm.FleetPayloadIdentifier, profile.PayloadIdentifier)
|
||||
}
|
||||
|
||||
@@ -295,7 +295,6 @@ func (r getMDMAppleEULAResponse) hijackRender(ctx context.Context, w http.Respon
|
||||
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
|
||||
|
||||
@@ -280,7 +280,7 @@ func (ts *withServer) getConfig() *appConfigResponse {
|
||||
|
||||
func (ts *withServer) LoginSSOUser(username, password string) (fleet.Auth, string) {
|
||||
t := ts.s.T()
|
||||
auth, res := ts.loginSSOUser(username, password, "/api/v1/fleet/sso")
|
||||
auth, res := ts.loginSSOUser(username, password, "/api/v1/fleet/sso", http.StatusOK)
|
||||
defer res.Body.Close()
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
@@ -288,11 +288,11 @@ func (ts *withServer) LoginSSOUser(username, password string) (fleet.Auth, strin
|
||||
}
|
||||
|
||||
func (ts *withServer) LoginMDMSSOUser(username, password string) *http.Response {
|
||||
_, res := ts.loginSSOUser(username, password, "/api/v1/fleet/mdm/sso")
|
||||
_, res := ts.loginSSOUser(username, password, "/api/v1/fleet/mdm/sso", http.StatusTemporaryRedirect)
|
||||
return res
|
||||
}
|
||||
|
||||
func (ts *withServer) loginSSOUser(username, password string, basePath string) (fleet.Auth, *http.Response) {
|
||||
func (ts *withServer) loginSSOUser(username, password string, basePath string, callbackStatus int) (fleet.Auth, *http.Response) {
|
||||
t := ts.s.T()
|
||||
|
||||
if _, ok := os.LookupEnv("SAML_IDP_TEST"); !ok {
|
||||
@@ -338,7 +338,7 @@ func (ts *withServer) loginSSOUser(username, password string, basePath string) (
|
||||
auth, err := sso.DecodeAuthResponse(rawSSOResp)
|
||||
require.NoError(t, err)
|
||||
q := url.QueryEscape(rawSSOResp)
|
||||
res := ts.DoRawNoAuth("POST", basePath+"/callback?SAMLResponse="+q, nil, http.StatusOK)
|
||||
res := ts.DoRawNoAuth("POST", basePath+"/callback?SAMLResponse="+q, nil, callbackStatus)
|
||||
|
||||
return auth, res
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user