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