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:
+10
-1
@@ -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 |
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
@@ -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";
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+67
@@ -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 can’t 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;
|
||||
|
||||
+64
@@ -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;
|
||||
+3
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user