Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions public/version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "8.7.1"
}
"version": "8.7.2"
}
8 changes: 0 additions & 8 deletions src/components/CippComponents/CippCentralSearch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions src/layouts/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ export const nativeMenuItems = [
"Tenant.Application.*",
"Tenant.DomainAnalyser.*",
"Exchange.Mailbox.*",
"CIPP.Scheduler.*",
],
items: [
{
Expand Down
8 changes: 0 additions & 8 deletions src/layouts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
171 changes: 163 additions & 8 deletions src/pages/endpoint/applications/list/index.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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: <UserIcon />,
color: "info",
},
Expand All @@ -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: <LaptopMac />,
color: "info",
},
Expand All @@ -43,21 +105,112 @@ 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: <GlobeAltIcon />,
color: "info",
},
{
label: "Assign to Custom Group",
type: "POST",
url: "/api/ExecAssignApp",
icon: <UserGroupIcon />,
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",
url: "/api/RemoveApp",
data: {
ID: "id",
},
confirmText: "Are you sure you want to delete this application?",
confirmText: 'Are you sure you want to delete "[displayName]"?',
icon: <TrashIcon />,
color: "danger",
},
Expand All @@ -83,8 +236,10 @@ const Page = () => {
const simpleColumns = [
"displayName",
"publishingState",
"installCommandLine",
"uninstallCommandLine",
"isAssigned",
"lastModifiedDateTime",
"createdDateTime",
"applicableDeviceType",
];

return (
Expand Down
99 changes: 96 additions & 3 deletions src/pages/tenant/manage/applied-standards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`;
Expand Down Expand Up @@ -1129,14 +1218,18 @@ const Page = () => {
typeof standard.standardValue === "object" &&
Object.keys(standard.standardValue).length > 0 ? (
Object.entries(standard.standardValue).map(([key, value]) => (
<Box key={key} sx={{ display: "flex", mb: 0.5 }}>
<Box key={key} sx={{ mb: 0.5 }}>
<Typography
variant="body2"
sx={{ fontWeight: "medium", mr: 1 }}
>
{key}:
</Typography>
<Typography variant="body2">
<Typography
variant="body2"
component="div"
sx={{ whiteSpace: "pre-line", mt: 0.5 }}
>
{typeof value === "object" && value !== null
? value?.label || JSON.stringify(value)
: value === true
Expand Down