diff --git a/changes/23899-IdP-labels b/changes/23899-IdP-labels new file mode 100644 index 0000000000..9b21c78290 --- /dev/null +++ b/changes/23899-IdP-labels @@ -0,0 +1 @@ +- Add support for IdP-based labels diff --git a/frontend/__mocks__/labelsMock.ts b/frontend/__mocks__/labelsMock.ts index 35b703a5ce..145ce11740 100644 --- a/frontend/__mocks__/labelsMock.ts +++ b/frontend/__mocks__/labelsMock.ts @@ -1,5 +1,5 @@ import { ILabel } from "interfaces/label"; -import { IGetLabelResonse } from "services/entities/labels"; +import { IGetLabelResponse } from "services/entities/labels"; const DEFAULT_LABEL_MOCK: ILabel = { created_at: "2024-04-12T13:32:00Z", @@ -14,18 +14,22 @@ const DEFAULT_LABEL_MOCK: ILabel = { display_text: "test macsss", count: 0, host_ids: null, + criteria: { + vital: "end_user_idp_department", + value: " IT admins", + }, }; export const createMockLabel = (overrides?: Partial): ILabel => { return { ...DEFAULT_LABEL_MOCK, ...overrides }; }; -const DEFAULT_GET_LABEL_RESPONSE_MOCK: IGetLabelResonse = { +const DEFAULT_GET_LABEL_RESPONSE_MOCK: IGetLabelResponse = { label: createMockLabel(), }; export const createMockGetLabelResponse = ( - overrides?: Partial -): IGetLabelResonse => { + overrides?: Partial +): IGetLabelResponse => { return { ...DEFAULT_GET_LABEL_RESPONSE_MOCK, ...overrides }; }; diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx index e8be1bff50..cf97b883d6 100644 --- a/frontend/components/LiveQuery/SelectTargets.tsx +++ b/frontend/components/LiveQuery/SelectTargets.tsx @@ -27,14 +27,14 @@ import { capitalize } from "lodash"; import permissions from "utilities/permissions"; import PageError from "components/DataError"; -import TargetsInput from "components/LiveQuery/TargetsInput"; +import TargetsInput from "components/TargetsInput"; +import { generateTableHeaders } from "components/TargetsInput/TargetsInputHostsTableConfig"; import Button from "components/buttons/Button"; import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; import SearchField from "components/forms/fields/SearchField"; import RevealButton from "components/buttons/RevealButton"; import TargetPillSelector from "./TargetChipSelector"; -import { generateTableHeaders } from "./TargetsInput/TargetsInputHostsTableConfig"; interface ISelectTargetsProps { baseClass: string; diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tests.tsx b/frontend/components/TargetsInput/TargetsInput.tests.tsx similarity index 100% rename from frontend/components/LiveQuery/TargetsInput/TargetsInput.tests.tsx rename to frontend/components/TargetsInput/TargetsInput.tests.tsx diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx b/frontend/components/TargetsInput/TargetsInput.tsx similarity index 100% rename from frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx rename to frontend/components/TargetsInput/TargetsInput.tsx diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInputHostsTableConfig.tsx b/frontend/components/TargetsInput/TargetsInputHostsTableConfig.tsx similarity index 100% rename from frontend/components/LiveQuery/TargetsInput/TargetsInputHostsTableConfig.tsx rename to frontend/components/TargetsInput/TargetsInputHostsTableConfig.tsx diff --git a/frontend/components/LiveQuery/TargetsInput/_styles.scss b/frontend/components/TargetsInput/_styles.scss similarity index 100% rename from frontend/components/LiveQuery/TargetsInput/_styles.scss rename to frontend/components/TargetsInput/_styles.scss diff --git a/frontend/components/LiveQuery/TargetsInput/index.ts b/frontend/components/TargetsInput/index.ts similarity index 100% rename from frontend/components/LiveQuery/TargetsInput/index.ts rename to frontend/components/TargetsInput/index.ts diff --git a/frontend/interfaces/label.ts b/frontend/interfaces/label.ts index a8733dbcb4..4672978144 100644 --- a/frontend/interfaces/label.ts +++ b/frontend/interfaces/label.ts @@ -15,7 +15,28 @@ export default PropTypes.shape({ }); export type LabelType = "regular" | "builtin"; -export type LabelMembershipType = "dynamic" | "manual"; +export type LabelMembershipType = "dynamic" | "manual" | "host_vitals"; +export type LabelHostVitalsCriterion = + | "end_user_idp_group" + | "end_user_idp_department"; // for now, may expand to be configurable + +export type LabelLeafCriterion = { + vital: LabelHostVitalsCriterion; + value: string; // from user input +}; + +type LabelAndCriterion = { + and: LabelHostVitalsCriteria[]; +}; + +type LabelOrCriterion = { + or: LabelHostVitalsCriteria[]; +}; + +export type LabelHostVitalsCriteria = + | LabelLeafCriterion + | LabelAndCriterion + | LabelOrCriterion; export interface ILabelSummary { id: number; @@ -43,17 +64,25 @@ export interface ILabel extends ILabelSummary { created_at: string; updated_at: string; uuid?: string; - query: string; - label_membership_type: LabelMembershipType; host_count?: number; // returned for built-in labels but not custom labels display_text: string; count: number; // seems to be a repeat of hosts_count issue #1618 - host_ids: number[] | null; type?: "custom" | "platform" | "status" | "all"; slug?: string; // e.g., "labels/13" | "online" target_type?: string; // e.g., "labels" - platform: string; author_id?: number; + + label_membership_type: LabelMembershipType; + + // dynamic-specific + query: string; // does return '""' for other types + platform: string; // does return '""' for other types + + // host_vitals-specific + criteria: LabelHostVitalsCriteria | null; + + // manual-specific + host_ids: number[] | null; } // corresponding to fleet>server>fleet>labels.go>LabelSpec diff --git a/frontend/pages/admin/IntegrationsPage/cards/IdentityProviders/IdentityProviders.tsx b/frontend/pages/admin/IntegrationsPage/cards/IdentityProviders/IdentityProviders.tsx index 90907413bc..ddd4616c43 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/IdentityProviders/IdentityProviders.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/IdentityProviders/IdentityProviders.tsx @@ -115,7 +115,7 @@ const FailedEndUserInfoCard = ({ const IdentityProviders = () => { const { isPremiumTier } = useContext(AppContext); - const { data, isLoading, isError } = useQuery( + const { data: scimIdPDetails, isLoading, isError } = useQuery( ["scim_details"], () => idpAPI.getSCIMDetails(), { @@ -137,19 +137,21 @@ const IdentityProviders = () => { return ; } - if (!data) return null; + if (!scimIdPDetails) return null; - if (data.last_request === null) { + if (scimIdPDetails.last_request === null) { return ; - } else if (data.last_request.status === "success") { + } else if (scimIdPDetails.last_request.status === "success") { return ( - + ); - } else if (data.last_request.status === "error") { + } else if (scimIdPDetails.last_request.status === "error") { return ( ); } diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index 0a6af19b9a..e7279499bd 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx @@ -169,7 +169,12 @@ const HostsFilterBlock = ({ const renderLabelFilterPill = () => { if (selectedLabel) { - const { description, display_text, label_type } = selectedLabel; + const { + description, + display_text, + label_type, + label_membership_type, + } = selectedLabel; const pillLabel = (isPlatformLabelNameFromAPI(display_text) && PLATFORM_LABEL_DISPLAY_NAMES[display_text]) || @@ -196,13 +201,18 @@ const HostsFilterBlock = ({ !isOnlyObserver && (isOnGlobalTeam || currentUser?.id === selectedLabel.author_id) && ( <> - + { + // TODO - remove condition if/when can edit host_vitals labels + label_membership_type !== "host_vitals" && ( + + ) + } + ) : null + } + // readOnly={isEditing} TODO when extending to handle edits + onLoad={onLoadSQLEditor} + wrapperClassName={`${baseClass}__text-editor-wrapper form-field`} + // helpText={isEditing ? IMMUTABLE_QUERY_HELP_TEXT : ""} TODO when extending to handle edits + wrapEnabled + /> + { + setFormData((prevData) => ({ + ...prevData, + platform: newPlatform, + })); + }} + /> + + ); + + case "host_vitals": + return ( +
+ +

is equal to

+ +
+ ); + + case "manual": + return ( + + ); + default: + return null; + } + }; + + const renderLabelForm = () => ( +
+ + +
+
Type
+ + + +
+ {renderVariableFields()} +
+ + +
+ + ); + return ( <> -

Add label

-

- Dynamic (smart) labels are assigned to hosts if the query returns - results. Manual labels are assigned to selected hosts. -

- - - - {labelSubNav.map((navItem) => { - return ( - - {navItem.name} - - ); - })} - - - - {React.cloneElement(children, { - showOpenSidebarButton, - onOpenSidebar, - onOsqueryTableSelect, - })} +
+

New label

+

+ Create a new label for targeting and filtering hosts. +

+
+ {renderLabelForm()}
- {isDynamicLabel && isSidePanelOpen && ( + {type === "dynamic" && isSidePanelOpen && ( - + - - - + {/* maintaining previous 2 sub-routes for backward-compatibility of URL routes. NewLabelPage now sets the corresponding label type */} + + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index aff9270fcb..d40b425bd0 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -90,8 +90,12 @@ export default { SOFTWARE_ADD_APP_STORE: `${URL_PREFIX}/software/add/app-store`, // Label pages + NEW_LABEL: `${URL_PREFIX}/labels/new`, + // deprecated - now handled by `/new` route LABEL_NEW_DYNAMIC: `${URL_PREFIX}/labels/new/dynamic`, + // deprecated - now handled by `/new` route LABEL_NEW_MANUAL: `${URL_PREFIX}/labels/new/manual`, + LABEL_EDIT: (labelId: number) => `${URL_PREFIX}/labels/${labelId}`, EDIT_PACK: (packId: number): string => { @@ -185,7 +189,6 @@ export default { MANAGE_QUERIES: `${URL_PREFIX}/queries/manage`, MANAGE_SCHEDULE: `${URL_PREFIX}/schedule/manage`, MANAGE_POLICIES: `${URL_PREFIX}/policies/manage`, - NEW_LABEL: `${URL_PREFIX}/labels/new`, NEW_POLICY: `${URL_PREFIX}/policies/new`, NEW_QUERY: `${URL_PREFIX}/queries/new`, RESET_PASSWORD: `${URL_PREFIX}/login/reset`, diff --git a/frontend/services/entities/labels.ts b/frontend/services/entities/labels.ts index f88d9f6fc2..6799601305 100644 --- a/frontend/services/entities/labels.ts +++ b/frontend/services/entities/labels.ts @@ -6,6 +6,7 @@ import { ILabel, ILabelSummary } from "interfaces/label"; import { IDynamicLabelFormData } from "pages/labels/components/DynamicLabelForm/DynamicLabelForm"; import { IManualLabelFormData } from "pages/labels/components/ManualLabelForm/ManualLabelForm"; import { IHost } from "interfaces/host"; +import { INewLabelFormData } from "pages/labels/NewLabelPage/NewLabelPage"; export interface ILabelsResponse { labels: ILabel[]; @@ -19,7 +20,7 @@ export interface ICreateLabelResponse { label: ILabel; } export type IUpdateLabelResponse = ICreateLabelResponse; -export type IGetLabelResonse = ICreateLabelResponse; +export type IGetLabelResponse = ICreateLabelResponse; export interface IGetHostsInLabelResponse { hosts: IHost[]; @@ -31,7 +32,7 @@ const isManualLabelFormData = ( return "targetedHosts" in formData; }; -const generateCreateLabelBody = ( +const generateUpdateLabelBody = ( formData: IDynamicLabelFormData | IManualLabelFormData ) => { // we need to prepare the post body for only manual labels. @@ -45,7 +46,34 @@ const generateCreateLabelBody = ( return formData; }; -const generateUpdateLabelBody = generateCreateLabelBody; +const generateCreateLabelBody = (formData: INewLabelFormData) => { + switch (formData.type) { + case "manual": + return { + name: formData.name, + description: formData.description, + host_ids: formData.targetedHosts.map((host) => host.id), + }; + case "dynamic": + return { + name: formData.name, + description: formData.description, + query: formData.labelQuery, + platform: formData.platform, + }; + case "host_vitals": + return { + name: formData.name, + description: formData.description, + criteria: { + vital: formData.vital, + value: formData.vitalValue, + }, + }; + default: + throw new Error(`Unknown label type: ${formData.type}`); + } +}; /** gets the custom label and returns them in case-insensitive alphabetical * ascending order by label name. (e.g. [A, B, C, a, b, c] => [A, a, B, b, C, c]) @@ -70,12 +98,9 @@ export const getCustomLabels = ( }; export default { - create: ( - formData: IDynamicLabelFormData | IManualLabelFormData - ): Promise => { + create: (formData: INewLabelFormData): Promise => { const { LABELS } = endpoints; - const postBody = generateCreateLabelBody(formData); - return sendRequest("POST", LABELS, postBody); + return sendRequest("POST", LABELS, generateCreateLabelBody(formData)); }, destroy: (label: ILabel) => { @@ -117,7 +142,7 @@ export default { return sendRequest("GET", path); }, - getLabel: (labelId: number): Promise => { + getLabel: (labelId: number): Promise => { const { LABEL } = endpoints; return sendRequest("GET", LABEL(labelId)); },