diff --git a/public/version.json b/public/version.json index 1194adaf2480..f3db8faafeda 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.7.1" -} \ No newline at end of file + "version": "8.7.2" +} diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx index 11e4b1e67a62..523615e7c659 100644 --- a/src/components/CippComponents/CippCentralSearch.jsx +++ b/src/components/CippComponents/CippCentralSearch.jsx @@ -88,14 +88,6 @@ async function loadTabOptions() { */ function filterItemsByPermissionsAndRoles(items, userPermissions, userRoles) { return items.filter((item) => { - // Check roles if specified - if (item.roles && item.roles.length > 0) { - const hasRole = item.roles.some((requiredRole) => userRoles.includes(requiredRole)); - if (!hasRole) { - return false; - } - } - // Check permissions with pattern matching support if (item.permissions && item.permissions.length > 0) { const hasPermission = userPermissions?.some((userPerm) => { diff --git a/src/layouts/config.js b/src/layouts/config.js index ce0edeb5cdd9..a335b5be498a 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -744,6 +744,7 @@ export const nativeMenuItems = [ "Tenant.Application.*", "Tenant.DomainAnalyser.*", "Exchange.Mailbox.*", + "CIPP.Scheduler.*", ], items: [ { diff --git a/src/layouts/index.js b/src/layouts/index.js index 8508f200f287..e719e62772e8 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -114,14 +114,6 @@ export const Layout = (props) => { const filterItemsByRole = (items) => { return items .map((item) => { - // role - if (item.roles && item.roles.length > 0) { - const hasRole = item.roles.some((requiredRole) => userRoles.includes(requiredRole)); - if (!hasRole) { - return null; - } - } - // Check permission with pattern matching support if (item.permissions && item.permissions.length > 0) { const hasPermission = userPermissions?.some((userPerm) => { diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 9822447c7151..6205f1a8f633 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -1,13 +1,33 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; -import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline"; import { LaptopMac, Sync } from "@mui/icons-material"; import { CippApplicationDeployDrawer } from "/src/components/CippComponents/CippApplicationDeployDrawer"; import { Button, Box } from "@mui/material"; import { useSettings } from "/src/hooks/use-settings.js"; import { useDialog } from "/src/hooks/use-dialog.js"; +const assignmentIntentOptions = [ + { label: "Required", value: "Required" }, + { label: "Available", value: "Available" }, + { label: "Available without enrollment", value: "AvailableWithoutEnrollment" }, + { label: "Uninstall", value: "Uninstall" }, +]; + +const assignmentModeOptions = [ + { label: "Replace existing assignments", value: "replace" }, + { label: "Append to existing assignments", value: "append" }, +]; + +const getAppAssignmentSettingsType = (odataType) => { + if (!odataType || typeof odataType !== "string") { + return undefined; + } + + return odataType.replace("#microsoft.graph.", "").replace(/App$/i, ""); +}; + const Page = () => { const pageTitle = "Applications"; const syncDialog = useDialog(); @@ -22,7 +42,28 @@ const Page = () => { AssignTo: "!AllUsers", ID: "id", }, - confirmText: "Are you sure you want to assign this app to all users?", + fields: [ + { + type: "radio", + name: "Intent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, + ], + confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , color: "info", }, @@ -34,7 +75,28 @@ const Page = () => { AssignTo: "!AllDevices", ID: "id", }, - confirmText: "Are you sure you want to assign this app to all devices?", + fields: [ + { + type: "radio", + name: "Intent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, + ], + confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , color: "info", }, @@ -43,13 +105,104 @@ const Page = () => { type: "POST", url: "/api/ExecAssignApp", data: { - AssignTo: "!Both", + AssignTo: "!AllDevicesAndUsers", ID: "id", }, - confirmText: "Are you sure you want to assign this app to all users and devices?", + fields: [ + { + type: "radio", + name: "Intent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, + ], + confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , color: "info", }, + { + label: "Assign to Custom Group", + type: "POST", + url: "/api/ExecAssignApp", + icon: , + color: "info", + confirmText: 'Select the target groups and intent for "[displayName]".', + fields: [ + { + type: "autoComplete", + name: "groupTargets", + label: "Group(s)", + multiple: true, + creatable: false, + allowResubmit: true, + validators: { required: "Please select at least one group" }, + api: { + url: "/api/ListGraphRequest", + dataKey: "Results", + queryKey: `ListAppAssignmentGroups-${tenant}`, + labelField: (group) => + group.id ? `${group.displayName} (${group.id})` : group.displayName, + valueField: "id", + addedField: { + description: "description", + }, + data: { + Endpoint: "groups", + manualPagination: true, + $select: "id,displayName,description", + $orderby: "displayName", + $top: 999, + $count: true, + }, + }, + }, + { + type: "radio", + name: "assignmentIntent", + label: "Assignment intent", + options: assignmentIntentOptions, + defaultValue: "Required", + validators: { required: "Select an assignment intent" }, + helperText: + "Available assigns to Company Portal, Required installs automatically, Uninstall removes the app, Available without enrollment exposes it without device enrollment.", + }, + { + type: "radio", + name: "assignmentMode", + label: "Assignment mode", + options: assignmentModeOptions, + defaultValue: "replace", + helperText: + "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", + }, + ], + customDataformatter: (row, action, formData) => { + const selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : []; + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + GroupIds: selectedGroups.map((group) => group.value).filter(Boolean), + GroupNames: selectedGroups.map((group) => group.label).filter(Boolean), + Intent: formData?.assignmentIntent || "Required", + AssignmentMode: formData?.assignmentMode || "replace", + AppType: getAppAssignmentSettingsType(row?.["@odata.type"]), + }; + }, + }, { label: "Delete Application", type: "POST", @@ -57,7 +210,7 @@ const Page = () => { data: { ID: "id", }, - confirmText: "Are you sure you want to delete this application?", + confirmText: 'Are you sure you want to delete "[displayName]"?', icon: , color: "danger", }, @@ -83,8 +236,10 @@ const Page = () => { const simpleColumns = [ "displayName", "publishingState", - "installCommandLine", - "uninstallCommandLine", + "isAssigned", + "lastModifiedDateTime", + "createdDateTime", + "applicableDeviceType", ]; return ( diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index e19e38b00bbc..3c86e87632c3 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -64,7 +64,10 @@ const Page = () => { const templateDetails = ApiGetCall({ url: `/api/listStandardTemplates`, - queryKey: `listStandardTemplates-reports`, + data: { + templateId: templateId, + }, + queryKey: `listStandardTemplates-reports-${templateId}`, }); // Normalize template data structure to always work with an array @@ -408,6 +411,92 @@ const Page = () => { } } }); + } else if (standardKey === "GroupTemplate") { + // GroupTemplate structure has groupTemplate array and action array at the top level + const groupTemplates = standardConfig.groupTemplate || []; + const actions = standardConfig.action || []; + const standardId = `standards.GroupTemplate`; + const standardInfo = standards.find((s) => s.name === standardId); + + // Find the tenant's value for this template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + let isCompliant = false; + + // For GroupTemplate, the value is true if compliant + if (directStandardValue === true) { + isCompliant = true; + } else if (currentTenantStandard?.value) { + isCompliant = currentTenantStandard.value === true; + } + + // Build a list of all group names with their types + const groupList = groupTemplates + .map((groupTemplate) => { + const rawGroupType = ( + groupTemplate.rawData?.groupType || "generic" + ).toLowerCase(); + let prettyGroupType = "Generic"; + + if (rawGroupType.includes("dynamicdistribution")) { + prettyGroupType = "Dynamic Distribution Group"; + } else if (rawGroupType.includes("dynamic")) { + prettyGroupType = "Dynamic Security Group"; + } else if (rawGroupType.includes("azurerole")) { + prettyGroupType = "Azure Role-Assignable Group"; + } else if ( + rawGroupType.includes("m365") || + rawGroupType.includes("unified") || + rawGroupType.includes("microsoft") + ) { + prettyGroupType = "Microsoft 365 Group"; + } else if ( + rawGroupType.includes("distribution") || + rawGroupType.includes("mail") + ) { + prettyGroupType = "Distribution Group"; + } else if ( + rawGroupType.includes("security") || + rawGroupType === "mail-enabled security" + ) { + prettyGroupType = "Security Group"; + } else if (rawGroupType.includes("generic")) { + prettyGroupType = "Security Group"; + } + + const groupName = + groupTemplate.label || groupTemplate.rawData?.displayName || "Unknown Group"; + return `- ${groupName} (${prettyGroupType})`; + }) + .join("\n"); + + // Create a single standard entry for all groups + const templateSettings = { + Groups: groupList, + }; + + allStandards.push({ + standardId, + standardName: `Group Templates`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + } + : currentTenantStandard?.value, + standardValue: templateSettings, + complianceStatus: isCompliant ? "Compliant" : "Non-Compliant", + complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: actions, + }); } else { // Regular handling for other standards const standardId = `standards.${standardKey}`; @@ -1129,14 +1218,18 @@ const Page = () => { typeof standard.standardValue === "object" && Object.keys(standard.standardValue).length > 0 ? ( Object.entries(standard.standardValue).map(([key, value]) => ( - + {key}: - + {typeof value === "object" && value !== null ? value?.label || JSON.stringify(value) : value === true