check disk encryption key from host details page (#9691)

related to https://github.com/fleetdm/fleet/issues/8708

This allows a user to check a disk encryption key for a host on the host
details page.

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Gabriel Hernandez
2023-02-14 17:00:36 +00:00
committed by GitHub
parent 1c44d54454
commit 52d0078bbc
36 changed files with 518 additions and 245 deletions
+10 -1
View File
@@ -5,15 +5,24 @@
"body": [
"import React from \"react\";",
"",
"const baseClass = \"${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}\";",
"",
"interface I${TM_FILENAME_BASE}Props {}",
"",
"const $TM_FILENAME_BASE = ({}: I${TM_FILENAME_BASE}Props) => {",
" return <></>;",
"\treturn <div className={baseClass}></div>;",
"};",
"",
"export default $TM_FILENAME_BASE;",
"",
],
"description": "Creates a React stateless component with the typescrip interface setup"
},
"Fleet - baseClass classname": {
"scope": "typescriptreact,javascriptreact",
"prefix": "bc",
"body": [
"`\\${baseClass}__$0`"
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 B

-25
View File
@@ -161,27 +161,6 @@ describe("Hosts flow", () => {
cy.getAttached(".button--text-link").first().click();
});
});
it("runs query on an existing host", () => {
cy.getAttached(".host-details__action-button-container").within(() => {
cy.getAttached('img[alt="Query host icon"]').click();
});
cy.getAttached(".select-query-modal__modal").within(() => {
cy.getAttached(".modal-query-button").eq(2).click();
});
cy.getAttached(".query-form__button-wrap--new-query").within(() => {
cy.findByText(/run query/i)
.should("exist")
.click();
});
cy.getAttached(".query-page__wrapper").within(() => {
cy.getAttached(".data-table").within(() => {
cy.findByText(hostname).should("exist");
});
cy.findByText(/run/i).click();
});
});
it("renders and searches the host's users", () => {
cy.getAttached(".section--users").within(() => {
cy.getAttached("tbody>tr").should("have.length.greaterThan", 0);
@@ -278,9 +257,5 @@ describe("Hosts flow", () => {
});
}
);
it("deletes an existing host", () => {
hostDetailsPage.allowsDeleteHost();
hostDetailsPage.verifiesDeletedHost(hostname);
});
});
});
-12
View File
@@ -104,24 +104,12 @@ describe(
hostDetailsPage.verifiesTeamsisDisabled();
hostDetailsPage.hidesButton("Transfer");
});
it("allows admin to delete the host", () => {
hostDetailsPage.allowsDeleteHost();
});
it("allows admin to custom query the host", () => {
hostDetailsPage.allowsCustomQueryHost();
});
});
describe("Manage software page", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
manageSoftwarePage.visitManageSoftwarePage();
});
// it(`displays "Vulnerabilities" column`, () => {
// cy.getAttached("thead").within(() => {
// cy.findByText(/vulnerabilities/i).should("exist");
// cy.findByText(/probability of exploit/i).should("not.exist");
// });
// });
it("allows admin to click 'Manage automations' button", () => {
manageSoftwarePage.allowsManageAutomations();
});
@@ -99,12 +99,6 @@ describe(
it("allows maintainer to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
it("allows maintainer to custom query the host", () => {
hostDetailsPage.allowsCustomQueryHost();
});
it("allows maintainer to delete the host", () => {
hostDetailsPage.allowsDeleteHost();
});
});
describe("Manage software page", () => {
beforeEach(() => manageSoftwarePage.visitManageSoftwarePage());
@@ -18,68 +18,6 @@ const hostDetailsPage = {
cy.contains("button", text).should("not.exist");
},
allowsDeleteHost: () => {
cy.findByRole("button", { name: /delete/i }).click();
cy.getAttached(".modal__modal_container").within(() => {
cy.findByRole("button", { name: /delete/i }).should("be.enabled");
});
},
verifiesDeletedHost: (hostname: string) => {
cy.getAttached(".modal__modal_container")
.within(() => {
cy.findByRole("button", { name: /delete/i }).click();
})
.then(() => {
cy.findByText(/add your devices to fleet/i).should("exist");
cy.findByText(/add hosts/i).should("exist");
cy.findByText(/about this host/i).should("not.exist");
cy.findByText(hostname).should("not.exist");
});
},
allowsTransferHost: (create?: boolean) => {
cy.findByRole("button", { name: /transfer/i }).click();
if (create) {
cy.findByText(/create a team/i).should("exist");
} else {
cy.findByText(/create a team/i).should("not.exist");
}
cy.getAttached(".Select-control").click();
cy.getAttached(".Select-menu").within(() => {
cy.findByText(/no team/i).should("exist");
cy.findByText(/oranges/i).should("exist");
cy.findByText(/apples/i).click();
});
cy.getAttached(".transfer-host-modal .modal-cta-wrap")
.contains("button", /transfer/i)
.should("be.enabled");
},
verifiesTransferredHost: () => {
cy.getAttached(".transfer-host-modal .modal-cta-wrap")
.contains("button", /transfer/i)
.click();
cy.findByText(/transferred to apples/i).should("exist");
cy.findByText(/team/i).next().contains("Apples");
},
allowsCustomQueryHost: () => {
cy.findByRole("button", { name: /query/i }).click();
cy.findByRole("button", { name: /create custom query/i }).should(
"be.enabled"
);
cy.getAttached(".modal__ex").within(() => {
cy.findByRole("button").click();
});
},
hidesCustomQueryHost: () => {
cy.findByRole("button", { name: /query/i }).click();
cy.contains("button", /create custom query/i).should("not.exist");
cy.getAttached(".modal__ex").click();
},
allowsCreateOsPolicy: () => {
cy.getAttached(".info-flex").within(() => {
cy.findByText(/ubuntu/i).should("exist");
-10
View File
@@ -386,19 +386,9 @@ describe("Premium tier - Global Admin user", () => {
});
describe("Host details page", () => {
beforeEach(() => hostDetailsPage.visitsHostDetailsPage(1));
it("allows global admin to transfer host to an existing team", () => {
hostDetailsPage.allowsTransferHost("andCreate");
hostDetailsPage.verifiesTransferredHost();
});
it("allows global admin to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
it("allows global admin to custom query a host", () => {
hostDetailsPage.allowsCustomQueryHost();
});
it("allows global admin to delete a host", () => {
hostDetailsPage.allowsDeleteHost();
});
});
describe("Manage software page", () => {
beforeEach(() => {
@@ -82,19 +82,9 @@ describe("Premium tier - Maintainer user", () => {
beforeEach(() => {
hostDetailsPage.visitsHostDetailsPage(1);
});
it("allows global maintainer to transfer host to an existing team", () => {
hostDetailsPage.allowsTransferHost();
hostDetailsPage.verifiesTransferredHost();
});
it("allows global maintainer to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
it("allows global maintainer to custom query a host", () => {
hostDetailsPage.allowsCustomQueryHost();
});
it("allows global maintainer to delete a host", () => {
hostDetailsPage.allowsDeleteHost();
});
});
describe("Manage software page", () => {
beforeEach(() => manageSoftwarePage.visitManageSoftwarePage());
@@ -85,9 +85,6 @@ describe("Premium tier - Observer user", () => {
beforeEach(() => hostDetailsPage.visitsHostDetailsPage(1));
it("should render elements according to role-based access controls", () => {
hostDetailsPage.verifiesTeam("Apples");
hostDetailsPage.hidesButton("Transfer");
hostDetailsPage.hidesButton("Delete");
hostDetailsPage.hidesCustomQueryHost();
hostDetailsPage.hidesCreateOSPolicy();
});
});
@@ -87,12 +87,6 @@ describe("Premium tier - Team Admin user", () => {
it("allows team admin to create an operating system policy", () => {
hostDetailsPage.allowsCreateOsPolicy();
});
it("allows team admin to query host, delete host but not transfer host", () => {
hostDetailsPage.allowsCustomQueryHost();
hostDetailsPage.allowsDeleteHost();
hostDetailsPage.verifiesDeletedHost;
hostDetailsPage.hidesButton("Transfer");
});
});
describe("Manage software page", () => {
beforeEach(() => manageSoftwarePage.visitManageSoftwarePage());
+1
View File
@@ -33,6 +33,7 @@ const DEFAULT_HOST_MOCK: IHost = {
hardware_serial: "",
computer_name: "9b20fc72a247",
mdm: {
encryption_key_available: false,
enrollment_status: "Off",
server_url: "https://www.example.com/1",
},
+1
View File
@@ -3,6 +3,7 @@ import { IMacadminsResponse } from "interfaces/host";
const DEFAULT_MAC_ADMINS_MOCK: IMacadminsResponse = {
macadmins: {
mobile_device_management: {
encryption_key_available: false,
enrollment_status: "On (manual)",
server_url: "https://kandji.com/2",
name: "Kandji",
+1
View File
@@ -15,6 +15,7 @@ export const createMockMdmSolution = (
};
const DEFAULT_HOST_MDM_DATA: IHostMdmData = {
encryption_key_available: false,
enrollment_status: "On (automatic)",
server_url: "http://mdmsolution.com",
name: "MDM Solution",
@@ -98,6 +98,7 @@ const EnrollSecretRow = ({
key={uniqueId()}
data-testid="osquery-secret"
>
{/* TODO: replace with InputFieldHiddenContent component */}
<InputField
disabled
inputWrapperClass={`${baseClass}__secret-input`}
@@ -7,6 +7,7 @@
.form-field {
margin-bottom: 0;
width: 500px; // TODO: configurable from classname in reusable component
}
&__secret-input {
@@ -15,7 +16,6 @@
font-size: $x-small;
font-weight: $bold;
margin-bottom: 0;
width: 500px;
height: 0;
min-height: 0;
}
@@ -0,0 +1,91 @@
import React, { useState } from "react";
import { stringToClipboard } from "utilities/copy_text";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import classnames from "classnames";
const baseClass = "input-field-hidden-content";
interface IInputFieldHiddenContentProps {
value: string;
name?: string;
className?: string;
}
const InputFieldHiddenContent = ({
value,
name,
className,
}: IInputFieldHiddenContentProps) => {
const [copyMessage, setCopyMessage] = useState("");
const [showSecret, setShowSecret] = useState(false);
const classNames = classnames(baseClass, className);
const onCopySecret = (evt: React.MouseEvent) => {
evt.preventDefault();
stringToClipboard(value)
.then(() => setCopyMessage("Copied!"))
.catch(() => setCopyMessage("Copy failed"));
// Clear message after 1 second
setTimeout(() => setCopyMessage(""), 1000);
return false;
};
const onToggleSecret = (evt: React.MouseEvent) => {
evt.preventDefault();
setShowSecret(!showSecret);
return false;
};
const renderLabel = () => {
return (
<span className={`${baseClass}__name`}>
<span className="buttons">
{copyMessage && (
<span
className={`${baseClass}__copy-message`}
>{`${copyMessage} `}</span>
)}
<Button
variant="unstyled"
className={`${baseClass}__copy-secret-icon`}
onClick={onCopySecret}
>
<Icon name="clipboard" />
</Button>
<Button
variant="unstyled"
className={`${baseClass}__show-secret-icon`}
onClick={onToggleSecret}
>
<Icon name="eye" />
</Button>
</span>
</span>
);
};
return (
<div className={classNames}>
<InputField
disabled
inputWrapperClass={`${baseClass}__secret-input`}
name={name}
label={renderLabel()}
type={showSecret ? "text" : "password"}
value={value}
/>
</div>
);
};
export default InputFieldHiddenContent;
@@ -0,0 +1,63 @@
.input-field-hidden-content {
&__secret {
display: flex;
align-items: center;
margin-bottom: $pad-medium;
}
.form-field {
margin-bottom: 0;
}
&__secret-input {
.form-field__label {
position: relative;
font-size: $x-small;
font-weight: $bold;
margin-bottom: 0;
// TODO: figure out width when pulling out to common component
height: 0;
min-height: 0;
}
.input-field {
&--disabled {
letter-spacing: 0;
}
&--password {
letter-spacing: 4px;
}
}
}
&__copy-message {
position: absolute;
right: 65px;
background-color: $ui-light-grey;
border: solid 1px #e2e4ea;
border-radius: 10px;
padding: 2px 6px;
}
.buttons {
display: flex;
align-items: center;
position: absolute;
right: 16px;
top: 12px;
height: 16px;
span {
font-weight: $regular;
}
}
&__show-secret-icon,
&__copy-secret-icon,
&__edit-secret-icon,
&__delete-secret-icon {
padding: 0 $pad-small;
margin-left: $pad-xsmall;
}
}
@@ -0,0 +1 @@
export { default } from "./InputFieldHiddenContent";
+1
View File
@@ -32,6 +32,7 @@ export enum ActivityType {
MdmEnrolled = "mdm_enrolled",
MdmUnenrolled = "mdm_unenrolled",
EditedMacosMinVersion = "edited_macos_min_version",
ReadHostDiskEncryptionKey = "read_host_disk_encryption_key",
}
export interface IActivity {
created_at: string;
+9
View File
@@ -87,6 +87,7 @@ export interface IMunkiData {
}
export interface IHostMdmData {
encryption_key_available: boolean;
enrollment_status: MdmEnrollmentStatus | null;
server_url: string;
id?: number;
@@ -149,6 +150,14 @@ export interface IDeviceUserResponse {
global_config: IDeviceGlobalConfig;
}
export interface IHostEncrpytionKeyResponse {
host_id: number;
encryption_key: {
updated_at: string;
key: string;
};
}
export interface IHost {
created_at: string;
updated_at: string;
@@ -200,6 +200,16 @@ const TAGGED_TEMPLATES = {
);
},
readHostDiskEncryptionKey: (activity: IActivity) => {
return (
<>
{" "}
viewed the disk encryption key for {activity.details?.host_display_name}
.
</>
);
},
defaultActivityTemplate: (activity: IActivity) => {
const entityName = find(activity.details, (_, key) =>
key.includes("_name")
@@ -280,6 +290,9 @@ const getDetail = (
case ActivityType.EditedMacosMinVersion: {
return TAGGED_TEMPLATES.editedMacosMinVersion(activity);
}
case ActivityType.ReadHostDiskEncryptionKey: {
return TAGGED_TEMPLATES.readHostDiskEncryptionKey(activity);
}
default: {
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
}
@@ -0,0 +1,67 @@
import React, { useContext } from "react";
import { MdmEnrollmentStatus } from "interfaces/mdm";
import permissionUtils from "utilities/permissions";
import { AppContext } from "context/app";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import { generateHostActionOptions } from "./helpers";
const baseClass = "host-actions-dropdown";
interface IHostActionsDropdownProps {
onSelect: (value: string) => void;
teamId: number | null;
hostStatus: string;
hostMdmEnrollemntStatus: MdmEnrollmentStatus | null;
doesStoreEncryptionKey?: boolean;
}
const HostActionsDropdown = ({
onSelect,
teamId,
hostStatus,
hostMdmEnrollemntStatus,
doesStoreEncryptionKey,
}: IHostActionsDropdownProps) => {
const {
currentUser,
isPremiumTier = false,
isGlobalAdmin = false,
isGlobalMaintainer = false,
} = useContext(AppContext);
const options = generateHostActionOptions({
isPremiumTier,
isGlobalAdmin,
isGlobalMaintainer,
isTeamAdmin: permissionUtils.isTeamAdmin(currentUser, teamId ?? null),
isTeamMaintainer: permissionUtils.isTeamMaintainer(
currentUser,
teamId ?? null
),
isHostOnline: hostStatus === "online",
isEnrolledInMdm: ["On (automatic)", "On (manual)"].includes(
hostMdmEnrollemntStatus ?? ""
),
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
});
// No options to render. Exit early
if (options.length === 0) return null;
return (
<div className={baseClass}>
<Dropdown
className={`${baseClass}__host-actions-dropdown`}
onChange={onSelect}
placeholder={"Actions"}
searchable={false}
options={options}
/>
</div>
);
};
export default HostActionsDropdown;
@@ -0,0 +1,3 @@
.host-actions-dropdown {
width: 204px
}
@@ -0,0 +1,132 @@
import { IDropdownOption } from "interfaces/dropdownOption";
import { cloneDeep } from "lodash";
const DEFAULT_OPTIONS: IDropdownOption[] = [
{
label: "Transfer",
value: "transfer",
disabled: false,
},
{
label: "Query",
value: "query",
disabled: false,
},
{
label: "Show disk encryption key",
value: "diskEncryption",
disabled: false,
},
{
label: "Turn off MDM",
value: "mdmOff",
disabled: false,
},
{
label: "Delete",
disabled: false,
value: "delete",
},
];
// eslint-disable-next-line import/prefer-default-export
interface IHostActionConfigOptions {
isPremiumTier: boolean;
isGlobalAdmin: boolean;
isGlobalMaintainer: boolean;
isTeamAdmin: boolean;
isTeamMaintainer: boolean;
isHostOnline: boolean;
isEnrolledInMdm: boolean;
doesStoreEncryptionKey: boolean;
}
const canTransferTeam = (config: IHostActionConfigOptions) => {
const { isPremiumTier, isGlobalAdmin, isGlobalMaintainer } = config;
return isPremiumTier && (isGlobalAdmin || isGlobalMaintainer);
};
const canEditMdm = (config: IHostActionConfigOptions) => {
const {
isGlobalAdmin,
isGlobalMaintainer,
isTeamAdmin,
isTeamMaintainer,
isEnrolledInMdm,
} = config;
return (
isEnrolledInMdm &&
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
);
};
const canDeleteHost = (config: IHostActionConfigOptions) => {
const {
isGlobalAdmin,
isGlobalMaintainer,
isTeamAdmin,
isTeamMaintainer,
} = config;
return isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
};
const canShowDiskEncryption = (config: IHostActionConfigOptions) => {
const { isPremiumTier, doesStoreEncryptionKey } = config;
return isPremiumTier && doesStoreEncryptionKey;
};
const filterOutOptions = (
options: IDropdownOption[],
config: IHostActionConfigOptions
) => {
if (!canTransferTeam(config)) {
options = options.filter((option) => option.value !== "transfer");
}
if (!canShowDiskEncryption(config)) {
options = options.filter((option) => option.value !== "diskEncryption");
}
if (!canEditMdm(config)) {
options = options.filter((option) => option.value !== "mdmOff");
}
if (!canDeleteHost(config)) {
options = options.filter((option) => option.value !== "delete");
}
return options;
};
const setOptionsAsDisabled = (
options: IDropdownOption[],
isHostOnline: boolean
) => {
if (!isHostOnline) {
const disableOptions = options.filter(
(option) => option.value === "query" || option.value === "mdmOff"
);
disableOptions.forEach((option) => {
option.disabled = true;
});
}
return options;
};
/**
* Generates the host actions options depending on the configuration. There are
* many variations of the options that are shown/not shown or disabled/enabled
* which are all controlled by the configurations options argument.
*/
// eslint-disable-next-line import/prefer-default-export
export const generateHostActionOptions = (config: IHostActionConfigOptions) => {
// deep clone to always start with a fresh copy of the default options.
let options = cloneDeep(DEFAULT_OPTIONS);
options = filterOutOptions(options, config);
if (options.length === 0) return options;
options = setOptionsAsDisabled(options, config.isHostOnline);
return options;
};
@@ -0,0 +1 @@
export { default } from "./HostActionsDropdown";
@@ -28,12 +28,8 @@ import { IQuery, IFleetQueriesResponse } from "interfaces/query";
import { IQueryStats } from "interfaces/query_stats";
import { ISoftware } from "interfaces/software";
import { ITeam } from "interfaces/team";
import { IUser } from "interfaces/user";
import permissionUtils from "utilities/permissions";
import ReactTooltip from "react-tooltip";
import Spinner from "components/Spinner";
import Button from "components/buttons/Button";
import TabsWrapper from "components/TabsWrapper";
import MainContent from "components/MainContent";
import InfoBanner from "components/InfoBanner";
@@ -63,10 +59,9 @@ import TransferHostModal from "../../components/TransferHostModal";
import DeleteHostModal from "../../components/DeleteHostModal";
import parseOsVersion from "./modals/OSPolicyModal/helpers";
import DeleteIcon from "../../../../../assets/images/icon-action-delete-14x14@2x.png";
import QueryIcon from "../../../../../assets/images/icon-action-query-16x16@2x.png";
import TransferIcon from "../../../../../assets/images/icon-action-transfer-16x16@2x.png";
import CloseIcon from "../../../../../assets/images/icon-action-close-16x15@2x.png";
import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal";
import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
const baseClass = "host-details";
@@ -111,11 +106,9 @@ const HostDetailsPage = ({
const hostIdFromURL = parseInt(host_id, 10);
const {
config,
currentUser,
isGlobalAdmin,
isPremiumTier,
isGlobalAdmin = false,
isPremiumTier = false,
isOnlyObserver,
isGlobalMaintainer,
filteredHostsPath,
} = useContext(AppContext);
const {
@@ -128,17 +121,6 @@ const HostDetailsPage = ({
} = useContext(PolicyContext);
const { renderFlash } = useContext(NotificationContext);
const handlePageError = useErrorHandler();
const canTransferTeam =
isPremiumTier && (isGlobalAdmin || isGlobalMaintainer);
const canDeleteHost = (user: IUser, host: IHost) => {
return (
isGlobalAdmin ||
isGlobalMaintainer ||
permissionUtils.isTeamAdmin(user, host.team_id) ||
permissionUtils.isTeamMaintainer(user, host.team_id)
);
};
const [showDeleteHostModal, setShowDeleteHostModal] = useState(false);
const [showTransferHostModal, setShowTransferHostModal] = useState(false);
@@ -146,6 +128,7 @@ const HostDetailsPage = ({
const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false);
const [showOSPolicyModal, setShowOSPolicyModal] = useState(false);
const [showUnenrollMdmModal, setShowUnenrollMdmModal] = useState(false);
const [showDiskEncryptionModal, setShowDiskEncryptionModal] = useState(false);
const [selectedPolicy, setSelectedPolicy] = useState<IHostPolicy | null>(
null
);
@@ -333,19 +316,6 @@ const HostDetailsPage = ({
}
);
const canEditMdm = (() => {
const userHasPermission =
!!currentUser &&
!!host &&
(isGlobalAdmin ||
isGlobalMaintainer ||
permissionUtils.isTeamMaintainerOrTeamAdmin(currentUser, host.team_id));
const hostEnrolled = ["On (automatic)", "On (manual)"].includes(
host?.mdm.enrollment_status ?? ""
);
return userHasPermission && hostEnrolled;
})();
const featuresConfig = host?.team_id
? teams?.find((t) => t.id === host.team_id)?.features
: config?.features;
@@ -360,7 +330,7 @@ const HostDetailsPage = ({
}) || []
);
});
}, [usersSearchString]);
}, [usersSearchString, host?.users]);
const titleData = normalizeEmptyValues(
pick(host, [
@@ -536,72 +506,40 @@ const HostDetailsPage = ({
[]
);
const onSelectHostAction = (action: string) => {
switch (action) {
case "transfer":
setShowTransferHostModal(true);
break;
case "query":
setShowQueryHostModal(true);
break;
case "diskEncryption":
setShowDiskEncryptionModal(true);
break;
case "mdmOff":
toggleUnenrollMdmModal();
break;
case "delete":
setShowDeleteHostModal(true);
break;
default:
}
};
const renderActionButtons = () => {
const isOnline = host?.status === "online";
if (!host) {
return null;
}
return (
<div className={`${baseClass}__action-button-container`}>
{canTransferTeam && (
<Button
onClick={() => setShowTransferHostModal(true)}
variant="text-icon"
className={`${baseClass}__transfer-button`}
>
<>
Transfer <img src={TransferIcon} alt="Transfer host icon" />
</>
</Button>
)}
<div
data-tip
data-for="query"
data-tip-disable={isOnline}
className={`${!isOnline && "tooltip"}`}
>
<Button
onClick={() => setShowQueryHostModal(true)}
variant="text-icon"
disabled={!isOnline}
className={`${baseClass}__query-button`}
>
<>
Query <img src={QueryIcon} alt="Query host icon" />
</>
</Button>
</div>
<ReactTooltip
place="bottom"
effect="solid"
id="query"
backgroundColor="#3e4771"
>
<span className={`${baseClass}__tooltip-text`}>
You cant query <br /> an offline host.
</span>
</ReactTooltip>
{canEditMdm && !hideEditMdm && (
<Button
onClick={toggleUnenrollMdmModal}
variant="text-icon"
className={`${baseClass}__unenroll-host-from-mdm-button`}
disabled={!isOnline}
>
<>
Turn off MDM{" "}
<img src={CloseIcon} alt="Unenroll host from mdm icon" />
</>
</Button>
)}
{currentUser && host && canDeleteHost(currentUser, host) && (
<Button
onClick={() => setShowDeleteHostModal(true)}
variant="text-icon"
>
<>
Delete <img src={DeleteIcon} alt="Delete host icon" />
</>
</Button>
)}
</div>
<HostActionDropdown
onSelect={onSelectHostAction}
teamId={host.team_id}
hostStatus={host.status}
hostMdmEnrollemntStatus={host.mdm.enrollment_status}
doesStoreEncryptionKey={host.mdm.encryption_key_available}
/>
);
};
@@ -808,6 +746,12 @@ const HostDetailsPage = ({
}}
/>
)}
{showDiskEncryptionModal && host && (
<DiskEncryptionKeyModal
hostId={host.id}
onCancel={() => setShowDiskEncryptionModal(false)}
/>
)}
</div>
</MainContent>
);
@@ -348,12 +348,6 @@
margin: $pad-xxlarge 0 0;
}
&__action-button-container {
display: flex;
align-items: center;
gap: $pad-large;
}
&__device_mapping {
.device_mapping--tooltip {
flex-direction: column;
@@ -0,0 +1,64 @@
import React from "react";
import { useQuery } from "react-query";
import { IHostEncrpytionKeyResponse } from "interfaces/host";
import hostAPI from "services/entities/hosts";
import Modal from "components/Modal";
import CustomLink from "components/CustomLink";
import Button from "components/buttons/Button";
import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent";
import DataError from "components/DataError";
const baseClass = "disk-encryption-key-modal";
interface IDiskEncryptionKeyModal {
hostId: number;
onCancel: () => void;
}
const DiskEncryptionKeyModal = ({
hostId,
onCancel,
}: IDiskEncryptionKeyModal) => {
const { data: encrpytionKey, error: encryptionKeyError } = useQuery<
IHostEncrpytionKeyResponse,
unknown,
string
>("hostEncrpytionKey", () => hostAPI.getEncryptionKey(hostId), {
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
retry: false,
select: (data) => data.encryption_key.key,
});
return (
<Modal title="Disk encryption key" onExit={onCancel} className={baseClass}>
{encryptionKeyError ? (
<DataError />
) : (
<>
<InputFieldHiddenContent value={encrpytionKey ?? ""} />
<p>
The disk encryption key refers to the FileVault recovery key for
macOS.
</p>
<p>
Use this key to log in to the host if you forgot the password.{" "}
<CustomLink
text="View recovery instructions"
url="https://fleetdm.com/docs/using-fleet/mobile-device-management#unlock-a-device-using-the-disk-encryption-key"
newTab
/>
</p>
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Done</Button>
</div>
</>
)}
</Modal>
);
};
export default DiskEncryptionKeyModal;
@@ -0,0 +1,3 @@
.disk-encryption-key-modal {
width: 500px;
}
@@ -0,0 +1 @@
export { default } from "./DiskEncryptionKeyModal";
@@ -28,7 +28,7 @@ interface IHostSummaryProps {
onRefetchHost: (
evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>
) => void;
renderActionButtons: () => JSX.Element;
renderActionButtons: () => JSX.Element | null;
deviceUser?: boolean;
}
@@ -58,7 +58,7 @@ const HostSummary = ({
<Button
className={`
button
${!isOnline ? "refetch-offline tooltip" : ""}
${!isOnline ? "refetch-offline tooltip" : ""}
${showRefetchSpinner ? "refetch-spinner" : "refetch-btn"}
`}
disabled={!isOnline}
+5
View File
@@ -302,4 +302,9 @@ export default {
const fullPath = params !== "" ? `${MDM_SUMMARY}?${params}` : MDM_SUMMARY;
return sendRequest("GET", fullPath);
},
getEncryptionKey: (id: number) => {
const { HOST_ENCRYPTION_KEY } = endpoints;
return sendRequest("GET", HOST_ENCRYPTION_KEY(id));
},
};
+2
View File
@@ -44,6 +44,8 @@ export default {
HOST_MDM: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/mdm`,
HOST_MDM_UNENROLL: (id: number) =>
`/${API_VERSION}/fleet/mdm/hosts/${id}/unenroll`,
HOST_ENCRYPTION_KEY: (id: number) =>
`/${API_VERSION}/fleet/hosts/${id}/encryption_key`,
ME: `/${API_VERSION}/fleet/me`,
OS_VERSIONS: `/${API_VERSION}/fleet/os_versions`,
OSQUERY_OPTIONS: `/${API_VERSION}/fleet/spec/osquery_options`,