add ability for end users to enrol their device into fleet mdm (#21751)
relates to #21559 This adds the ability for end users to enrol their own device in fleet mdm. > NOTE: this new byod HTML page is a separate HTML asset that contains all styles and scripts needed for the page to work. We do not send the fleet UI assets and this drastically cuts down the response time to the users who will be visiting this page on mobile devices There are two sides included in this: **Adding a new add host modal ios and iPad section for IT admins**  **delivering a new byod HTML page to end users that will allow end users to download the config profile to enrol into fleet mdm**  <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
@@ -0,0 +1 @@
|
||||
- add feature for end users to enroll their device into fleet mdm
|
||||
+4
-1
@@ -967,7 +967,7 @@ the way that the Fleet server works.
|
||||
KeyPrefix: "ratelimit::",
|
||||
}
|
||||
|
||||
var apiHandler, frontendHandler http.Handler
|
||||
var apiHandler, frontendHandler, endUserEnrollOTAHandler http.Handler
|
||||
{
|
||||
frontendHandler = service.PrometheusMetricsHandler(
|
||||
"get_frontend",
|
||||
@@ -985,8 +985,10 @@ the way that the Fleet server works.
|
||||
if setupRequired {
|
||||
apiHandler = service.WithSetup(svc, logger, apiHandler)
|
||||
frontendHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix)
|
||||
endUserEnrollOTAHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix)
|
||||
} else {
|
||||
frontendHandler = service.RedirectSetupToLogin(svc, logger, frontendHandler, config.Server.URLPrefix)
|
||||
endUserEnrollOTAHandler = service.ServeEndUserEnrollOTA(config.Server.URLPrefix, logger)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1121,6 +1123,7 @@ the way that the Fleet server works.
|
||||
}
|
||||
apiHandler.ServeHTTP(rw, req)
|
||||
})
|
||||
rootMux.Handle("/enroll", endUserEnrollOTAHandler)
|
||||
rootMux.Handle("/", frontendHandler)
|
||||
|
||||
debugHandler := &debugMux{
|
||||
|
||||
@@ -73,8 +73,9 @@ describe("AddHostsModal", () => {
|
||||
expect(screen.queryByText(/--enable-scripts/i)).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "iOS & iPadOS" }));
|
||||
expect(screen.queryByText(/Apple Business Manager/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Learn more/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Send this to your end users:/i)
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Advanced" }));
|
||||
const advancedText = screen.getByText(/--type=YOUR_TYPE/i);
|
||||
|
||||
@@ -9,7 +9,6 @@ import Modal from "components/Modal";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
import PlatformWrapper from "./PlatformWrapper/PlatformWrapper";
|
||||
import DownloadInstallers from "./DownloadInstallers/DownloadInstallers";
|
||||
|
||||
const baseClass = "add-hosts-modal";
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
|
||||
const generateUrl = (serverUrl: string, enrollSecret: string) => {
|
||||
return `${serverUrl}/enroll?enroll_secret=${enrollSecret}`;
|
||||
};
|
||||
|
||||
const baseClass = "ios-ipados-panel";
|
||||
|
||||
interface IosIpadosPanelProps {
|
||||
enrollSecret: string;
|
||||
}
|
||||
|
||||
const IosIpadosPanel = ({ enrollSecret }: IosIpadosPanelProps) => {
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const helpText =
|
||||
"When the end user navigates to this URL, the enrollment profile " +
|
||||
"will download in their browser. End users will have to install the profile " +
|
||||
"to enroll to Fleet.";
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const url = generateUrl(config.server_settings.server_url, enrollSecret);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<InputField
|
||||
label="Send this to your end users:"
|
||||
enableCopy
|
||||
copyButtonPosition="inside"
|
||||
readOnly
|
||||
inputWrapperClass
|
||||
name="enroll-link"
|
||||
value={url}
|
||||
helpText={helpText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IosIpadosPanel;
|
||||
@@ -0,0 +1,5 @@
|
||||
.ios-ipados-panel {
|
||||
&__spinner {
|
||||
margin: $pad-xxlarge auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./IosIpadosPanel";
|
||||
@@ -19,6 +19,7 @@ import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import CustomLink from "components/CustomLink/CustomLink";
|
||||
|
||||
import { isValidPemCertificate } from "../../../pages/hosts/ManageHostsPage/helpers";
|
||||
import IosIpadosPanel from "./IosIpadosPanel";
|
||||
|
||||
interface IPlatformSubNav {
|
||||
name: string;
|
||||
@@ -324,7 +325,7 @@ const PlatformWrapper = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderTab = (packageType: string) => {
|
||||
const renderPanel = (packageType: string) => {
|
||||
const CHROME_OS_INFO = {
|
||||
extensionId: "fleeedmmihkfkeemmipgmhhjemlljidg",
|
||||
installationUrl: "https://chrome.fleetdm.com/updates.xml",
|
||||
@@ -395,19 +396,7 @@ const PlatformWrapper = ({
|
||||
}
|
||||
|
||||
if (packageType === "ios-ipados") {
|
||||
return (
|
||||
<div className={`${baseClass}__ios-ipados--info`}>
|
||||
<p>
|
||||
Enroll iPhones and iPads by adding them to Fleet in Apple Business
|
||||
Manager (ABM).{" "}
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/learn-more-about/setup-abm"
|
||||
text="Learn more"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
return <IosIpadosPanel enrollSecret={enrollSecret} />;
|
||||
}
|
||||
|
||||
if (packageType === "advanced") {
|
||||
@@ -590,7 +579,7 @@ const PlatformWrapper = ({
|
||||
return (
|
||||
<TabPanel className={`${baseClass}__info`} key={navItem.type}>
|
||||
<div className={`${baseClass} form`}>
|
||||
{renderTab(navItem.type)}
|
||||
{renderPanel(navItem.type)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ class InputField extends Component {
|
||||
PropTypes.object,
|
||||
]),
|
||||
enableCopy: PropTypes.bool,
|
||||
copyButtonPosition: PropTypes.oneOfType(["inside", "outside"]),
|
||||
ignore1password: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -62,6 +63,7 @@ class InputField extends Component {
|
||||
labelTooltipPosition: undefined,
|
||||
helpText: "",
|
||||
enableCopy: false,
|
||||
copyButtonPosition: "outside",
|
||||
ignore1password: false,
|
||||
};
|
||||
|
||||
@@ -97,6 +99,59 @@ class InputField extends Component {
|
||||
return onChange(value);
|
||||
};
|
||||
|
||||
renderCopyButton = () => {
|
||||
const { value, copyButtonPosition } = this.props;
|
||||
|
||||
const copyValue = (e) => {
|
||||
e.preventDefault();
|
||||
stringToClipboard(value).then(() => {
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const copyButtonValue =
|
||||
copyButtonPosition === "outside" ? (
|
||||
<>
|
||||
<Icon name="copy" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
) : (
|
||||
<Icon name="copy" />
|
||||
);
|
||||
|
||||
const wrapperClasses = classnames(
|
||||
`${baseClass}__copy-wrapper`,
|
||||
copyButtonPosition === "outside"
|
||||
? `${baseClass}__copy-wrapper-outside`
|
||||
: `${baseClass}__copy-wrapper-inside`
|
||||
);
|
||||
|
||||
const copiedConfirmationClasses = classnames(
|
||||
`${baseClass}__copied-confirmation`,
|
||||
copyButtonPosition === "outside"
|
||||
? `${baseClass}__copied-confirmation-outside`
|
||||
: `${baseClass}__copied-confirmation-inside`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<Button
|
||||
variant="text-icon"
|
||||
onClick={copyValue}
|
||||
className={`${baseClass}__copy-value-button`}
|
||||
>
|
||||
{copyButtonValue}
|
||||
</Button>
|
||||
{this.state.copied && (
|
||||
<span className={copiedConfirmationClasses}>Copied!</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
readOnly,
|
||||
@@ -113,6 +168,8 @@ class InputField extends Component {
|
||||
blockAutoComplete,
|
||||
value,
|
||||
ignore1password,
|
||||
enableCopy,
|
||||
copyButtonPosition,
|
||||
} = this.props;
|
||||
|
||||
const { onInputChange } = this;
|
||||
@@ -139,16 +196,6 @@ class InputField extends Component {
|
||||
"labelTooltipPosition",
|
||||
]);
|
||||
|
||||
const copyValue = (e) => {
|
||||
e.preventDefault();
|
||||
stringToClipboard(value).then(() => {
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
if (type === "textarea") {
|
||||
return (
|
||||
<FormField
|
||||
@@ -175,7 +222,9 @@ class InputField extends Component {
|
||||
}
|
||||
|
||||
const inputContainerClasses = classnames(`${baseClass}__input-container`, {
|
||||
"copy-enabled": this.props.enableCopy,
|
||||
"copy-enabled": enableCopy,
|
||||
"copy-outside": enableCopy && copyButtonPosition === "outside",
|
||||
"copy-inside": enableCopy && copyButtonPosition === "inside",
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -203,22 +252,8 @@ class InputField extends Component {
|
||||
autoComplete={blockAutoComplete ? "new-password" : ""}
|
||||
data-1p-ignore={ignore1password}
|
||||
/>
|
||||
{this.props.enableCopy && (
|
||||
<div className={`${baseClass}__copy-wrapper`}>
|
||||
<Button
|
||||
variant="text-icon"
|
||||
onClick={copyValue}
|
||||
className={`${baseClass}__copy-value-button`}
|
||||
>
|
||||
<Icon name="copy" /> Copy
|
||||
</Button>
|
||||
{this.state.copied && (
|
||||
<span className={`${baseClass}__copied-confirmation`}>
|
||||
Copied!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enableCopy && this.renderCopyButton()}
|
||||
</div>
|
||||
</FormField>
|
||||
);
|
||||
|
||||
@@ -8,3 +8,16 @@ const meta = {
|
||||
export default meta;
|
||||
|
||||
export const Basic = {};
|
||||
|
||||
export const WithCopyEnabled = {
|
||||
args: {
|
||||
enableCopy: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCopyEnabledInsideInput = {
|
||||
args: {
|
||||
enableCopy: true,
|
||||
copyButtonPosition: "inside",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -86,17 +86,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__copy-wrapper {
|
||||
&__copy-wrapper-outside {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__copy-wrapper-inside {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
|
||||
&__input-container.copy-enabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
&.copy-outside {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&.copy-inside {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
&__copied-confirmation {
|
||||
font-size: $x-small;
|
||||
position: absolute;
|
||||
background-color: $ui-light-grey;
|
||||
border: solid 1px $ui-fleet-black-10;
|
||||
@@ -104,6 +119,13 @@
|
||||
padding: $pad-xxsmall 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&__copied-confirmation-inside {
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
&__copied-confirmation-outside {
|
||||
left: -90px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="shortcut icon" href="{{.URLPrefix}}/assets/favicon.ico" />
|
||||
<title>Fleet</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-weight: 400;
|
||||
src: url("../assets/fonts/inter/Inter-Regular.woff2") format("woff2"),
|
||||
url("../assets/fonts/inter/Inter-Regular.woff") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-weight: 700;
|
||||
src: url("../assets/fonts/inter/Inter-Bold.woff2") format("woff2"),
|
||||
url("../assets/fonts/inter/Inter-Bold.woff") format("woff");
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-family: "Inter", sans-serif;
|
||||
color: #192147;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
color: #fff;
|
||||
background-color: #6a67fe;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #192147;
|
||||
padding: 13px 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
ol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
li > p {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 16px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.profile-downloaded-container,
|
||||
.install-profile-container {
|
||||
width: 100%;
|
||||
background-color: #f9fafc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-downloaded-img,
|
||||
.install-profile-img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.profile-downloaded-img {
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.install-profile-img {
|
||||
max-height: 118px;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<img src="{{.URLPrefix}}/assets/images/fleet-logo.svg" />
|
||||
</header>
|
||||
<section id="main-content">
|
||||
<h1>Enroll your device to Fleet</h1>
|
||||
<p class="page-description">
|
||||
Follow the instructions below to download and install the Fleet profile
|
||||
on your device.
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
<span>1.</span>
|
||||
<span>
|
||||
<b>Download</b> the Fleet profile and select <b>Allow</b> in the
|
||||
pop-up.
|
||||
</span>
|
||||
</p>
|
||||
<a class="download-link" href="{{.EnrollURL}}">Download</a>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<span>2.</span>
|
||||
<span>
|
||||
Navigate to <b>Settings</b> and select <b>Profile Downloaded</b>.
|
||||
</span>
|
||||
</p>
|
||||
<div class="profile-downloaded-container">
|
||||
<img
|
||||
class="profile-downloaded-img"
|
||||
src=""
|
||||
alt="select profile downloaded in settings"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<span>3.</span>
|
||||
<span>Select <b>Install</b>.</span>
|
||||
</p>
|
||||
<div class="install-profile-container">
|
||||
<img class="install-profile-img" src="" alt="select install" />
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<script>
|
||||
const os = navigator.userAgent.includes("iPhone") ? "ios" : "iPadOS";
|
||||
|
||||
const profileDownloadedImg = document.querySelector(
|
||||
".profile-downloaded-img"
|
||||
);
|
||||
const installProfileImg = document.querySelector(".install-profile-img");
|
||||
|
||||
// setting image src based on OS
|
||||
profileDownloadedImg.setAttribute(
|
||||
"src",
|
||||
`{{.URLPrefix}}/assets/images/${os}-profile-downloaded.png`
|
||||
);
|
||||
installProfileImg.setAttribute(
|
||||
"src",
|
||||
`{{.URLPrefix}}/assets/images/${os}-install-profile.png`
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,11 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/fleetdm/fleet/v4/server/bindata"
|
||||
@@ -68,6 +70,69 @@ func ServeFrontend(urlPrefix string, sandbox bool, logger log.Logger) http.Handl
|
||||
})
|
||||
}
|
||||
|
||||
func ServeEndUserEnrollOTA(urlPrefix string, logger log.Logger) http.Handler {
|
||||
herr := func(w http.ResponseWriter, err string) {
|
||||
logger.Log("err", err)
|
||||
http.Error(w, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeBrowserSecurityHeaders(w)
|
||||
|
||||
fs := newBinaryFileSystem("/frontend")
|
||||
file, err := fs.Open("templates/enroll-ota.html")
|
||||
if err != nil {
|
||||
herr(w, "load enroll ota template: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
herr(w, "read bindata file: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t, err := template.New("enroll-ota").Parse(string(data))
|
||||
if err != nil {
|
||||
herr(w, "create react template: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
enrollURL, err := generateEnrollOTAURL(urlPrefix, r.URL.Query().Get("enroll_secret"))
|
||||
if err != nil {
|
||||
herr(w, "generate enroll ota url: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := t.Execute(w, struct {
|
||||
EnrollURL string
|
||||
URLPrefix string
|
||||
}{
|
||||
URLPrefix: urlPrefix,
|
||||
EnrollURL: enrollURL,
|
||||
}); err != nil {
|
||||
herr(w, "execute react template: "+err.Error())
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func generateEnrollOTAURL(fleetURL string, enrollSecret string) (string, error) {
|
||||
path, err := url.JoinPath(fleetURL, "/api/v1/fleet/enrollment_profiles/ota")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating path for end user ota enrollment url: %w", err)
|
||||
}
|
||||
|
||||
enrollURL, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing end user ota enrollment url: %w", err)
|
||||
}
|
||||
|
||||
q := enrollURL.Query()
|
||||
q.Set("enroll_secret", enrollSecret)
|
||||
enrollURL.RawQuery = q.Encode()
|
||||
return enrollURL.String(), nil
|
||||
}
|
||||
|
||||
func ServeStaticAssets(path string) http.Handler {
|
||||
return http.StripPrefix(path, http.FileServer(newBinaryFileSystem("/assets")))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -40,3 +41,29 @@ func TestServeFrontend(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusMethodNotAllowed, response.StatusCode)
|
||||
}
|
||||
|
||||
func TestServeEndUserEnrollOTA(t *testing.T) {
|
||||
if !hasBuildTag("full") {
|
||||
t.Skip("This test requires running with -tags full")
|
||||
}
|
||||
logger := log.NewLogfmtLogger(os.Stdout)
|
||||
h := ServeEndUserEnrollOTA("", logger)
|
||||
ts := httptest.NewServer(h)
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
// assert html is returned
|
||||
response, err := http.DefaultClient.Get(ts.URL + "?enroll_secret=foo")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, response.Header.Get("Content-Type"), "text/html; charset=utf-8")
|
||||
|
||||
// assert it contains the content we expect
|
||||
defer response.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
bodyString := string(bodyBytes)
|
||||
require.Contains(t, bodyString, "Enroll your device to Fleet")
|
||||
require.Contains(t, bodyString, "?enroll_secret=foo")
|
||||
}
|
||||
|
||||
@@ -567,9 +567,6 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
mdmAppleMW.GET("/api/_version_/fleet/mdm/manual_enrollment_profile", getManualEnrollmentProfileEndpoint, getManualEnrollmentProfileRequest{})
|
||||
mdmAppleMW.GET("/api/_version_/fleet/enrollment_profiles/manual", getManualEnrollmentProfileEndpoint, getManualEnrollmentProfileRequest{})
|
||||
|
||||
// Get OTA profile
|
||||
mdmAppleMW.GET("/api/_version_/fleet/enrollment_profiles/ota", getOTAProfileEndpoint, getOTAProfileRequest{})
|
||||
|
||||
// bootstrap-package routes
|
||||
|
||||
// Deprecated: POST /mdm/bootstrap is now deprecated, replaced by the
|
||||
@@ -890,6 +887,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
// Deprecated: GET /mdm/apple/setup/eula/:token is now deprecated, replaced by the platform agnostic /mdm/setup/eula/:token
|
||||
neAppleMDM.GET("/api/_version_/fleet/mdm/apple/setup/eula/{token}", getMDMEULAEndpoint, getMDMEULARequest{})
|
||||
|
||||
// Get OTA profile
|
||||
neAppleMDM.GET("/api/_version_/fleet/enrollment_profiles/ota", getOTAProfileEndpoint, getOTAProfileRequest{})
|
||||
|
||||
// These endpoint are used by Microsoft devices during MDM device enrollment phase
|
||||
neWindowsMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user