diff --git a/.gitignore b/.gitignore index 50c2efb4b8d6..5ee28a7a617b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,5 @@ yarn-error.log* debug.log app.log -# Cursor IDE -.cursor/rules - +# AI rules +.*/rules diff --git a/public/version.json b/public/version.json index 7dce10929b8b..40f8aee65efa 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.1.1" -} \ No newline at end of file + "version": "8.2.0" +} diff --git a/src/components/CippCards/CippChartCard.jsx b/src/components/CippCards/CippChartCard.jsx index 2bd455d87761..b05652ef9ec8 100644 --- a/src/components/CippCards/CippChartCard.jsx +++ b/src/components/CippCards/CippChartCard.jsx @@ -92,6 +92,7 @@ export const CippChartCard = ({ chartType = "donut", title, actions, + onClick, }) => { const [range, setRange] = useState("Last 7 days"); const [barSeries, setBarSeries] = useState([]); @@ -109,7 +110,18 @@ export const CippChartCard = ({ }, [chartType, chartSeries.length, labels]); return ( - + theme.shadows[8], + transform: "translateY(-2px)", + } : {}, + }} + > { + switch (category) { + case "Global Standards": + return ; + case "Entra (AAD) Standards": + return ; + case "Exchange Standards": + return ; + case "Defender Standards": + return ; + case "Intune Standards": + return ; + default: + return ; + } +}; + +const getActionIcon = (action) => { + switch (action?.toLowerCase()) { + case "report": + return ; + case "alert": + case "warn": + return ; + case "remediate": + return ; + default: + return ; + } +}; + +const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "low impact": + return "info"; + case "medium impact": + return "warning"; + case "high impact": + return "error"; + default: + return "default"; + } +}; + +export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenant }) => { + const [expanded, setExpanded] = useState(false); + if (!standardsData) return null; + + // Get applicable templates for the current tenant + const applicableTemplates = standardsData.filter((template) => { + const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; + const excludedTenantsArr = Array.isArray(template?.excludedTenants) + ? template.excludedTenants + : []; + + const tenantInFilter = + tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); + + const allTenantsTemplate = + tenantFilterArr.some((tf) => tf.value === "AllTenants") && + (excludedTenantsArr.length === 0 || + !excludedTenantsArr.some((et) => et.value === currentTenant)); + + return tenantInFilter || allTenantsTemplate; + }); + + // Combine standards from all applicable templates + const combinedStandards = {}; + for (const template of applicableTemplates) { + for (const [standardKey, standardValue] of Object.entries(template.standards)) { + combinedStandards[standardKey] = standardValue; + } + } + + // Group standards by category + const standardsByCategory = {}; + Object.entries(combinedStandards).forEach(([standardKey, standardConfig]) => { + const standardInfo = standards.find((s) => s.name === `standards.${standardKey}`); + if (standardInfo) { + const category = standardInfo.cat; + if (!standardsByCategory[category]) { + standardsByCategory[category] = []; + } + standardsByCategory[category].push({ + key: standardKey, + config: standardConfig, + info: standardInfo, + }); + } + }); + + const handleAccordionChange = (panel) => (event, isExpanded) => { + setExpanded(isExpanded ? panel : false); + }; + + return ( + + + Standards Configuration + theme.palette.grey[500], + }} + > + + + + + + + + Showing standards configuration for tenant: {currentTenant} + + + Total templates applied: {applicableTemplates.length} | Total + standards: {Object.keys(combinedStandards).length} + + + + {Object.entries(standardsByCategory).map(([category, categoryStandards], idx) => ( + `1px solid ${theme.palette.divider}`, + "&:before": { display: "none" }, + }} + > + } + aria-controls={`${category}-content`} + id={`${category}-header`} + sx={{ + minHeight: 48, + "& .MuiAccordionSummary-content": { alignItems: "center", m: 0 }, + }} + > + + {getCategoryIcon(category)} + + {category} + + + + + + + {categoryStandards.map(({ key, config, info }) => ( + + + + + + + {info.label} + + + {info.helpText} + + + + + {info.tag && info.tag.length > 0 && ( + + )} + + + + Actions: + + + {config.action && Array.isArray(config.action) ? ( + config.action.map((action, index) => ( + + )) + ) : ( + + No actions configured + + )} + + + + {info.addedComponent && info.addedComponent.length > 0 && ( + + + Fields: + + + {info.addedComponent.map((component, index) => { + const componentValue = _.get(config, component.name); + const displayValue = + componentValue?.label || componentValue || "N/A"; + return ( + + + {component.label || component.name}: + + + + ); + })} + + + )} + + + + + ))} + + + + ))} + + {Object.keys(standardsByCategory).length === 0 && ( + + + No standards configured for this tenant + + + Standards templates may not be applied to this tenant or no standards are currently + active. + + + )} + + + + + + + ); +}; diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx index 5239c63e2a13..19585794ac4b 100644 --- a/src/components/CippCards/CippUserInfoCard.jsx +++ b/src/components/CippCards/CippUserInfoCard.jsx @@ -10,12 +10,15 @@ export const CippUserInfoCard = (props) => { const { user, tenant, isFetching = false, ...other } = props; // Helper function to check if a section has any data - const hasWorkInfo = user?.jobTitle || user?.department || user?.manager?.displayName; - const hasAddressInfo = user?.streetAddress || user?.postalCode || user?.city || user?.country || user?.officeLocation; - const hasContactInfo = user?.mobilePhone || (user?.businessPhones && user?.businessPhones.length > 0); + const hasWorkInfo = user?.jobTitle || user?.department || user?.manager?.displayName || user?.companyName; + const hasAddressInfo = + user?.streetAddress || user?.postalCode || user?.city || user?.country || user?.officeLocation; + const hasContactInfo = + user?.mobilePhone || (user?.businessPhones && user?.businessPhones.length > 0); // Handle image URL - only set if user and tenant exist, otherwise let Avatar fall back to children - const imageUrl = user?.id && tenant ? `/api/ListUserPhoto?TenantFilter=${tenant}&UserId=${user.id}` : undefined; + const imageUrl = + user?.id && tenant ? `/api/ListUserPhoto?TenantFilter=${tenant}&UserId=${user.id}` : undefined; return ( @@ -44,7 +47,7 @@ export const CippUserInfoCard = (props) => { - + {/* Status information section */} @@ -56,7 +59,7 @@ export const CippUserInfoCard = (props) => { {getCippFormatting(user?.accountEnabled, "accountEnabled")} - + Synced from AD: @@ -71,7 +74,7 @@ export const CippUserInfoCard = (props) => { ) } /> - + {/* Basic Identity Information */} { ) } /> - + {/* Licenses */} { ) } /> - + {/* Work Information Section */} { isFetching ? ( ) : !hasWorkInfo ? ( - + No work information available ) : ( @@ -144,9 +147,15 @@ export const CippUserInfoCard = (props) => { Job Title: - - {user.jobTitle} + {user.jobTitle} + + )} + {user?.companyName && ( + + + Company Name: + {user.companyName} )} {user?.department && ( @@ -154,9 +163,7 @@ export const CippUserInfoCard = (props) => { Department: - - {user.department} - + {user.department} )} {user?.manager?.displayName && ( @@ -164,16 +171,14 @@ export const CippUserInfoCard = (props) => { Manager: - - {user.manager.displayName} - + {user.manager.displayName} )} ) } /> - + {/* Contact Information Section */} { isFetching ? ( ) : !hasContactInfo ? ( - + No contact information available ) : ( @@ -192,9 +197,7 @@ export const CippUserInfoCard = (props) => { Mobile Phone: - - {user.mobilePhone} - + {user.mobilePhone} )} {user?.businessPhones && user.businessPhones.length > 0 && ( @@ -202,16 +205,14 @@ export const CippUserInfoCard = (props) => { Business Phones: - - {user.businessPhones.join(", ")} - + {user.businessPhones.join(", ")} )} ) } /> - + {/* Address Information Section */} { isFetching ? ( ) : !hasAddressInfo ? ( - + No address information available ) : ( @@ -229,9 +230,7 @@ export const CippUserInfoCard = (props) => { Street Address: - - {user.streetAddress} - + {user.streetAddress} )} {user?.city && ( @@ -239,9 +238,7 @@ export const CippUserInfoCard = (props) => { City: - - {user.city} - + {user.city} )} {user?.postalCode && ( @@ -249,9 +246,7 @@ export const CippUserInfoCard = (props) => { Postal Code: - - {user.postalCode} - + {user.postalCode} )} {user?.country && ( @@ -259,9 +254,7 @@ export const CippUserInfoCard = (props) => { Country: - - {user.country} - + {user.country} )} {user?.officeLocation && ( @@ -269,9 +262,7 @@ export const CippUserInfoCard = (props) => { Office Location: - - {user.officeLocation} - + {user.officeLocation} )} diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx index ac2377b3734d..171a88c47938 100644 --- a/src/components/CippComponents/AppApprovalTemplateForm.jsx +++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx @@ -1,9 +1,11 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, use } from "react"; import { Alert, Skeleton, Stack, Typography, Button, Box } from "@mui/material"; import { CippFormComponent } from "./CippFormComponent"; +import { CippFormCondition } from "./CippFormCondition"; import { CippApiResults } from "./CippApiResults"; import { Grid } from "@mui/system"; import CippPermissionPreview from "./CippPermissionPreview"; +import { useWatch } from "react-hook-form"; const AppApprovalTemplateForm = ({ formControl, @@ -18,8 +20,33 @@ const AppApprovalTemplateForm = ({ const [selectedPermissionSet, setSelectedPermissionSet] = useState(null); const [permissionsLoaded, setPermissionsLoaded] = useState(false); + // Watch for app type selection changes + const selectedAppType = useWatch({ + control: formControl?.control, + name: "appType", + defaultValue: "EnterpriseApp", + }); + const selectedGalleryTemplate = useWatch({ + control: formControl?.control, + name: "galleryTemplateId", + }); + + // Watch for application manifest changes + const selectedApplicationManifest = useWatch({ + control: formControl?.control, + name: "applicationManifest", + }); + + // Watch for app selection changes to update template name + const selectedApp = useWatch({ + control: formControl?.control, + name: "appId", + }); + // When templateData changes, update the form useEffect(() => { + if (!formControl) return; // Early return if formControl is not available + if (!isEditing && !isCopy) { formControl.setValue("templateName", "New App Deployment Template"); formControl.setValue("appType", "EnterpriseApp"); @@ -29,75 +56,161 @@ const AppApprovalTemplateForm = ({ if (templateData[0]) { const copyName = `Copy of ${templateData[0].TemplateName}`; formControl.setValue("templateName", copyName); - formControl.setValue("appId", { - label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, - value: templateData[0].AppId, - addedFields: { - displayName: templateData[0].AppName, - }, - }); - // Set permission set and trigger loading of permissions - const permissionSetValue = { - label: templateData[0].PermissionSetName || "Custom Permissions", - value: templateData[0].PermissionSetId, - addedFields: { - Permissions: templateData[0].Permissions || {}, - }, - }; - - formControl.setValue("permissionSetId", permissionSetValue); - setSelectedPermissionSet(permissionSetValue); - setPermissionsLoaded(true); + // Set app type based on whether it's a gallery template, defaulting to EnterpriseApp for backward compatibility + const appType = + templateData[0].AppType || + (templateData[0].GalleryTemplateId + ? "GalleryTemplate" + : templateData[0].ApplicationManifest + ? "ApplicationManifest" + : "EnterpriseApp"); + formControl.setValue("appType", appType); + + if (appType === "GalleryTemplate") { + formControl.setValue("galleryTemplateId", { + label: templateData[0].AppName || "Unknown", + value: templateData[0].GalleryTemplateId, + addedFields: { + displayName: templateData[0].AppName, + applicationId: templateData[0].AppId, + // Include saved gallery information for proper display + ...(templateData[0].GalleryInformation || {}), + }, + }); + } else if (appType === "ApplicationManifest") { + // For Application Manifest, load the manifest JSON + if (templateData[0].ApplicationManifest) { + formControl.setValue( + "applicationManifest", + JSON.stringify(templateData[0].ApplicationManifest, null, 2) + ); + } + } else { + formControl.setValue("appId", { + label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, + value: templateData[0].AppId, + addedFields: { + displayName: templateData[0].AppName, + }, + }); + } + + // Set permission set and trigger loading of permissions (only for Enterprise Apps) + if (appType === "EnterpriseApp") { + const permissionSetValue = { + label: templateData[0].PermissionSetName || "Custom Permissions", + value: templateData[0].PermissionSetId, + addedFields: { + Permissions: templateData[0].Permissions || {}, + }, + }; + + formControl.setValue("permissionSetId", permissionSetValue); + setSelectedPermissionSet(permissionSetValue); + setPermissionsLoaded(true); + } else { + // For Gallery Templates, no permission set needed + setSelectedPermissionSet(null); + setPermissionsLoaded(false); + } } } else if (templateData) { // For editing, load all template data if (templateData[0]) { formControl.setValue("templateName", templateData[0].TemplateName); - formControl.setValue("appId", { - label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, - value: templateData[0].AppId, - addedFields: { - displayName: templateData[0].AppName, - }, - }); - // Set permission set and trigger loading of permissions - const permissionSetValue = { - label: templateData[0].PermissionSetName || "Custom Permissions", - value: templateData[0].PermissionSetId, - addedFields: { - Permissions: templateData[0].Permissions || {}, - }, - }; - - formControl.setValue("permissionSetId", permissionSetValue); - setSelectedPermissionSet(permissionSetValue); - setPermissionsLoaded(true); + // Set app type based on whether it's a gallery template, defaulting to EnterpriseApp for backward compatibility + const appType = + templateData[0].AppType || + (templateData[0].GalleryTemplateId + ? "GalleryTemplate" + : templateData[0].ApplicationManifest + ? "ApplicationManifest" + : "EnterpriseApp"); + formControl.setValue("appType", appType); + + if (appType === "GalleryTemplate") { + formControl.setValue("galleryTemplateId", { + label: templateData[0].AppName || "Unknown", + value: templateData[0].GalleryTemplateId, + addedFields: { + displayName: templateData[0].AppName, + applicationId: templateData[0].AppId, + // Include saved gallery information for proper display + ...(templateData[0].GalleryInformation || {}), + }, + }); + } else if (appType === "ApplicationManifest") { + // For Application Manifest, load the manifest JSON + if (templateData[0].ApplicationManifest) { + formControl.setValue( + "applicationManifest", + JSON.stringify(templateData[0].ApplicationManifest, null, 2) + ); + } + } else { + formControl.setValue("appId", { + label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, + value: templateData[0].AppId, + addedFields: { + displayName: templateData[0].AppName, + }, + }); + } + + // Set permission set and trigger loading of permissions (only for Enterprise Apps) + if (appType === "EnterpriseApp") { + const permissionSetValue = { + label: templateData[0].PermissionSetName || "Custom Permissions", + value: templateData[0].PermissionSetId, + addedFields: { + Permissions: templateData[0].Permissions || {}, + }, + }; + + formControl.setValue("permissionSetId", permissionSetValue); + setSelectedPermissionSet(permissionSetValue); + setPermissionsLoaded(true); + } else { + // For Gallery Templates and Application Manifests, no permission set needed + setSelectedPermissionSet(null); + setPermissionsLoaded(false); + } } } }, [templateData, isCopy, isEditing, formControl]); - // Watch for app selection changes to update template name - const selectedApp = formControl.watch("appId"); - useEffect(() => { + if (!formControl) return; // Early return if formControl is not available + // Update template name when app is selected if we're in add mode and name hasn't been manually changed - if (selectedApp && !isEditing && !isCopy) { + if (!isEditing && !isCopy) { const currentName = formControl.getValues("templateName"); // Only update if it's still the default or empty if (currentName === "New App Deployment Template" || !currentName) { - // Extract app name from the label (format is usually "AppName (AppId)") - const appName = selectedApp.label.split(" (")[0]; + let appName = null; + + if (selectedAppType === "GalleryTemplate" && selectedGalleryTemplate) { + appName = + selectedGalleryTemplate.addedFields?.displayName || selectedGalleryTemplate.label; + } else if (selectedAppType === "EnterpriseApp" && selectedApp) { + // Extract app name from the label (format is usually "AppName (AppId)") + appName = selectedApp.label.split(" (")[0]; + } + if (appName) { formControl.setValue("templateName", `${appName} Template`); } } } - }, [selectedApp, isEditing, isCopy, formControl]); + }, [selectedApp, selectedGalleryTemplate, selectedAppType, isEditing, isCopy, formControl]); // Watch for permission set selection changes - const selectedPermissionSetValue = formControl.watch("permissionSetId"); + const selectedPermissionSetValue = useWatch({ + control: formControl?.control, + name: "permissionSetId", + }); useEffect(() => { if (selectedPermissionSetValue?.value) { @@ -122,19 +235,69 @@ const AppApprovalTemplateForm = ({ // Handle form submission const handleSubmit = (data) => { - const appDisplayName = - data.appId?.addedFields?.displayName || - (data.appId?.label ? data.appId.label.split(" (")[0] : undefined); + let appDisplayName, appId, galleryTemplateId, applicationManifest; + + if (data.appType === "GalleryTemplate") { + appDisplayName = + data.galleryTemplateId?.addedFields?.displayName || data.galleryTemplateId?.label; + appId = data.galleryTemplateId?.addedFields?.applicationId; + galleryTemplateId = data.galleryTemplateId?.value; + } else if (data.appType === "ApplicationManifest") { + try { + applicationManifest = JSON.parse(data.applicationManifest); + + // Validate signInAudience - only allow null/undefined or "AzureADMyOrg" + if ( + applicationManifest.signInAudience && + applicationManifest.signInAudience !== "AzureADMyOrg" + ) { + return; // Don't submit if validation fails + } + + // Extract app name from manifest + appDisplayName = + applicationManifest.displayName || + applicationManifest.appDisplayName || + "Custom Application"; + // Application ID will be generated during deployment for manifests + appId = null; + } catch (error) { + console.error("Failed to parse application manifest:", error); + return; // Don't submit if manifest is invalid + } + } else { + appDisplayName = + data.appId?.addedFields?.displayName || + (data.appId?.label ? data.appId.label.split(" (")[0] : undefined); + appId = data.appId?.value; + } const payload = { TemplateName: data.templateName, - AppId: data.appId?.value, + AppType: data.appType, + AppId: appId, AppName: appDisplayName, - PermissionSetId: data.permissionSetId?.value, - PermissionSetName: data.permissionSetId?.label, - Permissions: data.permissionSetId?.addedFields?.Permissions, }; + // Only include permission set data for Enterprise Apps + if (data.appType === "EnterpriseApp") { + payload.PermissionSetId = data.permissionSetId?.value; + payload.PermissionSetName = data.permissionSetId?.label; + payload.Permissions = data.permissionSetId?.addedFields?.Permissions; + } + // For Gallery Templates, permissions will be auto-handled from the template's app registration + if (data.appType === "GalleryTemplate") { + payload.Permissions = null; // No permissions needed for Gallery Templates + payload.GalleryTemplateId = galleryTemplateId; + payload.GalleryInformation = selectedGalleryTemplate?.addedFields || {}; + } + + // For Application Manifests, store the manifest data + if (data.appType === "ApplicationManifest") { + payload.Permissions = null; // Permissions defined in manifest + payload.ApplicationManifest = applicationManifest; + } + if (isEditing && !isCopy && templateData?.[0]?.TemplateId) { payload.TemplateId = templateData[0].TemplateId; } @@ -142,8 +305,11 @@ const AppApprovalTemplateForm = ({ // Store values before submission to set them back afterward const currentValues = { templateName: data.templateName, + appType: data.appType, appId: data.appId, + galleryTemplateId: data.galleryTemplateId, permissionSetId: data.permissionSetId, + applicationManifest: data.applicationManifest, }; onSubmit(payload); @@ -153,10 +319,17 @@ const AppApprovalTemplateForm = ({ if (!isEditing) { setTimeout(() => { formControl.setValue("templateName", currentValues.templateName, { shouldDirty: false }); + formControl.setValue("appType", currentValues.appType, { shouldDirty: false }); formControl.setValue("appId", currentValues.appId, { shouldDirty: false }); + formControl.setValue("galleryTemplateId", currentValues.galleryTemplateId, { + shouldDirty: false, + }); formControl.setValue("permissionSetId", currentValues.permissionSetId, { shouldDirty: false, }); + formControl.setValue("applicationManifest", currentValues.applicationManifest, { + shouldDirty: false, + }); }, 100); } }; @@ -171,9 +344,18 @@ const AppApprovalTemplateForm = ({ <> App approval templates allow you to define an application with its permissions that - can be deployed to multiple tenants. Select a multi-tenant application and - permission set to create a template. If your application is not listed, check the - Supported account types in the App Registration properties in Entra. + can be deployed to multiple tenants. Choose from three template types: +
+
+ • Enterprise Application: Deploy existing multi-tenant apps from + your tenant. Requires "Multiple organizations" or "Personal Microsoft accounts" in + App Registration settings. +
+ • Gallery Template: Deploy pre-configured applications from + Microsoft's Enterprise Application Gallery with standard permissions. +
+ • Application Manifest: Deploy custom applications using JSON + manifests. For security, only single-tenant apps (AzureADMyOrg) are supported.
`${item.displayName} (${item.appId})`, - valueField: "appId", - addedField: { - displayName: "displayName", - signInAudience: "signInAudience", - }, - dataFilter: (data) => { - return data.filter( - (item) => item.addedFields?.signInAudience === "AzureADMultipleOrgs" - ); - }, - showRefresh: true, - }} - multiple={false} + name="appType" + label="Application Type" + type="select" + clearable={false} + options={[ + { label: "Enterprise Application", value: "EnterpriseApp" }, + { label: "Gallery Template", value: "GalleryTemplate" }, + { label: "Application Manifest", value: "ApplicationManifest" }, + ]} creatable={false} required={true} - validators={{ required: "Application is required" }} + validators={{ required: "Application type is required" }} /> + + `${item.displayName} (${item.appId})`, + valueField: "appId", + addedField: { + displayName: "displayName", + signInAudience: "signInAudience", + }, + dataFilter: (data) => { + return data.filter( + (item) => + item.addedFields?.signInAudience === "AzureADMultipleOrgs" || + item.addedFields?.signInAudience === "AzureADandPersonalMicrosoftAccount" + ); + }, + showRefresh: true, + }} + multiple={false} + creatable={false} + required={true} + validators={{ required: "Application is required" }} + /> + + + item.displayName, + valueField: "id", + addedField: { + displayName: "displayName", + applicationId: "applicationId", + description: "description", + categories: "categories", + publisher: "publisher", + logoUrl: "logoUrl", + homePageUrl: "homePageUrl", + supportedSingleSignOnModes: "supportedSingleSignOnModes", + supportedProvisioningTypes: "supportedProvisioningTypes", + }, + showRefresh: true, + }} + multiple={false} + creatable={false} + required={true} + sortOptions={true} + validators={{ required: "Gallery template is required" }} + /> + + + { + try { + const manifest = JSON.parse(value); - + + + item.TemplateName, - valueField: "TemplateId", - addedField: { - Permissions: "Permissions", - }, - showRefresh: true, - }} - multiple={false} - creatable={false} - required={true} - validators={{ required: "Permission Set is required" }} - /> + > + item.TemplateName, + valueField: "TemplateId", + addedField: { + Permissions: "Permissions", + }, + showRefresh: true, + }} + multiple={false} + creatable={false} + required={true} + validators={{ required: "Permission Set is required" }} + /> + @@ -250,9 +544,31 @@ const AppApprovalTemplateForm = ({ { + try { + return JSON.parse(selectedApplicationManifest); + } catch (e) { + return null; // Return null if JSON is invalid + } + })() + : null + } /> diff --git a/src/components/CippComponents/CertificateCredentialRemovalForm.jsx b/src/components/CippComponents/CertificateCredentialRemovalForm.jsx new file mode 100644 index 000000000000..9b499297ed59 --- /dev/null +++ b/src/components/CippComponents/CertificateCredentialRemovalForm.jsx @@ -0,0 +1,23 @@ +import { CippFormComponent } from "./CippFormComponent.jsx"; + +export const CertificateCredentialRemovalForm = ({ formHook, row }) => { + return ( + ({ + label: `${cred.displayName || "Unnamed"} (Expiration: ${new Date( + cred.endDateTime + ).toLocaleDateString()})`, + value: cred.keyId, + })) || [] + } + /> + ); +}; diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 33ce316d5cfa..6d441e7412ce 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -12,7 +12,7 @@ import { Stack } from "@mui/system"; import { CippApiResults } from "./CippApiResults"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import React, { useEffect, useState } from "react"; -import { useForm, useWatch } from "react-hook-form"; +import { useForm, useFormState } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; import CippFormComponent from "./CippFormComponent"; @@ -41,9 +41,13 @@ export const CippApiDialog = (props) => { } const formHook = useForm({ - defaultValues: defaultvalues || {} + defaultValues: defaultvalues || {}, + mode: "onChange", // Enable real-time validation }); + // Get form state for validation + const { isValid } = useFormState({ control: formHook.control }); + useEffect(() => { if (createDialog.open) { setIsFormSubmitted(false); @@ -357,7 +361,7 @@ export const CippApiDialog = (props) => { diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index bc0d12572885..388b27984ced 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -1,4 +1,4 @@ -import { Close, Download, Help } from "@mui/icons-material"; +import { Close, Download, Help, ExpandMore, ExpandLess } from "@mui/icons-material"; import { Alert, CircularProgress, @@ -16,6 +16,7 @@ import { useEffect, useState, useMemo, useCallback } from "react"; import { getCippError } from "../../utils/get-cipp-error"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; import { CippDocsLookup } from "./CippDocsLookup"; +import { CippCodeBlock } from "./CippCodeBlock"; import React from "react"; import { CippTableDialog } from "./CippTableDialog"; import { EyeIcon } from "@heroicons/react/24/outline"; @@ -43,12 +44,14 @@ const extractAllResults = (data) => { const copyField = item.copyField || ""; const severity = typeof item.state === "string" ? item.state : getSeverity(item) ? "error" : "success"; + const details = item.details || null; if (text) { return { text, copyField, severity, + details, ...item, }; } @@ -123,6 +126,7 @@ export const CippApiResults = (props) => { const [errorVisible, setErrorVisible] = useState(false); const [fetchingVisible, setFetchingVisible] = useState(false); const [finalResults, setFinalResults] = useState([]); + const [showDetails, setShowDetails] = useState({}); const tableDialog = useDialog(); const pageTitle = `${document.title} - Results`; const correctResultObj = useMemo(() => { @@ -194,6 +198,10 @@ export const CippApiResults = (props) => { setFinalResults((prev) => prev.map((r) => (r.id === id ? { ...r, visible: false } : r))); }, []); + const toggleDetails = useCallback((id) => { + setShowDetails((prev) => ({ ...prev, [id]: !prev[id] })); + }, []); + const handleDownloadCsv = useCallback(() => { if (!finalResults?.length) return; @@ -272,7 +280,21 @@ export const CippApiResults = (props) => { { Get Help )} - + + + {resultObj.details && ( + + toggleDetails(resultObj.id)} + aria-label={showDetails[resultObj.id] ? "Hide Details" : "Show Details"} + > + {showDetails[resultObj.id] ? ( + + ) : ( + + )} + + + )} { } > - {resultObj.text} + + {resultObj.text} + {resultObj.details && ( + + + + + + )} + diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx index 3e489d043304..46adfc153008 100644 --- a/src/components/CippComponents/CippAppPermissionBuilder.jsx +++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx @@ -821,7 +821,6 @@ const CippAppPermissionBuilder = ({ { return ( - + diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx index 0ac6f6fe9faa..5e01fc542588 100644 --- a/src/components/CippComponents/CippExchangeActions.jsx +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -17,6 +17,7 @@ import { MailLock, SettingsEthernet, CalendarMonth, + Email, } from "@mui/icons-material"; export const CippExchangeActions = () => { @@ -45,40 +46,28 @@ export const CippExchangeActions = () => { icon: , }, { - label: "Convert to User Mailbox", + label: "Convert Mailbox", type: "POST", + icon: , url: "/api/ExecConvertMailbox", - icon: , - data: { - ID: "UPN", - MailboxType: "!Regular", - }, - confirmText: "Are you sure you want to convert [UPN] to a user mailbox?", - condition: (row) => row.recipientTypeDetails !== "UserMailbox", - }, - { - label: "Convert to Shared Mailbox", - type: "POST", - icon: , - url: "/api/ExecConvertMailbox", - data: { - ID: "UPN", - MailboxType: "!Shared", - }, - confirmText: "Are you sure you want to convert [UPN] to a shared mailbox?", - condition: (row) => row.recipientTypeDetails !== "SharedMailbox", - }, - { - label: "Convert to Room Mailbox", - type: "POST", - url: "/api/ExecConvertMailbox", - icon: , - data: { - ID: "UPN", - MailboxType: "!Room", - }, - confirmText: "Are you sure you want to convert [UPN] to a room mailbox?", - condition: (row) => row.recipientTypeDetails !== "RoomMailbox", + data: { ID: "userPrincipalName" }, + fields: [ + { + type: "radio", + name: "MailboxType", + label: "Mailbox Type", + options: [ + { label: "User Mailbox", value: "Regular" }, + { label: "Shared Mailbox", value: "Shared" }, + { label: "Room Mailbox", value: "Room" }, + { label: "Equipment Mailbox", value: "Equipment" }, + ], + validators: { required: "Please select a mailbox type" }, + }, + ], + confirmText: + "Pick the type of mailbox you want to convert [UPN] of mailbox type [recipientTypeDetails] to:", + multiPost: false, }, { //tested @@ -103,30 +92,27 @@ export const CippExchangeActions = () => { condition: (row) => row.ArchiveGuid !== "00000000-0000-0000-0000-000000000000", }, { - label: "Hide from Global Address List", - type: "POST", - url: "/api/ExecHideFromGAL", - icon: , - data: { - ID: "UPN", - HidefromGAL: true, - }, - confirmText: - "Are you sure you want to hide [UPN] from the global address list? This will not work if the user is AD Synced.", - condition: (row) => row.HiddenFromAddressListsEnabled === false, - }, - { - label: "Unhide from Global Address List", + label: "Set Global Address List visibility", type: "POST", url: "/api/ExecHideFromGAL", icon: , data: { ID: "UPN", - HidefromGAL: false, }, + fields: [ + { + type: "radio", + name: "HidefromGAL", + label: "Global Address List visibility", + options: [ + { label: "Hidden", value: true }, + { label: "Shown", value: false }, + ], + validators: { required: "Please select a global address list state" }, + }, + ], confirmText: - "Are you sure you want to unhide [UPN] from the global address list? This will not work if the user is AD Synced.", - condition: (row) => row.HiddenFromAddressListsEnabled === true, + "Are you sure you want to set the global address list state for [UPN]? Changes can take up to 72 hours to take effect.", }, { label: "Start Managed Folder Assistant", @@ -149,22 +135,24 @@ export const CippExchangeActions = () => { multiPost: false, }, { - label: "Copy Sent Items to for Delegated Mailboxes", + label: "Set Copy Sent Items for Delegated Mailboxes", type: "POST", - url: "/api/ExecCopyForSent", - data: { ID: "UPN", MessageCopyForSentAsEnabled: true }, - confirmText: "Are you sure you want to enable Copy Sent Items on [UPN]?", icon: , - condition: (row) => row.MessageCopyForSentAsEnabled === false, - }, - { - label: "Disable Copy Sent Items for Delegated Mailboxes", - type: "POST", url: "/api/ExecCopyForSent", - data: { ID: "UPN", MessageCopyForSentAsEnabled: false }, - confirmText: "Are you sure you want to disable Copy Sent Items on [UPN]?", - icon: , - condition: (row) => row.MessageCopyForSentAsEnabled === true, + data: { ID: "UPN" }, + fields: [ + { + type: "radio", + name: "MessageCopyForSentAsEnabled", + label: "Copy Sent Items", + options: [ + { label: "Enabled", value: true }, + { label: "Disabled", value: false }, + ], + validators: { required: "Please select a copy sent items state" }, + }, + ], + confirmText: "Are you sure you want to set Copy Sent Items for [UPN]?", }, { label: "Set Litigation Hold", @@ -216,6 +204,7 @@ export const CippExchangeActions = () => { name: "locale", type: "textField", placeholder: "e.g. en-US", + validators: { required: "Please enter a locale" }, }, ], }, @@ -254,6 +243,7 @@ export const CippExchangeActions = () => { name: "quota", type: "textField", placeholder: "e.g. 1000MB, 10GB,1TB", + validators: { required: "Please enter a quota" }, }, ], }, @@ -273,6 +263,7 @@ export const CippExchangeActions = () => { name: "quota", type: "textField", placeholder: "e.g. 1000MB, 10GB,1TB", + validators: { required: "Please enter a quota" }, }, ], }, @@ -289,6 +280,7 @@ export const CippExchangeActions = () => { name: "quota", type: "textField", placeholder: "e.g. 1000MB, 10GB,1TB", + validators: { required: "Please enter a quota" }, }, ], }, @@ -299,7 +291,9 @@ export const CippExchangeActions = () => { data: { UPN: "UPN" }, confirmText: "Configure calendar processing settings for [UPN]", icon: , - condition: (row) => row.recipientTypeDetails === "RoomMailbox" || row.recipientTypeDetails === "EquipmentMailbox", + condition: (row) => + row.recipientTypeDetails === "RoomMailbox" || + row.recipientTypeDetails === "EquipmentMailbox", fields: [ { label: "Automatically Process Meeting Requests", diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index c1a92c76d0e4..7ddc2965b0b0 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -10,6 +10,7 @@ import { RadioGroup, Button, Box, + Input, } from "@mui/material"; import { CippAutoComplete } from "./CippAutocomplete"; import { Controller, useFormState } from "react-hook-form"; @@ -27,7 +28,7 @@ import { import StarterKit from "@tiptap/starter-kit"; import { CippDataTable } from "../CippTable/CippDataTable"; import React from "react"; -import { AccessTime } from "@mui/icons-material"; +import { CloudUpload } from "@mui/icons-material"; // Helper function to convert bracket notation to dot notation // Improved to correctly handle nested bracket notations @@ -134,6 +135,11 @@ export const CippFormComponent = (props) => { {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); case "password": @@ -156,6 +162,11 @@ export const CippFormComponent = (props) => { {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); case "number": @@ -177,6 +188,11 @@ export const CippFormComponent = (props) => { {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); @@ -314,7 +330,10 @@ export const CippFormComponent = (props) => { ); - case "richText": + case "richText": { + const editorInstanceRef = React.useRef(null); + const hasSetInitialValue = React.useRef(false); + return ( <>
@@ -322,30 +341,48 @@ export const CippFormComponent = (props) => { name={convertedName} control={formControl.control} rules={validators} - render={({ field }) => ( - <> - {label} - { - field.onChange(editor.getHTML()); - }} - label={label} - renderControls={() => ( - - - - - - - )} - /> - - )} + render={({ field }) => { + const { value, onChange, ref } = field; + + // Set content only once on first render + React.useEffect(() => { + if ( + editorInstanceRef.current && + !hasSetInitialValue.current && + typeof value === "string" + ) { + editorInstanceRef.current.commands.setContent(value || "", false); + hasSetInitialValue.current = true; + } + }, [value]); + + return ( + <> + {label} + { + editorInstanceRef.current = editor; + }} + onUpdate={({ editor }) => { + onChange(editor.getHTML()); + }} + label={label} + renderControls={() => ( + + + + + + + )} + /> + + ); + }} />
@@ -353,7 +390,7 @@ export const CippFormComponent = (props) => { ); - + } case "CSVReader": const remapData = (data, nameToCSVMapping) => { if (nameToCSVMapping && data) { @@ -417,7 +454,7 @@ export const CippFormComponent = (props) => { control={formControl.control} rules={validators} render={({ field }) => ( - + { field.onChange(unixTimestamp); }} sx={{ - height: '42px', - minWidth: '42px', - padding: '8px 12px', - alignSelf: 'flex-end', - marginBottom: '0px', // Adjust to align with input field + height: "42px", + minWidth: "42px", + padding: "8px 12px", + alignSelf: "flex-end", + marginBottom: "0px", // Adjust to align with input field }} title="Set to current date and time" > @@ -486,6 +523,71 @@ export const CippFormComponent = (props) => { ); + case "file": + return ( + <> +
+ ( + + + {label} + + document.getElementById(`file-input-${convertedName}`).click()} + > + + + {field.value ? field.value.name : "Click to upload file or drag and drop"} + + {field.value && ( + + Size: {(field.value.size / 1024).toFixed(2)} KB + + )} + + { + const file = e.target.files[0]; + field.onChange(file); + if (other.onChange) { + other.onChange(file); + } + }} + /> + + )} + /> +
+ + {get(errors, convertedName, {})?.message} + + {helperText && ( + + {helperText} + + )} + + ); + default: return null; } diff --git a/src/components/CippComponents/CippFormContactSelector.jsx b/src/components/CippComponents/CippFormContactSelector.jsx index 98cb8ad5c40e..ec79ea8424bd 100644 --- a/src/components/CippComponents/CippFormContactSelector.jsx +++ b/src/components/CippComponents/CippFormContactSelector.jsx @@ -12,6 +12,7 @@ export const CippFormContactSelector = ({ select, addedField, valueField, + dataFilter = null, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); @@ -33,6 +34,12 @@ export const CippFormContactSelector = ({ })`, valueField: valueField ? valueField : "WindowsEmailAddress" || "mail", queryKey: `listcontacts-${currentTenant?.value ? currentTenant.value : selectedTenant}`, + dataFilter: (options) => { + if (dataFilter) { + return options.filter(dataFilter); + } + return options; + }, }} creatable={false} {...other} diff --git a/src/components/CippComponents/CippFormInputArray.jsx b/src/components/CippComponents/CippFormInputArray.jsx index 9d9a202c6812..159b11b36119 100644 --- a/src/components/CippComponents/CippFormInputArray.jsx +++ b/src/components/CippComponents/CippFormInputArray.jsx @@ -1,4 +1,4 @@ -import { TextField, IconButton, Typography, SvgIcon } from "@mui/material"; +import { TextField, IconButton, Typography, Box } from "@mui/material"; import { Controller, useFieldArray } from "react-hook-form"; import { Add, Remove } from "@mui/icons-material"; @@ -7,68 +7,111 @@ const convertBracketsToDots = (name) => { return name.replace(/\[(\d+)\]/g, ".$1"); // Replace [0] with .0 }; -export const CippFormInputArray = ({ formControl, name, label, validators, ...other }) => { +export const CippFormInputArray = ({ + formControl, + name, + label, + validators, + mode = "keyValue", // Default to keyValue for backward compatibility + placeholder, + keyPlaceholder = "Key", + valuePlaceholder = "Value", + ...other +}) => { // Convert the name from bracket notation to dot notation const convertedName = convertBracketsToDots(name); + // Determine initial value based on mode + const getInitialValue = () => { + if (mode === "simple") { + return ""; + } else { + return { Key: "", Value: "" }; + } + }; + // Use `useFieldArray` to manage dynamic field arrays const { fields, append, remove } = useFieldArray({ control: formControl.control, - name: convertedName, // Specify the converted name for useFieldArray + name: convertedName, }); + // Render simple mode (single input field) + const renderSimpleField = (field, index) => ( + + ( + + )} + /> + remove(index)} aria-label="remove item" size="small"> + + + + ); + + // Render key-value mode (two input fields) - original functionality + const renderKeyValueField = (field, index) => ( + + ( + + )} + /> + ( + + )} + /> + remove(index)} aria-label="remove item" size="small"> + + + + ); + return ( - <> -
+ + {label && {label}} - append({ Key: "", Value: "" })} variant="outlined"> + append(getInitialValue())} variant="outlined" size="small"> -
+
- {fields.map((field, index) => ( -
- ( - - )} - /> - ( - - )} - /> - remove(index)} aria-label="remove item"> - - - - -
- ))} - + {fields.map((field, index) => + mode === "simple" ? renderSimpleField(field, index) : renderKeyValueField(field, index) + )} +
); }; diff --git a/src/components/CippComponents/CippFormUserSelector.jsx b/src/components/CippComponents/CippFormUserSelector.jsx index ed9912c5338f..7e5b11c0f551 100644 --- a/src/components/CippComponents/CippFormUserSelector.jsx +++ b/src/components/CippComponents/CippFormUserSelector.jsx @@ -12,6 +12,7 @@ export const CippFormUserSelector = ({ select, addedField, valueField, + dataFilter = null, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); @@ -41,6 +42,12 @@ export const CippFormUserSelector = ({ $orderby: "displayName", $top: 999, }, + dataFilter: (options) => { + if (dataFilter) { + return options.filter(dataFilter); + } + return options; + } }} creatable={false} {...other} diff --git a/src/components/CippComponents/CippPermissionPreview.jsx b/src/components/CippComponents/CippPermissionPreview.jsx index 6347fff47c6e..ad1bb530020c 100644 --- a/src/components/CippComponents/CippPermissionPreview.jsx +++ b/src/components/CippComponents/CippPermissionPreview.jsx @@ -13,9 +13,14 @@ import { Tabs, Chip, SvgIcon, + Accordion, + AccordionSummary, + AccordionDetails, } from "@mui/material"; import { ShieldCheckIcon } from "@heroicons/react/24/outline"; +import { ExpandMore } from "@mui/icons-material"; import { CippCardTabPanel } from "./CippCardTabPanel"; +import { ApiGetCall } from "../../api/ApiCall"; const CippPermissionPreview = ({ permissions, @@ -23,6 +28,8 @@ const CippPermissionPreview = ({ isLoading = false, maxHeight = "100%", showAppIds = true, + galleryTemplate = null, + applicationManifest = null, }) => { const [selectedPermissionTab, setSelectedPermissionTab] = useState(0); const [servicePrincipalDetails, setServicePrincipalDetails] = useState({}); @@ -115,7 +122,6 @@ const CippPermissionPreview = ({ // Better checks for permissions object to prevent rendering errors if (isLoading || loadingDetails) { - return ( <> {title} @@ -124,7 +130,7 @@ const CippPermissionPreview = ({ ); } - if (!permissions) { + if (!permissions && !galleryTemplate && !applicationManifest) { return ( Select a template with permissions to see what will be consented. @@ -132,6 +138,165 @@ const CippPermissionPreview = ({ ); } + // If we have gallery template data, show that instead of permissions + if (galleryTemplate) { + return ( + + {title} + + + {/* App Logo and Name */} + + {galleryTemplate.addedFields?.logoUrl && ( + + {galleryTemplate.addedFields?.displayName { + e.target.style.display = "none"; + }} + /> + + )} + + + {galleryTemplate.addedFields?.displayName || galleryTemplate.label} + + {galleryTemplate.addedFields?.publisher && ( + + by {galleryTemplate.addedFields.publisher} + + )} + + + + {/* Description */} + {galleryTemplate.addedFields?.description && ( + + + {galleryTemplate.addedFields.description} + + + )} + + {/* Categories */} + {galleryTemplate.addedFields?.categories && + galleryTemplate.addedFields.categories.length > 0 && ( + + + Categories: + + + {galleryTemplate.addedFields.categories.map((category, idx) => ( + + ))} + + + )} + + {/* SSO Modes */} + {galleryTemplate.addedFields?.supportedSingleSignOnModes && + galleryTemplate.addedFields.supportedSingleSignOnModes.length > 0 && ( + + + Supported SSO Modes: + + + {galleryTemplate.addedFields.supportedSingleSignOnModes.map((mode, idx) => ( + + ))} + + + )} + + {/* Provisioning Types */} + {galleryTemplate.addedFields?.supportedProvisioningTypes && + galleryTemplate.addedFields.supportedProvisioningTypes.length > 0 && ( + + + Supported Provisioning: + + + {galleryTemplate.addedFields.supportedProvisioningTypes.map((type, idx) => ( + + ))} + + + )} + + {/* Home Page URL */} + {galleryTemplate.addedFields?.homePageUrl && ( + + + Home Page: + + + {galleryTemplate.addedFields.homePageUrl} + + + )} + + {/* Template ID */} + + + Template ID: {galleryTemplate.value} + + + + {/* Auto-consent note */} + + Gallery templates will automatically consent to the required permissions defined in + the template's app registration. No manual permission configuration needed. + + + + + ); + } + + // If we have application manifest data, show that instead of permissions + if (applicationManifest) { + return ( + + ); + } + // Ensure permissions is an object and has entries if ( typeof permissions !== "object" || @@ -376,4 +541,293 @@ const CippPermissionPreview = ({ ); }; +// Component to handle individual service principal resource details +const ServicePrincipalResourceDetails = ({ + resource, + servicePrincipalId, + expandedResource, + handleAccordionChange, +}) => { + // Fetch individual service principal details using ApiGetCall + const { + data: servicePrincipalData, + isSuccess: spDetailSuccess, + isFetching: spDetailFetching, + isLoading: spDetailLoading, + } = ApiGetCall({ + url: "/api/ExecServicePrincipals", + data: { Id: servicePrincipalId }, + queryKey: `execServicePrincipal-details-${servicePrincipalId}`, + waiting: !!servicePrincipalId, + }); + + const spDetails = servicePrincipalData?.Results; + + // Helper to get permission details + const getPermissionDetails = (permissionId, type) => { + if (!spDetails) return { name: permissionId, description: "Loading..." }; + + if (type === "Role") { + const foundRole = spDetails.appRoles?.find((role) => role.id === permissionId); + return { + name: foundRole?.value || permissionId, + description: foundRole?.description || "No description available", + }; + } else { + const foundScope = spDetails.publishedPermissionScopes?.find( + (scope) => scope.id === permissionId + ); + return { + name: foundScope?.value || permissionId, + description: + foundScope?.userConsentDescription || + foundScope?.description || + "No description available", + }; + } + }; + + const resourceName = spDetails?.displayName || resource.resourceAppId; + const appPermissions = resource.resourceAccess?.filter((access) => access.type === "Role") || []; + const delegatedPermissions = + resource.resourceAccess?.filter((access) => access.type === "Scope") || []; + + return ( + + }> + + + {spDetailLoading || spDetailFetching ? "Loading..." : resourceName} + + + + + + } + title="Application/Delegated Permissions" + /> + + + + + {(spDetailLoading || spDetailFetching) && ( + + )} + + {spDetailSuccess && spDetails && ( + <> + {appPermissions.length > 0 && ( + + + Application Permissions ({appPermissions.length}) + + + {appPermissions.map((permission, idx) => { + const permDetails = getPermissionDetails(permission.id, "Role"); + return ( + + + + ); + })} + + + )} + + {delegatedPermissions.length > 0 && ( + + + Delegated Permissions ({delegatedPermissions.length}) + + + {delegatedPermissions.map((permission, idx) => { + const permDetails = getPermissionDetails(permission.id, "Scope"); + return ( + + + + ); + })} + + + )} + + )} + + + ); +}; + +// Component to handle Application Manifest preview with detailed permission expansion +const ApplicationManifestPreview = ({ applicationManifest, title, maxHeight }) => { + const [expandedResource, setExpandedResource] = useState(false); + + // Get unique resource IDs from required resource access + const resourceIds = + applicationManifest.requiredResourceAccess?.map((resource) => resource.resourceAppId) || []; + + // Fetch the service principal list to get object IDs + const { + data: servicePrincipals = [], + isSuccess: spSuccess, + isFetching: spFetching, + isLoading: spLoading, + } = ApiGetCall({ + url: "/api/ExecServicePrincipals", + data: { Select: "appId,displayName,id" }, + queryKey: "execServicePrincipalList-cipp-permission-preview", + waiting: true, + }); + + // Helper to get service principal ID by appId + const getServicePrincipalId = (appId) => { + if (spSuccess && servicePrincipals?.Results) { + const sp = servicePrincipals.Results.find((sp) => sp.appId === appId); + return sp?.id || null; + } + return null; + }; + + const handleAccordionChange = (panel) => (event, newExpanded) => { + setExpandedResource(newExpanded ? panel : false); + }; + + return ( + + {title} + + + {/* App Basic Info */} + + + {applicationManifest.displayName || "Custom Application"} + + {applicationManifest.description && ( + + {applicationManifest.description} + + )} + + + {/* Application Properties */} + + + Application Properties: + + + {applicationManifest.signInAudience && ( + + + + )} + {applicationManifest.web?.redirectUris && + applicationManifest.web.redirectUris.length > 0 && ( + + + + )} + + + + {/* Required Resource Access with detailed permissions */} + {applicationManifest.requiredResourceAccess && + applicationManifest.requiredResourceAccess.length > 0 && ( + + + Required Permissions: + + {(spLoading || spFetching) && ( + + )} + {spSuccess && + servicePrincipals?.Results && + applicationManifest.requiredResourceAccess.map((resource, index) => { + const servicePrincipalId = getServicePrincipalId(resource.resourceAppId); + + return ( + + ); + })} + + )} + + {/* Custom application note */} + {/* Validation warning for signInAudience */} + {applicationManifest.signInAudience && + applicationManifest.signInAudience !== "AzureADMyOrg" && ( + + + Invalid signInAudience: "{applicationManifest.signInAudience}" + + + For security reasons, Application Manifests must have signInAudience set to + "AzureADMyOrg" or not defined in the JSON. This template cannot be deployed with + the current signInAudience value. + + + )} + + + This application will be created from a custom manifest. All permissions and + configuration are defined within the manifest JSON. + + + + + ); +}; + export default CippPermissionPreview; diff --git a/src/components/CippComponents/CippScheduledTaskActions.jsx b/src/components/CippComponents/CippScheduledTaskActions.jsx new file mode 100644 index 000000000000..2a1ec00236ff --- /dev/null +++ b/src/components/CippComponents/CippScheduledTaskActions.jsx @@ -0,0 +1,58 @@ +import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; +import { usePermissions } from "../../hooks/use-permissions"; + +export const CippScheduledTaskActions = () => { + const { checkPermissions } = usePermissions(); + const canWriteScheduler = checkPermissions(["CIPP.Scheduler.ReadWrite"]); + const canReadScheduler = checkPermissions(["CIPP.Scheduler.Read", "CIPP.Scheduler.ReadWrite"]); + + return [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + condition: () => canReadScheduler, + }, + { + label: "Run Now", + type: "POST", + url: "/api/AddScheduledItem", + data: { RowKey: "RowKey", RunNow: true }, + icon: , + confirmText: "Are you sure you want to run [Name]?", + allowResubmit: true, + condition: () => canWriteScheduler, + }, + { + label: "Edit Job", + link: "/cipp/scheduler/job?id=[RowKey]", + multiPost: false, + icon: , + color: "success", + showInActionsMenu: true, + condition: () => canWriteScheduler, + }, + { + label: "Clone and Edit Job", + link: "/cipp/scheduler/job?id=[RowKey]&Clone=True", + multiPost: false, + icon: , + color: "success", + showInActionsMenu: true, + condition: () => canWriteScheduler, + }, + { + label: "Delete Job", + icon: , + type: "POST", + url: "/api/RemoveScheduledItem", + data: { id: "RowKey" }, + confirmText: "Are you sure you want to delete this job?", + multiPost: false, + condition: () => canWriteScheduler, + }, + ]; +}; + +export default CippScheduledTaskActions; diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx index 563983ba6e10..bb62097e02ca 100644 --- a/src/components/CippComponents/CippSettingsSideBar.jsx +++ b/src/components/CippComponents/CippSettingsSideBar.jsx @@ -29,9 +29,38 @@ export const CippSettingsSideBar = (props) => { relatedQueryKeys: "userSettings", }); const handleSaveChanges = () => { + const formValues = formcontrol.getValues(); + + // Only include the specific form fields from preferences.js to avoid unmapped data + const currentSettings = { + // General Settings + usageLocation: formValues.usageLocation, + tablePageSize: formValues.tablePageSize, + userAttributes: formValues.userAttributes, + + // Offboarding Defaults + offboardingDefaults: { + ConvertToShared: formValues.offboardingDefaults?.ConvertToShared, + RemoveGroups: formValues.offboardingDefaults?.RemoveGroups, + HideFromGAL: formValues.offboardingDefaults?.HideFromGAL, + RemoveLicenses: formValues.offboardingDefaults?.RemoveLicenses, + removeCalendarInvites: formValues.offboardingDefaults?.removeCalendarInvites, + RevokeSessions: formValues.offboardingDefaults?.RevokeSessions, + removePermissions: formValues.offboardingDefaults?.removePermissions, + RemoveRules: formValues.offboardingDefaults?.RemoveRules, + ResetPass: formValues.offboardingDefaults?.ResetPass, + KeepCopy: formValues.offboardingDefaults?.KeepCopy, + DeleteUser: formValues.offboardingDefaults?.DeleteUser, + RemoveMobile: formValues.offboardingDefaults?.RemoveMobile, + DisableSignIn: formValues.offboardingDefaults?.DisableSignIn, + RemoveMFADevices: formValues.offboardingDefaults?.RemoveMFADevices, + ClearImmutableId: formValues.offboardingDefaults?.ClearImmutableId, + }, + }; + const shippedValues = { user: formcontrol.getValues("user").value, - currentSettings: formcontrol.getValues(), + currentSettings: currentSettings, }; saveSettingsPost.mutate({ url: "/api/ExecUserSettings", data: shippedValues }); }; diff --git a/src/components/CippComponents/CippTemplateEditor.jsx b/src/components/CippComponents/CippTemplateEditor.jsx new file mode 100644 index 000000000000..e223abf49962 --- /dev/null +++ b/src/components/CippComponents/CippTemplateEditor.jsx @@ -0,0 +1,317 @@ +import React, { useEffect, useState } from "react"; +import { Box, Typography, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; + +const CippTemplateEditor = ({ + templateId, + templateType, + apiConfig, + schemaConfig, + blacklistConfig, + priorityFields = [], + title, + backButtonTitle, + customDataFormatter, +}) => { + const [templateData, setTemplateData] = useState(null); + + // Default blacklist patterns that apply to all template types + const defaultBlacklistPatterns = [ + "id", + "createdDateTime", + "modifiedDateTime", + "@odata.*", + "GUID", + "Type", + "times", + "tenantFilter", + "*Id", + "*DateTime", + ]; + + // Combine default and custom blacklist patterns + const blacklistedFields = [ + ...defaultBlacklistPatterns, + ...(blacklistConfig?.patterns || []), + ]; + + const formControl = useForm({ mode: "onChange" }); + + // Fetch the template data + const templateQuery = ApiGetCall({ + url: `${apiConfig.fetchUrl}?${apiConfig.idParam}=${templateId}`, + queryKey: `${templateType}-${templateId}`, + enabled: !!templateId, + }); + + // Function to check if a field matches any blacklisted pattern (including wildcards) + const isFieldBlacklisted = (fieldName) => { + return blacklistedFields.some(pattern => { + if (pattern.includes('*')) { + // Convert wildcard pattern to regex + const regexPattern = pattern + .replace(/\*/g, '.*') + .replace(/\./g, '\\.'); + const regex = new RegExp(`^${regexPattern}$`, 'i'); + return regex.test(fieldName); + } + return pattern === fieldName; + }); + }; + + useEffect(() => { + if (templateQuery.isSuccess && templateQuery.data) { + // Find the template with matching ID + const template = Array.isArray(templateQuery.data) + ? templateQuery.data.find((t) => t[apiConfig.idParam] === templateId) + : templateQuery.data; + + if (template) { + setTemplateData(template); + // Set form values excluding blacklisted fields + const formValues = {}; + Object.keys(template).forEach((key) => { + if (!isFieldBlacklisted(key)) { + formValues[key] = template[key]; + } + }); + formControl.reset(formValues); + } + } + }, [templateQuery.isSuccess, templateQuery.data, templateId]); + + const renderFormField = (key, value, path = "") => { + const fieldPath = path ? `${path}.${key}` : key; + + if (isFieldBlacklisted(key)) { + return null; + } + + // Check for custom schema handling + const schemaField = schemaConfig?.fields?.[key.toLowerCase()]; + if (schemaField) { + return ( + + + + ); + } + + // Special handling for complex array fields (like LocationInfo and GroupInfo) + if (schemaConfig?.complexArrayFields?.some(pattern => + key.toLowerCase().includes(pattern.toLowerCase()) + )) { + // Don't render if value is null, undefined, empty array, or contains only null/empty items + if ( + !value || + (Array.isArray(value) && value.length === 0) || + (Array.isArray(value) && + value.every( + (item) => + item === null || + item === undefined || + (typeof item === "string" && item.trim() === "") || + (typeof item === "object" && item !== null && Object.keys(item).length === 0) + )) + ) { + return null; + } + + return ( + + + {getCippTranslation(key)} + + + + {Array.isArray(value) ? ( + value + .filter( + (item) => + item !== null && + item !== undefined && + !(typeof item === "string" && item.trim() === "") && + !(typeof item === "object" && item !== null && Object.keys(item).length === 0) + ) + .map((item, index) => ( + + + {getCippTranslation(key)} {index + 1} + + + {typeof item === "object" && item !== null ? ( + Object.entries(item).map(([subKey, subValue]) => + renderFormField(subKey, subValue, `${fieldPath}.${index}`) + ) + ) : ( + + + + )} + + + )) + ) : ( + + + No {getCippTranslation(key)} data available + + + )} + + + ); + } + + // Generic field type handling + if (typeof value === "boolean") { + return ( + + + + ); + } + + if (typeof value === "string") { + return ( + + + + ); + } + + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + return ( + + ({ label: item, value: item }))} + /> + + ); + } + + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return ( + + + {getCippTranslation(key)} + + + + {Object.entries(value).map(([subKey, subValue]) => + renderFormField(subKey, subValue, fieldPath) + )} + + + ); + } + + // For other types (numbers, complex arrays, etc.), render as text field + return ( + + + + ); + }; + + const defaultDataFormatter = (values) => { + return { + [apiConfig.idParam]: templateId, + ...values, + }; + }; + + if (templateQuery.isLoading) { + return ( + + + + ); + } + + if (templateQuery.isError || !templateData) { + return ( + + + Error loading template or template not found. + + + ); + } + + return ( + + + + Edit the properties of this template. Only editable properties are shown below. + + + + {templateData && ( + <> + {/* Render priority fields first */} + {priorityFields.map(fieldName => + templateData[fieldName] !== undefined && + renderFormField(fieldName, templateData[fieldName]) + )} + + {/* Render all other fields except priority fields */} + {Object.entries(templateData) + .filter(([key]) => !priorityFields.includes(key)) + .map(([key, value]) => renderFormField(key, value))} + + )} + + + + ); +}; + +export default CippTemplateEditor; \ No newline at end of file diff --git a/src/components/CippComponents/CippTemplateFieldRenderer.jsx b/src/components/CippComponents/CippTemplateFieldRenderer.jsx new file mode 100644 index 000000000000..9db3cfe458f8 --- /dev/null +++ b/src/components/CippComponents/CippTemplateFieldRenderer.jsx @@ -0,0 +1,700 @@ +import React from "react"; +import { Typography, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; +import intuneCollection from "/src/data/intuneCollection.json"; + +const CippTemplateFieldRenderer = ({ + templateData, + formControl, + templateType = "conditionalAccess", +}) => { + // Default blacklisted fields with wildcard support + const defaultBlacklistedFields = [ + "id", + "isAssigned", + "createdDateTime", + "modifiedDateTime", + "@odata.*", + "GUID", + "Type", + "times", + "tenantFilter", + "*Id", + "*DateTime", + ]; + + // Template-specific configurations + const templateConfigs = { + conditionalAccess: { + blacklistedFields: [ + ...defaultBlacklistedFields, + "membershipKind", + "countryLookupMethod", + "applicationFilter", + "includeAuthenticationContextClassReferences", + ], + priorityFields: ["displayName", "state", "DisplayName", "Name", "displayname"], + complexArrayFields: ["locationinfo", "groupinfo"], + schemaFields: { + operator: { + multiple: false, + options: [ + { label: "OR", value: "OR" }, + { label: "AND", value: "AND" }, + ], + }, + builtincontrols: { + multiple: true, + options: [ + { label: "Block", value: "block" }, + { label: "Multi-factor Authentication", value: "mfa" }, + { label: "Compliant Device", value: "compliantDevice" }, + { label: "Domain Joined Device", value: "domainJoinedDevice" }, + { label: "Approved Application", value: "approvedApplication" }, + { label: "Compliant Application", value: "compliantApplication" }, + { label: "Password Change", value: "passwordChange" }, + { label: "Unknown Future Value", value: "unknownFutureValue" }, + ], + }, + authenticationtype: { + multiple: false, + options: [ + { + label: "Primary and Secondary Authentication", + value: "primaryAndSecondaryAuthentication", + }, + { label: "Secondary Authentication", value: "secondaryAuthentication" }, + { label: "Unknown Future Value", value: "unknownFutureValue" }, + ], + }, + frequencyinterval: { + multiple: false, + options: [ + { label: "Time Based", value: "timeBased" }, + { label: "Every Time", value: "everyTime" }, + { label: "Unknown Future Value", value: "unknownFutureValue" }, + ], + }, + state: { + multiple: false, + options: [ + { label: "Enabled", value: "enabled" }, + { label: "Disabled", value: "disabled" }, + { label: "Enabled for Reporting", value: "enabledForReportingButNotEnforced" }, + ], + }, + }, + }, + intune: { + blacklistedFields: [ + ...defaultBlacklistedFields, + "deviceManagementApplicabilityRuleOsEdition", + "deviceManagementApplicabilityRuleOsVersion", + "deviceManagementApplicabilityRuleDeviceMode", + "roleScopeTagIds", + "supportsScopeTags", + "deviceSettingStateSummaries", + "RAWJson", // Handle RAWJson specially + ], + priorityFields: ["displayName", "description", "DisplayName", "Name", "displayname"], + complexArrayFields: ["assignments", "devicestatusoverview"], + schemaFields: { + devicecompliancepolicystate: { + multiple: false, + options: [ + { label: "Unknown", value: "unknown" }, + { label: "Compliant", value: "compliant" }, + { label: "Noncompliant", value: "noncompliant" }, + { label: "Conflict", value: "conflict" }, + { label: "Error", value: "error" }, + { label: "In Grace Period", value: "inGracePeriod" }, + { label: "Config Manager", value: "configManager" }, + ], + }, + // Common device policy enum values + applicationguardenabledoptions: { + multiple: false, + options: [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Enabled for Edge", value: "enabledForEdge" }, + { label: "Enabled for Office", value: "enabledForOffice" }, + { label: "Enabled for Edge and Office", value: "enabledForEdgeAndOffice" }, + ], + }, + firewallcertificaterevocationlistcheckmethod: { + multiple: false, + options: [ + { label: "Device Default", value: "deviceDefault" }, + { label: "None", value: "none" }, + { label: "Attempt", value: "attempt" }, + { label: "Require", value: "require" }, + ], + }, + firewallpacketqueueingmethod: { + multiple: false, + options: [ + { label: "Device Default", value: "deviceDefault" }, + { label: "Disabled", value: "disabled" }, + { label: "Queue Inbound", value: "queueInbound" }, + { label: "Queue Outbound", value: "queueOutbound" }, + { label: "Queue Both", value: "queueBoth" }, + ], + }, + startupmode: { + multiple: false, + options: [ + { label: "Manual", value: "manual" }, + { label: "Automatic", value: "automatic" }, + { label: "Disabled", value: "disabled" }, + ], + }, + applicationguardblockclipboardsharing: { + multiple: false, + options: [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Block Both", value: "blockBoth" }, + { label: "Block Host to Container", value: "blockHostToContainer" }, + { label: "Block Container to Host", value: "blockContainerToHost" }, + { label: "Block None", value: "blockNone" }, + ], + }, + bitlockerrecoverypasswordrotation: { + multiple: false, + options: [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Disabled", value: "disabled" }, + { label: "Enabled for Azure AD Joined", value: "enabledForAzureAd" }, + { + label: "Enabled for Azure AD and Hybrid Joined", + value: "enabledForAzureAdAndHybrid", + }, + ], + }, + bitlockerprebootrecoverymsgurloption: { + multiple: false, + options: [ + { label: "Default", value: "default" }, + { label: "Use Custom", value: "useCustom" }, + { label: "No URL", value: "noUrl" }, + ], + }, + }, + }, + exchange: { + blacklistedFields: [ + ...defaultBlacklistedFields, + "ExchangeVersion", + "DistinguishedName", + "ObjectCategory", + "WhenChanged", + "WhenCreated", + ], + priorityFields: ["Name", "Identity"], + complexArrayFields: ["accepteddomains", "remotedomain"], + schemaFields: {}, + }, + }; + + // Get configuration for the current template type + const config = templateConfigs[templateType] || templateConfigs.conditionalAccess; + const { blacklistedFields, priorityFields, complexArrayFields, schemaFields } = config; + + // Function to check if a field matches any blacklisted pattern (including wildcards) + const isFieldBlacklisted = (fieldName) => { + return blacklistedFields.some((pattern) => { + if (pattern.includes("*")) { + // Convert wildcard pattern to regex + const regexPattern = pattern.replace(/\*/g, ".*").replace(/\./g, "\\."); + const regex = new RegExp(`^${regexPattern}$`, "i"); + return regex.test(fieldName); + } + return pattern === fieldName; + }); + }; + + // Parse RAWJson for Intune templates + const parseIntuneRawJson = (templateData) => { + if (templateType === "intune" && templateData.RAWJson) { + try { + const parsedJson = JSON.parse(templateData.RAWJson); + return { + ...templateData, + parsedRAWJson: parsedJson, + }; + } catch (error) { + console.warn("Failed to parse RAWJson:", error); + return templateData; + } + } + return templateData; + }; + + // Reset form with filtered values when templateData changes + React.useEffect(() => { + if (templateData && formControl) { + const processedData = parseIntuneRawJson(templateData); + const formValues = {}; + + Object.keys(processedData).forEach((key) => { + if (!isFieldBlacklisted(key)) { + formValues[key] = processedData[key]; + } + }); + formControl.reset(formValues); + } + }, [templateData]); + + const renderFormField = (key, value, path = "") => { + const fieldPath = path ? `${path}.${key}` : key; + + if (isFieldBlacklisted(key)) { + return null; + } + + // Check for custom schema handling + const schemaField = schemaFields[key.toLowerCase()]; + if (schemaField) { + return ( + + + + ); + } + + // Special handling for Intune RAWJson structure + if (templateType === "intune" && key === "parsedRAWJson" && value) { + // Check if this is a classic policy (has 'added' array) - these are not editable + if (value.added) { + return ( + + + This is a legacy policy and the settings cannot be edited through the form interface. + + + ); + } + + // Handle modern policies with settings array + if (value.settings && Array.isArray(value.settings)) { + return ( + + + Policy Settings + + + + {value.settings.map((setting, index) => { + const settingInstance = setting.settingInstance; + if (!settingInstance) return null; + + // Handle different setting types + if (settingInstance.choiceSettingValue) { + // Find the setting definition in the intune collection + const intuneObj = intuneCollection.find( + (item) => item.id === settingInstance.settingDefinitionId + ); + + const label = intuneObj?.displayName || `Setting ${index + 1}`; + const options = + intuneObj?.options?.map((option) => ({ + label: option.displayName || option.id, + value: option.id, + })) || []; + + return ( + + + + ); + } + + if (settingInstance.simpleSettingValue) { + // Find the setting definition in the intune collection + const intuneObj = intuneCollection.find( + (item) => item.id === settingInstance.settingDefinitionId + ); + + const label = intuneObj?.displayName || `Setting ${index + 1}`; + + return ( + + + + ); + } + + // Handle group setting collections + if (settingInstance.groupSettingCollectionValue) { + // Find the setting definition in the intune collection + const intuneObj = intuneCollection.find( + (item) => item.id === settingInstance.settingDefinitionId + ); + + const label = intuneObj?.displayName || `Group Setting Collection ${index + 1}`; + + return ( + + + {label} + + + Definition ID: {settingInstance.settingDefinitionId} + + {/* Group collections are complex - show as read-only for now */} + + Complex group setting collection - view in JSON mode for details + + + ); + } + + return null; + })} + + + ); + } + + // Handle OMA settings + if (value.omaSettings && Array.isArray(value.omaSettings)) { + return ( + + + OMA Settings + + + + {value.omaSettings.map((omaSetting, index) => ( + + + {omaSetting.displayName || `OMA Setting ${index + 1}`} + + + + + + + + + + + ))} + + + ); + } + + // Handle device policies (direct configuration properties) + if (!value.settings && !value.omaSettings && !value.added) { + return ( + + + Device Policy Configuration + + + + {Object.entries(value) + .filter(([deviceKey]) => !isFieldBlacklisted(deviceKey)) + .map(([deviceKey, deviceValue]) => + renderFormField(deviceKey, deviceValue, fieldPath) + )} + + + ); + } + + // Fallback for other RAWJson structures + return ( + + + Policy Configuration + + + + This policy structure is not supported for editing. + + + ); + } + + // Special handling for complex array fields + if (complexArrayFields.some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()))) { + // Don't render if value is null, undefined, empty array, or contains only null/empty items + if ( + !value || + (Array.isArray(value) && value.length === 0) || + (Array.isArray(value) && + value.every( + (item) => + item === null || + item === undefined || + (typeof item === "string" && item.trim() === "") || + (typeof item === "object" && item !== null && Object.keys(item).length === 0) + )) + ) { + return null; + } + + return ( + + + {getCippTranslation(key)} + + + + {Array.isArray(value) ? ( + value + .filter( + (item) => + item !== null && + item !== undefined && + !(typeof item === "string" && item.trim() === "") && + !(typeof item === "object" && item !== null && Object.keys(item).length === 0) + ) + .map((item, index) => ( + + + {getCippTranslation(key)} {index + 1} + + + {typeof item === "object" && item !== null ? ( + Object.entries(item).map(([subKey, subValue]) => + renderFormField(subKey, subValue, `${fieldPath}.${index}`) + ) + ) : ( + + + + )} + + + )) + ) : ( + + + No {getCippTranslation(key)} data available + + + )} + + + ); + } + + // Generic field type handling + if (typeof value === "boolean") { + return ( + + + + ); + } + + if (typeof value === "string") { + const alwaysTextFields = [ + "displayname", + "displayName", + "name", + "description", + "identity", + "title", + ]; + + const isAlwaysTextField = alwaysTextFields.some( + (field) => key.toLowerCase() === field.toLowerCase() + ); + + // Check if this looks like an enum value (common patterns in device policies) + const enumPatterns = [ + "notConfigured", + "deviceDefault", + "manual", + "automatic", + "disabled", + "enabled", + "blocked", + "allowed", + "required", + "none", + "lockWorkstation", + ]; + + const looksLikeEnum = enumPatterns.some((pattern) => + value.toLowerCase().includes(pattern.toLowerCase()) + ); + + if (!isAlwaysTextField && looksLikeEnum) { + // Create basic options based on common patterns + const commonOptions = [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Device Default", value: "deviceDefault" }, + { label: "Manual", value: "manual" }, + { label: "Automatic", value: "automatic" }, + { label: "Disabled", value: "disabled" }, + { label: "Enabled", value: "enabled" }, + { label: "Blocked", value: "blocked" }, + { label: "Allowed", value: "allowed" }, + { label: "Required", value: "required" }, + { label: "None", value: "none" }, + ].filter( + (option) => + // Only include options that make sense for this field + option.value === value || + key.toLowerCase().includes(option.value.toLowerCase()) || + option.value === "notConfigured" // Always include notConfigured + ); + + return ( + + + + ); + } + + return ( + + + + ); + } + + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + return ( + + ({ label: item, value: item }))} + /> + + ); + } + + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return ( + + + {getCippTranslation(key)} + + + + {Object.entries(value).map(([subKey, subValue]) => + renderFormField(subKey, subValue, fieldPath) + )} + + + ); + } + + // For other types (numbers, complex arrays, etc.), render as text field + return ( + + + + ); + }; + + if (!templateData) { + return null; + } + + // Process template data (parse RAWJson for Intune templates) + const processedData = parseIntuneRawJson(templateData); + + return ( + + {/* Render priority fields first */} + {priorityFields.map( + (fieldName) => + processedData[fieldName] !== undefined && + renderFormField(fieldName, processedData[fieldName]) + )} + + {/* Render all other fields except priority fields */} + {Object.entries(processedData) + .filter(([key]) => !priorityFields.includes(key)) + .map(([key, value]) => renderFormField(key, value))} + + ); +}; + +export default CippTemplateFieldRenderer; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 28f4f654b6b9..1735e7717435 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -1,7 +1,6 @@ import { EyeIcon, MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; import { Archive, - Block, Clear, CloudDone, Edit, @@ -9,7 +8,6 @@ import { ForwardToInbox, GroupAdd, LockClock, - LockOpen, LockPerson, LockReset, MeetingRoom, @@ -20,10 +18,18 @@ import { PhonelinkSetup, Shortcut, } from "@mui/icons-material"; +import { getCippLicenseTranslation } from "../../utils/get-cipp-license-translation"; import { useSettings } from "/src/hooks/use-settings.js"; +import { usePermissions } from "../../hooks/use-permissions"; export const CippUserActions = () => { const tenant = useSettings().currentTenant; + + const { checkPermissions } = usePermissions(); + const canWriteUser = checkPermissions(["Identity.User.ReadWrite"]); + const canWriteMailbox = checkPermissions(["Exchange.Mailbox.ReadWrite"]); + const canWriteGroup = checkPermissions(["Identity.Group.ReadWrite"]); + return [ { //tested @@ -40,6 +46,7 @@ export const CippUserActions = () => { icon: , color: "success", target: "_self", + condition: () => canWriteUser, }, { //tested @@ -62,22 +69,23 @@ export const CippUserActions = () => { type: "number", name: "lifetimeInMinutes", label: "Lifetime (Minutes)", - placeholder: "Leave blank for default" + placeholder: "Leave blank for default", }, { type: "switch", name: "isUsableOnce", - label: "One-time use only" + label: "One-time use only", }, { type: "datePicker", name: "startDateTime", label: "Start Date/Time (leave blank for immediate)", - dateTimeType: "datetime" - } + dateTimeType: "datetime", + }, ], confirmText: "Are you sure you want to create a Temporary Access Password?", multiPost: false, + condition: () => canWriteUser, }, { //tested @@ -88,6 +96,7 @@ export const CippUserActions = () => { data: { ID: "userPrincipalName" }, confirmText: "Are you sure you want to reset MFA for this user?", multiPost: false, + condition: () => canWriteUser, }, { //tested @@ -118,29 +127,37 @@ export const CippUserActions = () => { ], multiple: false, creatable: false, + validators: { required: "Please select an MFA state" }, }, ], confirmText: "Are you sure you want to set per-user MFA for these users?", multiPost: false, + condition: () => canWriteUser, }, { //tested - label: "Convert to Shared Mailbox", + label: "Convert Mailbox", type: "POST", icon: , url: "/api/ExecConvertMailbox", - data: { ID: "userPrincipalName", MailboxType: "!Shared" }, - confirmText: "Are you sure you want to convert this user to a shared mailbox?", - multiPost: false, - }, - { - label: "Convert to User Mailbox", - type: "POST", - icon: , - url: "/api/ExecConvertMailbox", - data: { ID: "userPrincipalName", MailboxType: "!Regular" }, - confirmText: "Are you sure you want to convert this user to a user mailbox?", + data: { ID: "userPrincipalName" }, + fields: [ + { + type: "radio", + name: "MailboxType", + label: "Mailbox Type", + options: [ + { label: "User Mailbox", value: "Regular" }, + { label: "Shared Mailbox", value: "Shared" }, + { label: "Room Mailbox", value: "Room" }, + { label: "Equipment Mailbox", value: "Equipment" }, + ], + validators: { required: "Please select a mailbox type" }, + }, + ], + confirmText: "Pick the type of mailbox you want to convert [userPrincipalName] to:", multiPost: false, + condition: () => canWriteMailbox, }, { //tested @@ -149,8 +166,9 @@ export const CippUserActions = () => { icon: , url: "/api/ExecEnableArchive", data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to enable the online archive for this user?", + confirmText: "Are you sure you want to enable the online archive for [userPrincipalName]?", multiPost: false, + condition: (row) => canWriteMailbox, }, { //tested @@ -166,6 +184,7 @@ export const CippUserActions = () => { fields: [{ type: "richText", name: "input", label: "Out of Office Message" }], confirmText: "Are you sure you want to set the out of office?", multiPost: false, + condition: () => canWriteMailbox, }, { @@ -177,8 +196,9 @@ export const CippUserActions = () => { userId: "userPrincipalName", AutoReplyState: { value: "Disabled" }, }, - confirmText: "Are you sure you want to disable the out of office?", + confirmText: "Are you sure you want to disable the out of office for [userPrincipalName]?", multiPost: false, + condition: () => canWriteMailbox, }, { label: "Add to Group", @@ -191,18 +211,22 @@ export const CippUserActions = () => { row .map((r) => ({ label: r.displayName, - value: r.userPrincipalName, + value: r.id, addedFields: { id: r.id, + userPrincipalName: r.userPrincipalName, + displayName: r.displayName, }, })) .forEach((r) => addMember.push(r)); } else { addMember.push({ label: row.displayName, - value: row.userPrincipalName, + value: row.id, addedFields: { id: row.id, + userPrincipalName: row.userPrincipalName, + displayName: row.displayName, }, }); } @@ -219,6 +243,7 @@ export const CippUserActions = () => { label: "Select a group to add the user to", multiple: false, creatable: false, + validators: { required: "Please select a group" }, api: { url: "/api/ListGroups", labelField: "displayName", @@ -232,8 +257,10 @@ export const CippUserActions = () => { }, }, ], - confirmText: "Are you sure you want to add the user to this group?", + confirmText: "Are you sure you want to add [userPrincipalName] to this group?", multiPost: true, + allowResubmit: true, + condition: () => canWriteGroup, }, { label: "Manage Licenses", @@ -252,7 +279,7 @@ export const CippUserActions = () => { { label: "Remove Licenses", value: "Remove" }, { label: "Replace Licenses", value: "Replace" }, ], - required: true, + validators: { required: "Please select a license operation" }, }, { type: "switch", @@ -267,7 +294,8 @@ export const CippUserActions = () => { creatable: false, api: { url: "/api/ListLicenses", - labelField: "skuPartNumber", + labelField: (option) => + `${getCippLicenseTranslation([option])} (${option?.availableUnits} available)`, valueField: "skuId", queryKey: `licenses-${tenant}`, }, @@ -275,6 +303,7 @@ export const CippUserActions = () => { ], confirmText: "Are you sure you want to manage licenses for the selected users?", multiPost: true, + condition: () => canWriteUser, }, { label: "Disable Email Forwarding", @@ -286,8 +315,9 @@ export const CippUserActions = () => { userid: "userPrincipalName", ForwardOption: "!disabled", }, - confirmText: "Are you sure you want to disable forwarding of this user's emails?", + confirmText: "Are you sure you want to disable forwarding of [userPrincipalName]'s emails?", multiPost: false, + condition: () => canWriteMailbox, }, { label: "Pre-provision OneDrive", @@ -297,6 +327,7 @@ export const CippUserActions = () => { data: { UserPrincipalName: "userPrincipalName" }, confirmText: "Are you sure you want to pre-provision OneDrive for this user?", multiPost: false, + condition: () => canWriteUser, }, { label: "Add OneDrive Shortcut", @@ -314,6 +345,7 @@ export const CippUserActions = () => { label: "Select a Site", multiple: false, creatable: true, + validators: { required: "Please select or enter a SharePoint site URL" }, api: { url: "/api/ListSites", data: { type: "SharePointSiteUsage", URLOnly: true }, @@ -325,40 +357,29 @@ export const CippUserActions = () => { ], confirmText: "Select a SharePoint site to create a shortcut for:", multiPost: false, + condition: () => canWriteUser, }, { - label: "Block Sign In", + label: "Set Sign In State", type: "POST", - icon: , + icon: , url: "/api/ExecDisableUser", data: { ID: "id" }, - confirmText: "Are you sure you want to block the sign-in for this user?", - multiPost: false, - condition: (row) => row.accountEnabled, - }, - { - label: "Unblock Sign In", - type: "POST", - icon: , - url: "/api/ExecDisableUser", - data: { ID: "id", Enable: true }, - confirmText: "Are you sure you want to unblock sign-in for this user?", - multiPost: false, - condition: (row) => !row.accountEnabled, - }, - { - label: "Reset Password (Must Change)", - type: "POST", - icon: , - url: "/api/ExecResetPass", - data: { - MustChange: true, - ID: "userPrincipalName", - displayName: "displayName", - }, - confirmText: - "Are you sure you want to reset the password for this user? The user must change their password at next logon.", + fields: [ + { + type: "radio", + name: "Enable", + label: "Sign In State", + options: [ + { label: "Enabled", value: true }, + { label: "Disabled", value: false }, + ], + validators: { required: "Please select a sign-in state" }, + }, + ], + confirmText: "Are you sure you want to set the sign-in state for [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, }, { label: "Reset Password", @@ -366,35 +387,42 @@ export const CippUserActions = () => { icon: , url: "/api/ExecResetPass", data: { - MustChange: false, ID: "userPrincipalName", displayName: "displayName", }, - confirmText: "Are you sure you want to reset the password for this user?", + fields: [ + { + type: "switch", + name: "MustChange", + label: "Must Change Password at Next Logon", + }, + ], + confirmText: "Are you sure you want to reset the password for [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, }, { - label: "Set Password Never Expires", + label: "Set Password Expiration", type: "POST", icon: , url: "/api/ExecPasswordNeverExpires", data: { userId: "id", userPrincipalName: "userPrincipalName" }, fields: [ { - type: "autoComplete", + type: "radio", name: "PasswordPolicy", label: "Password Policy", options: [ { label: "Disable Password Expiration", value: "DisablePasswordExpiration" }, { label: "Enable Password Expiration", value: "None" }, ], - multiple: false, - creatable: false, + validators: { required: "Please select a password policy" }, }, ], confirmText: - "Set Password Never Expires state for this user. If the password of the user is older than the set expiration date of the organization, the user will be prompted to change their password at their next login.", + "Set Password Never Expires state for [userPrincipalName]. If the password of the user is older than the set expiration date of the organization, the user will be prompted to change their password at their next login.", multiPost: false, + condition: () => canWriteUser, }, { label: "Clear Immutable ID", @@ -404,9 +432,9 @@ export const CippUserActions = () => { data: { ID: "id", }, - confirmText: "Are you sure you want to clear the Immutable ID for this user?", + confirmText: "Are you sure you want to clear the Immutable ID for [userPrincipalName]?", multiPost: false, - condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId, + condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId && canWriteUser, }, { label: "Revoke all user sessions", @@ -414,8 +442,9 @@ export const CippUserActions = () => { icon: , url: "/api/ExecRevokeSessions", data: { ID: "id", Username: "userPrincipalName" }, - confirmText: "Are you sure you want to revoke all sessions for this user?", + confirmText: "Are you sure you want to revoke all sessions for [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, }, { label: "Delete User", @@ -423,8 +452,9 @@ export const CippUserActions = () => { icon: , url: "/api/RemoveUser", data: { ID: "id", userPrincipalName: "userPrincipalName" }, - confirmText: "Are you sure you want to delete this user?", + confirmText: "Are you sure you want to delete [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, }, ]; }; diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx index 88aabea0f512..8e3f50161694 100644 --- a/src/components/CippComponents/ScheduledTaskDetails.jsx +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -20,8 +20,10 @@ import { ExpandMore, Sync, Search, Close } from "@mui/icons-material"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; import { CippDataTable } from "../CippTable/CippDataTable"; import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; +import { ActionsMenu } from "/src/components/actions-menu"; +import { CippScheduledTaskActions } from "./CippScheduledTaskActions"; -const ScheduledTaskDetails = ({ data }) => { +const ScheduledTaskDetails = ({ data, showActions = true }) => { const [taskDetails, setTaskDetails] = useState(null); const [expanded, setExpanded] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -79,7 +81,20 @@ const ScheduledTaskDetails = ({ data }) => { return ( <> - {taskDetails?.Task?.Name} + + + {taskDetailResults.isLoading ? : taskDetails?.Task?.Name} + + {showActions && ( + + + + )} + @@ -206,7 +221,9 @@ const ScheduledTaskDetails = ({ data }) => { }, }} > - {result.TenantName || result.Tenant} + + {getCippFormatting(result.TenantName || result.Tenant, "Tenant")} + { /> - {result.Results === "null" ? ( + {result.Results === "null" || !result.Results ? ( No data available ) : Array.isArray(result.Results) ? ( { + + return ( + + + + + + + + @, + }} + /> + + + + + + + + + ); +}; + +export default CippAddRoomListForm; \ No newline at end of file diff --git a/src/components/CippFormPages/CippJSONView.jsx b/src/components/CippFormPages/CippJSONView.jsx index e590f3ae2ed6..f6d206703e14 100644 --- a/src/components/CippFormPages/CippJSONView.jsx +++ b/src/components/CippFormPages/CippJSONView.jsx @@ -519,7 +519,6 @@ function CippJsonView({ ?.filter((data) => data !== null && data !== undefined) .map((data, index) => ( { // Find tenantFilter in tenantList, and create a label/value pair for the autocomplete if (tenantList.isSuccess) { - const tenantFilter = tenantList.data.find( - (tenant) => tenant.defaultDomainName === task?.Tenant - ); + let tenantFilter = null; + let tenantFilterForForm = null; + + // Check if the task has a tenant group + if (task?.TenantGroupInfo) { + // Handle tenant group + tenantFilterForForm = { + value: task.TenantGroupInfo.value, + label: task.TenantGroupInfo.label, + type: "Group", + addedFields: task.TenantGroupInfo, + }; + } else { + // Handle regular tenant + tenantFilter = tenantList.data.find( + (tenant) => + tenant.defaultDomainName === task?.Tenant.value || + tenant.defaultDomainName === task?.Tenant + ); + if (tenantFilter) { + tenantFilterForForm = { + value: tenantFilter.defaultDomainName, + label: `${tenantFilter.displayName} (${tenantFilter.defaultDomainName})`, + type: "Tenant", + addedFields: tenantFilter, + }; + } + } if (commands.isSuccess) { const command = commands.data.find((command) => command.Function === task.Command); @@ -127,11 +152,34 @@ const CippSchedulerForm = (props) => { task.ScheduledTime = Math.floor(new Date(task.ScheduledTime).getTime() / 1000); } + // Check if any parameter values are complex objects that can't be represented as simple form fields + const hasComplexObjects = + task.Parameters && typeof task.Parameters === "object" + ? Object.values(task.Parameters).some((value) => { + // Check for arrays + if (Array.isArray(value)) return true; + // Check for objects (but not null) + if (value !== null && typeof value === "object") return true; + // Check for stringified objects that contain [object Object] + if (typeof value === "string" && value.includes("[object Object]")) return true; + // Check for stringified JSON arrays/objects + if ( + typeof value === "string" && + (value.trim().startsWith("[") || value.trim().startsWith("{")) + ) { + try { + const parsed = JSON.parse(value); + return typeof parsed === "object"; + } catch { + return false; + } + } + return false; + }) + : false; + const ResetParams = { - tenantFilter: { - value: tenantFilter?.defaultDomainName, - label: `${tenantFilter?.displayName} (${tenantFilter?.defaultDomainName})`, - }, + tenantFilter: tenantFilterForForm, RowKey: router.query.Clone ? null : task.RowKey, Name: router.query.Clone ? `${task.Name} (Clone)` : task?.Name, command: { label: task.Command, value: task.Command, addedFields: commandForForm }, @@ -139,10 +187,17 @@ const CippSchedulerForm = (props) => { Recurrence: recurrence, parameters: task.Parameters, postExecution: postExecution, - // Show advanced parameters if RawJsonParameters exist OR if it's a system command with no defined parameters + // Show advanced parameters if: + // 1. RawJsonParameters exist + // 2. It's a system command with no defined parameters + // 3. Any parameter contains complex objects (arrays, objects, etc.) advancedParameters: task.RawJsonParameters ? true - : !commandForForm?.Parameters || commandForForm.Parameters.length === 0, + : hasComplexObjects || + !commandForForm?.Parameters || + commandForForm.Parameters.length === 0, + // Set the RawJsonParameters if they exist + RawJsonParameters: task.RawJsonParameters || "", }; formControl.reset(ResetParams); } @@ -160,22 +215,50 @@ const CippSchedulerForm = (props) => { useEffect(() => { if (advancedParameters === true) { - var schedulerValues = formControl.getValues("parameters"); - // Add null check to prevent error when no command is selected - if (schedulerValues && typeof schedulerValues === "object") { - Object.keys(schedulerValues).forEach((key) => { - if (schedulerValues[key] === "" || schedulerValues[key] === null) { - delete schedulerValues[key]; + // Check if we're editing an existing task and it has RawJsonParameters + const currentRawJsonParameters = formControl.getValues("RawJsonParameters"); + + // If we already have raw JSON parameters (from editing existing task), use those + if ( + currentRawJsonParameters && + currentRawJsonParameters.trim() !== "" && + currentRawJsonParameters !== "{}" + ) { + // Already populated from existing task, no need to overwrite + return; + } + + // Get the original task parameters if we're editing (to preserve complex objects) + let parametersToUse = null; + if (router.query.id && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + if (task?.Parameters) { + parametersToUse = task.Parameters; + } + } + + // If we don't have original task parameters, use current form parameters + if (!parametersToUse) { + parametersToUse = formControl.getValues("parameters"); + } + + // Add null check to prevent error when no parameters exist + if (parametersToUse && typeof parametersToUse === "object") { + // Create a clean copy for JSON + const cleanParams = { ...parametersToUse }; + Object.keys(cleanParams).forEach((key) => { + if (cleanParams[key] === "" || cleanParams[key] === null) { + delete cleanParams[key]; } }); - const jsonString = JSON.stringify(schedulerValues, null, 2); + const jsonString = JSON.stringify(cleanParams, null, 2); formControl.setValue("RawJsonParameters", jsonString); } else { // If no parameters, set empty object formControl.setValue("RawJsonParameters", "{}"); } } - }, [advancedParameters]); + }, [advancedParameters, router.query.id, scheduledTaskList.isSuccess]); const gridSize = fullWidth ? 12 : 4; // Adjust size based on fullWidth prop @@ -191,6 +274,8 @@ const CippSchedulerForm = (props) => { formControl={formControl} type="single" allTenants={true} + includeGroups={true} + required={true} /> @@ -309,7 +394,6 @@ const CippSchedulerForm = (props) => { key={idx} > {param.Type === "System.Boolean" || @@ -378,7 +462,14 @@ const CippSchedulerForm = (props) => { }} formControl={formControl} multiline - rows={4} + rows={6} + maxRows={30} + sx={{ + "& .MuiInputBase-root": { + overflow: "auto", + minHeight: "200px", + }, + }} placeholder={`Enter a JSON object`} /> @@ -417,8 +508,10 @@ const CippSchedulerForm = (props) => { {router.query.id ? "Edit" : "Add"} Schedule
+ + + - ); }; diff --git a/src/components/CippSettings/CippBrandingSettings.jsx b/src/components/CippSettings/CippBrandingSettings.jsx index 1daadec108e8..a75330da986b 100644 --- a/src/components/CippSettings/CippBrandingSettings.jsx +++ b/src/components/CippSettings/CippBrandingSettings.jsx @@ -22,7 +22,7 @@ const CippBrandingSettings = () => { const saveBrandingSettings = ApiPostCall({ datafromUrl: true, - relatedQueryKeys: ["BrandingSettings"], + relatedQueryKeys: ["BrandingSettings", "userSettings"], }); const handleLogoUpload = (event) => { diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index ea3e80d2324b..b7fb9e7a2b27 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -149,27 +149,61 @@ export const CippRoleAddEdit = ({ selectedRole }) => { (role) => role.RowKey === selectedRole ); + // Process allowed tenants - handle both groups and tenant IDs var newAllowedTenants = []; - currentPermissions?.AllowedTenants.map((tenant) => { - var tenantInfo = tenants.find((t) => t.customerId === tenant); - var label = `${tenantInfo?.displayName} (${tenantInfo?.defaultDomainName})`; - if (tenantInfo?.displayName) { + currentPermissions?.AllowedTenants?.forEach((item) => { + if (typeof item === "object" && item.type === "Group") { + // Handle group objects newAllowedTenants.push({ - label: label, - value: tenantInfo.defaultDomainName, + label: item.label, + value: item.value, + type: "Group", }); + } else { + // Handle tenant customer IDs (legacy format) + var tenantInfo = tenants.find((t) => t.customerId === item); + if (tenantInfo?.displayName) { + var label = `${tenantInfo.displayName} (${tenantInfo.defaultDomainName})`; + newAllowedTenants.push({ + label: label, + value: tenantInfo.defaultDomainName, + type: "Tenant", + addedFields: { + defaultDomainName: tenantInfo.defaultDomainName, + displayName: tenantInfo.displayName, + customerId: tenantInfo.customerId, + }, + }); + } } }); + // Process blocked tenants - handle both groups and tenant IDs var newBlockedTenants = []; - currentPermissions?.BlockedTenants.map((tenant) => { - var tenantInfo = tenants.find((t) => t.customerId === tenant); - var label = `${tenantInfo?.displayName} (${tenantInfo?.defaultDomainName})`; - if (tenantInfo?.displayName) { + currentPermissions?.BlockedTenants?.forEach((item) => { + if (typeof item === "object" && item.type === "Group") { + // Handle group objects newBlockedTenants.push({ - label: label, - value: tenantInfo.defaultDomainName, + label: item.label, + value: item.value, + type: "Group", }); + } else { + // Handle tenant customer IDs (legacy format) + var tenantInfo = tenants.find((t) => t.customerId === item); + if (tenantInfo?.displayName) { + var label = `${tenantInfo.displayName} (${tenantInfo.defaultDomainName})`; + newBlockedTenants.push({ + label: label, + value: tenantInfo.defaultDomainName, + type: "Tenant", + addedFields: { + defaultDomainName: tenantInfo.defaultDomainName, + displayName: tenantInfo.displayName, + customerId: tenantInfo.customerId, + }, + }); + } } }); @@ -245,22 +279,44 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const handleSubmit = () => { let values = formControl.getValues(); - var allowedTenantIds = []; - - selectedTenant.map((tenant) => { - var tenant = tenants.find((t) => t.defaultDomainName === tenant?.value); - if (tenant?.customerId) { - allowedTenantIds.push(tenant.customerId); - } - }); - var blockedTenantIds = []; - blockedTenants.map((tenant) => { - var tenant = tenants.find((t) => t.defaultDomainName === tenant?.value); - if (tenant?.customerId) { - blockedTenantIds.push(tenant.customerId); - } - }); + // Process allowed tenants - preserve groups and convert tenants to IDs + const processedAllowedTenants = + selectedTenant + ?.map((tenant) => { + if (tenant.type === "Group") { + // Keep groups as-is for backend processing + return { + type: "Group", + value: tenant.value, + label: tenant.label, + }; + } else { + // Convert tenant domain names to customer IDs + const tenantInfo = tenants.find((t) => t.defaultDomainName === tenant.value); + return tenantInfo?.customerId; + } + }) + .filter(Boolean) || []; + + // Process blocked tenants - preserve groups and convert tenants to IDs + const processedBlockedTenants = + blockedTenants + ?.map((tenant) => { + if (tenant.type === "Group") { + // Keep groups as-is for backend processing + return { + type: "Group", + value: tenant.value, + label: tenant.label, + }; + } else { + // Convert tenant domain names to customer IDs + const tenantInfo = tenants.find((t) => t.defaultDomainName === tenant.value); + return tenantInfo?.customerId; + } + }) + .filter(Boolean) || []; updatePermissions.mutate({ url: "/api/ExecCustomRole?Action=AddUpdate", @@ -268,8 +324,8 @@ export const CippRoleAddEdit = ({ selectedRole }) => { RoleName: values?.["RoleName"], Permissions: selectedPermissions, EntraGroup: selectedEntraGroup, - AllowedTenants: allowedTenantIds, - BlockedTenants: blockedTenantIds, + AllowedTenants: processedAllowedTenants, + BlockedTenants: processedBlockedTenants, }, }); }; @@ -420,6 +476,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { allTenants={true} name="allowedTenants" fullWidth={true} + includeGroups={true} /> {allTenantSelected && blockedTenants?.length == 0 && ( @@ -437,6 +494,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { allTenants={false} name="blockedTenants" fullWidth={true} + includeGroups={true} />
)} diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index 3eeca3408a4d..ca2a16cf0f75 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -22,9 +22,24 @@ import { Stack, Divider, Collapse, + ToggleButton, + ToggleButtonGroup, + List, + ListItem, + ListItemText, + ListItemSecondaryAction, } from "@mui/material"; import { Grid } from "@mui/system"; -import { Add, Sort, Clear, FilterList, ExpandMore, ExpandLess } from "@mui/icons-material"; +import { + Add, + Sort, + Clear, + FilterList, + ExpandMore, + ExpandLess, + ViewModule, + ViewList, +} from "@mui/icons-material"; import { useState, useCallback, useMemo, memo, useEffect } from "react"; import { debounce } from "lodash"; import { Virtuoso } from "react-virtuoso"; @@ -83,13 +98,13 @@ const StandardCard = memo( {isNewStandard(standard.addedDate) && ( @@ -98,169 +113,163 @@ const StandardCard = memo( size="small" color="success" sx={{ - position: 'absolute', + position: "absolute", top: -10, left: 12, zIndex: 1, - fontSize: '0.7rem', + fontSize: "0.7rem", height: 20, - fontWeight: 'bold' + fontWeight: "bold", }} /> )} - - - - {standard.label} - - {expanded && standard.helpText && ( - <> - - Description: - - theme.palette.primary.main, - textDecoration: "underline", - "&:hover": { - textDecoration: "none", + border: "2px solid", + borderColor: "success.main", + }), + }} + > + + + {standard.label} + + {expanded && standard.helpText && ( + <> + + Description: + + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, }, - }, - color: "text.secondary", - fontSize: "0.875rem", - lineHeight: 1.43, - mb: 2, - }} - > - ( - - {children} - - ), - // Convert paragraphs to spans to avoid unwanted spacing - p: ({ children }) => {children}, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + mb: 2, }} > - {standard.helpText} - - - - )} - - Category: - - - {expanded && - standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > 0 && ( + ( + + {children} + + ), + // Convert paragraphs to spans to avoid unwanted spacing + p: ({ children }) => {children}, + }} + > + {standard.helpText} + + + + )} + + Category: + + + {expanded && + standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > 0 && ( + <> + + Tags: + + + {standard.tag + .filter((tag) => !tag.toLowerCase().includes("impact")) + .map((tag, idx) => ( + + ))} + + + )} + + Impact: + + + {expanded && standard.recommendedBy?.length > 0 && ( <> - Tags: + Recommended By: + + + {standard.recommendedBy.join(", ")} - - {standard.tag - .filter((tag) => !tag.toLowerCase().includes("impact")) - .map((tag, idx) => ( - - ))} - )} - - Impact: - - - {expanded && standard.recommendedBy?.length > 0 && ( - <> - - Recommended By: - - - {standard.recommendedBy.join(", ")} - - - )} - {expanded && standard.addedDate?.length > 0 && ( - <> - - Date Added: - - - - {standard.addedDate} + {expanded && standard.addedDate?.length > 0 && ( + <> + + Date Added: - - - )} - - - - {standard.multiple ? ( - handleAddClick(standard.name)} - > - - - ) : ( - - } - label="Add this standard to the template" - /> - )} - - + + + {standard.addedDate} + + + + )} + + + + {standard.multiple ? ( + handleAddClick(standard.name)} + > + + + ) : ( + + } + label="Add this standard to the template" + /> + )} + +
); @@ -315,20 +324,20 @@ const VirtualizedStandardGrid = memo(({ items, renderItem }) => { return ( ( - {rows[index].map(renderItem)} @@ -341,6 +350,195 @@ const VirtualizedStandardGrid = memo(({ items, renderItem }) => { VirtualizedStandardGrid.displayName = "VirtualizedStandardGrid"; +// Compact List View component for standards +const CompactStandardList = memo( + ({ items, selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled }) => { + return ( + + {items.map(({ standard, category }) => { + const isSelected = !!selectedStandards[standard.name]; + + const isNewStandard = (dateAdded) => { + if (!dateAdded) return false; + const currentDate = new Date(); + const addedDate = new Date(dateAdded); + return differenceInDays(currentDate, addedDate) <= 30; + }; + + const handleToggle = () => { + handleToggleSingleStandard(standard.name); + }; + + return ( + + + + {standard.label} + + {isNewStandard(standard.addedDate) && ( + + )} + + + + } + secondary={ + + {standard.helpText && ( + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + }} + > + ( + + {children} + + ), + p: ({ children }) => ( + + {children} + + ), + }} + > + {standard.helpText} + + + )} + + {standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > + 0 && ( + + {standard.tag + .filter((tag) => !tag.toLowerCase().includes("impact")) + .slice(0, 3) // Show only first 3 tags to save space + .map((tag, idx) => ( + + ))} + {standard.tag.filter((tag) => !tag.toLowerCase().includes("impact")) + .length > 3 && ( + + + + {standard.tag.filter((tag) => !tag.toLowerCase().includes("impact")) + .length - 3}{" "} + more + + )} + + )} + {standard.recommendedBy?.length > 0 && ( + + • Recommended by: {standard.recommendedBy.join(", ")} + + )} + {standard.addedDate && ( + + • Added: {standard.addedDate} + + )} + + + } + /> + + {standard.multiple ? ( + handleAddClick(standard.name)} + sx={{ mr: 1 }} + > + + + ) : ( + + } + label="" + sx={{ mr: 1 }} + /> + )} + + + ); + })} + + ); + } +); + +CompactStandardList.displayName = "CompactStandardList"; + const CippStandardDialog = ({ dialogOpen, handleCloseDialog, @@ -354,7 +552,8 @@ const CippStandardDialog = ({ const [isButtonDisabled, setButtonDisabled] = useState(false); const [localSearchQuery, setLocalSearchQuery] = useState(""); const [isInitialLoading, setIsInitialLoading] = useState(true); - + const [viewMode, setViewMode] = useState("card"); // "card" or "list" + // Enhanced filtering and sorting state const [sortBy, setSortBy] = useState("addedDate"); // Default sort by date added const [sortOrder, setSortOrder] = useState("desc"); // desc to show newest first @@ -363,7 +562,7 @@ const CippStandardDialog = ({ const [selectedRecommendedBy, setSelectedRecommendedBy] = useState([]); const [selectedTagFrameworks, setSelectedTagFrameworks] = useState([]); const [showOnlyNew, setShowOnlyNew] = useState(false); // Show only standards added in last 30 days - const [filtersExpanded, setFiltersExpanded] = useState(true); // Control filter section collapse/expand + const [filtersExpanded, setFiltersExpanded] = useState(false); // Control filter section collapse/expand // Auto-adjust sort order when sort type changes useEffect(() => { @@ -383,43 +582,44 @@ const CippStandardDialog = ({ const recommendedBySet = new Set(); const tagFrameworkSet = new Set(); - // Function to extract base framework from tag - const extractTagFramework = (tag) => { - // Compliance Frameworks - extract version dynamically - if (tag.startsWith('CIS M365')) { - const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/); - return versionMatch ? `CIS M365 ${versionMatch[1]}` : 'CIS M365'; - } - if (tag.startsWith('CISA ')) return 'CISA'; - if (tag.startsWith('EIDSCA.')) return 'EIDSCA'; - if (tag.startsWith('Essential 8')) return 'Essential 8'; - if (tag.startsWith('NIST CSF')) { - const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/); - return versionMatch ? `NIST CSF ${versionMatch[1]}` : 'NIST CSF'; - } - - // Microsoft Secure Score Categories - if (tag.startsWith('exo_')) return 'Secure Score - Exchange'; - if (tag.startsWith('mdo_')) return 'Secure Score - Defender'; - if (tag.startsWith('spo_')) return 'Secure Score - SharePoint'; - if (tag.startsWith('mip_')) return 'Secure Score - Purview'; - - // For any other tags, return null to exclude them - return null; - }; + // Function to extract base framework from tag + const extractTagFramework = (tag) => { + // Compliance Frameworks - extract version dynamically + if (tag.startsWith("CIS M365")) { + const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/); + return versionMatch ? `CIS M365 ${versionMatch[1]}` : "CIS M365"; + } + if (tag.startsWith("CISA ")) return "CISA"; + if (tag.startsWith("EIDSCA.")) return "EIDSCA"; + if (tag.startsWith("Essential 8")) return "Essential 8"; + if (tag.startsWith("NIST CSF")) { + const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/); + return versionMatch ? `NIST CSF ${versionMatch[1]}` : "NIST CSF"; + } + + // Microsoft Secure Score Categories + if (tag.startsWith("exo_")) return "Secure Score - Exchange"; + if (tag.startsWith("mdo_")) return "Secure Score - Defender"; + if (tag.startsWith("spo_")) return "Secure Score - SharePoint"; + if (tag.startsWith("mip_")) return "Secure Score - Purview"; + + // For any other tags, return null to exclude them + return null; + }; Object.keys(categories).forEach((category) => { categorySet.add(category); categories[category].forEach((standard) => { if (standard.impact) impactSet.add(standard.impact); if (standard.recommendedBy && Array.isArray(standard.recommendedBy)) { - standard.recommendedBy.forEach(rec => recommendedBySet.add(rec)); + standard.recommendedBy.forEach((rec) => recommendedBySet.add(rec)); } // Process tags to extract frameworks if (standard.tag && Array.isArray(standard.tag)) { - standard.tag.forEach(tag => { + standard.tag.forEach((tag) => { const framework = extractTagFramework(tag); - if (framework) { // Only add non-null frameworks + if (framework) { + // Only add non-null frameworks tagFrameworkSet.add(framework); } }); @@ -439,23 +639,23 @@ const CippStandardDialog = ({ const sortedTagFrameworks = Array.from(tagFrameworkSet).sort((a, b) => { // Define priority groups const getFrameworkPriority = (framework) => { - if (framework.startsWith('CIS M365')) return 1; - if (framework === 'CISA') return 2; - if (framework === 'EIDSCA') return 3; - if (framework === 'Essential 8') return 4; - if (framework.startsWith('NIST CSF')) return 5; - if (framework.startsWith('Secure Score -')) return 6; + if (framework.startsWith("CIS M365")) return 1; + if (framework === "CISA") return 2; + if (framework === "EIDSCA") return 3; + if (framework === "Essential 8") return 4; + if (framework.startsWith("NIST CSF")) return 5; + if (framework.startsWith("Secure Score -")) return 6; return 999; // Other tags go last }; - + const aPriority = getFrameworkPriority(a); const bPriority = getFrameworkPriority(b); - + // If different priorities, sort by priority if (aPriority !== bPriority) { return aPriority - bPriority; } - + // If same priority, sort alphabetically return a.localeCompare(b); }); @@ -469,113 +669,143 @@ const CippStandardDialog = ({ }, [categories]); // Enhanced filter function - const enhancedFilterStandards = useCallback((standardsList) => { - // Function to extract base framework from tag (same as in useMemo) + const enhancedFilterStandards = useCallback( + (standardsList) => { + // Function to extract base framework from tag (same as in useMemo) const extractTagFramework = (tag) => { // Compliance Frameworks - extract version dynamically - if (tag.startsWith('CIS M365')) { + if (tag.startsWith("CIS M365")) { const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/); - return versionMatch ? `CIS M365 ${versionMatch[1]}` : 'CIS M365'; + return versionMatch ? `CIS M365 ${versionMatch[1]}` : "CIS M365"; } - if (tag.startsWith('CISA ')) return 'CISA'; - if (tag.startsWith('EIDSCA.')) return 'EIDSCA'; - if (tag.startsWith('Essential 8')) return 'Essential 8'; - if (tag.startsWith('NIST CSF')) { + if (tag.startsWith("CISA ")) return "CISA"; + if (tag.startsWith("EIDSCA.")) return "EIDSCA"; + if (tag.startsWith("Essential 8")) return "Essential 8"; + if (tag.startsWith("NIST CSF")) { const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/); - return versionMatch ? `NIST CSF ${versionMatch[1]}` : 'NIST CSF'; + return versionMatch ? `NIST CSF ${versionMatch[1]}` : "NIST CSF"; } - + // Microsoft Secure Score Categories - if (tag.startsWith('exo_')) return 'Secure Score - Exchange'; - if (tag.startsWith('mdo_')) return 'Secure Score - Defender'; - if (tag.startsWith('spo_')) return 'Secure Score - SharePoint'; - if (tag.startsWith('mip_')) return 'Secure Score - Purview'; - + if (tag.startsWith("exo_")) return "Secure Score - Exchange"; + if (tag.startsWith("mdo_")) return "Secure Score - Defender"; + if (tag.startsWith("spo_")) return "Secure Score - SharePoint"; + if (tag.startsWith("mip_")) return "Secure Score - Purview"; + // For any other tags, return null to exclude them return null; }; - return standardsList.filter((standard) => { - // Original text search - const matchesSearch = !localSearchQuery || - standard.label.toLowerCase().includes(localSearchQuery.toLowerCase()) || - standard.helpText.toLowerCase().includes(localSearchQuery.toLowerCase()) || - (standard.tag && standard.tag.some((tag) => - tag.toLowerCase().includes(localSearchQuery.toLowerCase()) - )); - - // Category filter - const matchesCategory = selectedCategories.length === 0 || - selectedCategories.includes(standard.cat); - - // Impact filter - const matchesImpact = selectedImpacts.length === 0 || - selectedImpacts.includes(standard.impact); - - // Recommended by filter - const matchesRecommendedBy = selectedRecommendedBy.length === 0 || - (standard.recommendedBy && Array.isArray(standard.recommendedBy) && - standard.recommendedBy.some(rec => selectedRecommendedBy.includes(rec))); - - // Tag framework filter - const matchesTagFramework = selectedTagFrameworks.length === 0 || - (standard.tag && Array.isArray(standard.tag) && - standard.tag.some(tag => { - const framework = extractTagFramework(tag); - return framework && selectedTagFrameworks.includes(framework); - })); - - // New standards filter (last 30 days) - const isNewStandard = (dateAdded) => { - if (!dateAdded) return false; - const currentDate = new Date(); - const addedDate = new Date(dateAdded); - return differenceInDays(currentDate, addedDate) <= 30; - }; - const matchesNewFilter = !showOnlyNew || isNewStandard(standard.addedDate); - - return matchesSearch && matchesCategory && matchesImpact && matchesRecommendedBy && matchesTagFramework && matchesNewFilter; - }); - }, [localSearchQuery, selectedCategories, selectedImpacts, selectedRecommendedBy, selectedTagFrameworks, showOnlyNew]); + return standardsList.filter((standard) => { + // Original text search + const matchesSearch = + !localSearchQuery || + standard.label.toLowerCase().includes(localSearchQuery.toLowerCase()) || + standard.helpText.toLowerCase().includes(localSearchQuery.toLowerCase()) || + (standard.tag && + standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase()))); + + // Category filter + const matchesCategory = + selectedCategories.length === 0 || selectedCategories.includes(standard.cat); + + // Impact filter + const matchesImpact = + selectedImpacts.length === 0 || selectedImpacts.includes(standard.impact); + + // Recommended by filter + const matchesRecommendedBy = + selectedRecommendedBy.length === 0 || + (standard.recommendedBy && + Array.isArray(standard.recommendedBy) && + standard.recommendedBy.some((rec) => selectedRecommendedBy.includes(rec))); + + // Tag framework filter + const matchesTagFramework = + selectedTagFrameworks.length === 0 || + (standard.tag && + Array.isArray(standard.tag) && + standard.tag.some((tag) => { + const framework = extractTagFramework(tag); + return framework && selectedTagFrameworks.includes(framework); + })); + + // New standards filter (last 30 days) + const isNewStandard = (dateAdded) => { + if (!dateAdded) return false; + const currentDate = new Date(); + const addedDate = new Date(dateAdded); + return differenceInDays(currentDate, addedDate) <= 30; + }; + const matchesNewFilter = !showOnlyNew || isNewStandard(standard.addedDate); + + return ( + matchesSearch && + matchesCategory && + matchesImpact && + matchesRecommendedBy && + matchesTagFramework && + matchesNewFilter + ); + }); + }, + [ + localSearchQuery, + selectedCategories, + selectedImpacts, + selectedRecommendedBy, + selectedTagFrameworks, + showOnlyNew, + ] + ); // Enhanced sort function - const sortStandards = useCallback((standardsList) => { - return [...standardsList].sort((a, b) => { - let aValue, bValue; - - switch (sortBy) { - case "label": - aValue = a.label.toLowerCase(); - bValue = b.label.toLowerCase(); - break; - case "addedDate": - aValue = new Date(a.addedDate || "1900-01-01"); - bValue = new Date(b.addedDate || "1900-01-01"); - break; - case "category": - aValue = a.cat?.toLowerCase() || ""; - bValue = b.cat?.toLowerCase() || ""; - break; - case "impact": - // Sort by impact priority: High > Medium > Low - const impactOrder = { "High Impact": 3, "Medium Impact": 2, "Low Impact": 1 }; - aValue = impactOrder[a.impact] || 0; - bValue = impactOrder[b.impact] || 0; - break; - case "recommendedBy": - aValue = (a.recommendedBy && a.recommendedBy.length > 0) ? a.recommendedBy.join(", ").toLowerCase() : ""; - bValue = (b.recommendedBy && b.recommendedBy.length > 0) ? b.recommendedBy.join(", ").toLowerCase() : ""; - break; - default: - aValue = a.label.toLowerCase(); - bValue = b.label.toLowerCase(); - } + const sortStandards = useCallback( + (standardsList) => { + return [...standardsList].sort((a, b) => { + let aValue, bValue; + + switch (sortBy) { + case "label": + aValue = a.label.toLowerCase(); + bValue = b.label.toLowerCase(); + break; + case "addedDate": + aValue = new Date(a.addedDate || "1900-01-01"); + bValue = new Date(b.addedDate || "1900-01-01"); + break; + case "category": + aValue = a.cat?.toLowerCase() || ""; + bValue = b.cat?.toLowerCase() || ""; + break; + case "impact": + // Sort by impact priority: High > Medium > Low + const impactOrder = { "High Impact": 3, "Medium Impact": 2, "Low Impact": 1 }; + aValue = impactOrder[a.impact] || 0; + bValue = impactOrder[b.impact] || 0; + break; + case "recommendedBy": + aValue = + a.recommendedBy && a.recommendedBy.length > 0 + ? a.recommendedBy.join(", ").toLowerCase() + : ""; + bValue = + b.recommendedBy && b.recommendedBy.length > 0 + ? b.recommendedBy.join(", ").toLowerCase() + : ""; + break; + default: + aValue = a.label.toLowerCase(); + bValue = b.label.toLowerCase(); + } - if (aValue < bValue) return sortOrder === "asc" ? -1 : 1; - if (aValue > bValue) return sortOrder === "asc" ? 1 : -1; - return 0; - }); - }, [sortBy, sortOrder]); + if (aValue < bValue) return sortOrder === "asc" ? -1 : 1; + if (aValue > bValue) return sortOrder === "asc" ? 1 : -1; + return 0; + }); + }, + [sortBy, sortOrder] + ); // Optimize handleAddClick to be more performant const handleAddClick = useCallback( @@ -623,6 +853,7 @@ const CippStandardDialog = ({ setShowOnlyNew(false); setSortBy("addedDate"); setSortOrder("desc"); + setViewMode("card"); // Reset to card view handleSearchQueryChange(""); }, [handleSearchQueryChange]); @@ -634,6 +865,7 @@ const CippStandardDialog = ({ setSelectedRecommendedBy([]); setSelectedTagFrameworks([]); setShowOnlyNew(false); + setViewMode("card"); // Reset to card view handleSearchQueryChange(""); // Clear parent search state handleCloseDialog(); }, [handleCloseDialog, handleSearchQueryChange]); @@ -649,7 +881,7 @@ const CippStandardDialog = ({ Object.keys(categories).forEach((category) => { const categoryStandards = categories[category]; const filteredStandards = enhancedFilterStandards(categoryStandards); - + filteredStandards.forEach((standard) => { allItems.push({ standard, @@ -659,10 +891,12 @@ const CippStandardDialog = ({ }); // Apply sorting to the final combined array instead of per-category - const sortedAllItems = sortStandards(allItems.map(item => item.standard)).map(standard => { - const item = allItems.find(item => item.standard.name === standard.name); - return item; - }); + const sortedAllItems = sortStandards(allItems.map((item) => item.standard)).map( + (standard) => { + const item = allItems.find((item) => item.standard.name === standard.name); + return item; + } + ); setProcessedItems(sortedAllItems); setIsInitialLoading(false); @@ -701,7 +935,12 @@ const CippStandardDialog = ({ ); // Count active filters - const activeFiltersCount = selectedCategories.length + selectedImpacts.length + selectedRecommendedBy.length + selectedTagFrameworks.length + (showOnlyNew ? 1 : 0); + const activeFiltersCount = + selectedCategories.length + + selectedImpacts.length + + selectedRecommendedBy.length + + selectedTagFrameworks.length + + (showOnlyNew ? 1 : 0); // Don't render dialog contents until it's actually open (improves performance) return ( @@ -721,252 +960,243 @@ const CippStandardDialog = ({ sx: { minWidth: "720px", maxHeight: "90vh", + display: "flex", + flexDirection: "column", }, }} > Select a Standard to Add - + {/* Search and Filter Controls */} {/* Search Box */} - - {/* Filter Controls Section */} + {/* Unified Controls Section */} {/* Clickable header bar */} - setFiltersExpanded(!filtersExpanded)} - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - mb: 2, - p: 1, - borderRadius: 1, - cursor: 'pointer', - bgcolor: 'action.hover', - '&:hover': { - bgcolor: 'action.selected' - } + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + py: 0.75, + px: 1, + borderRadius: filtersExpanded ? "4px 4px 0 0" : 1, + cursor: "pointer", + bgcolor: "action.hover", + border: "1px solid", + borderColor: "divider", + borderBottom: filtersExpanded ? "none" : "none", + "&:hover": { + bgcolor: "action.selected", + }, }} > - - Sort & Filter Options - - {filtersExpanded ? : } - - - {/* Compact summary when collapsed */} - {!filtersExpanded && ( - - - Sorted by {sortBy === 'addedDate' ? 'Date Added' : 'Name'} ({sortOrder === 'desc' ? 'Desc' : 'Asc'}) + + + + View, Sort & Filter Options - {activeFiltersCount > 0 && ( - <> - - • {activeFiltersCount} filter{activeFiltersCount !== 1 ? 's' : ''} - - - + {!filtersExpanded && ( + + ({viewMode === "card" ? "Card" : "List"} •{" "} + {sortBy === "addedDate" ? "Date" : "Name"} {sortOrder === "desc" ? "↓" : "↑"} + {activeFiltersCount > 0 + ? ` • ${activeFiltersCount} filter${activeFiltersCount !== 1 ? "s" : ""}` + : ""} + ) + )} - )} - - {/* Collapsible filter controls */} + {filtersExpanded ? : } + + + {/* Single line controls when expanded */} - - {/* Sort Controls Card */} - - - SORT: - - - Sort By - - - - - Order - - - - - {/* Filter Controls Card */} - - - FILTER: - - - Categories - - - - - Impact - - - - - Recommended By - - - - - Compliance Tags - - - - {/* New Standards Toggle */} - setShowOnlyNew(e.target.checked)} - /> + bgcolor: "background.paper", + borderRadius: "0 0 4px 4px", + border: "1px solid", + borderColor: "divider", + borderTop: "none", + flexWrap: "wrap", + }} + > + {/* View Mode */} + { + if (newViewMode !== null) { + setViewMode(newViewMode); } - label="New (30 days)" - sx={{ ml: 1 }} - /> - - {/* Clear All Filters Button */} - {activeFiltersCount > 0 && ( - - )} - + }} + > + + + Cards + + + + List + + + + {/* Sort Controls */} + + Sort By + + + + + Order + + + + {/* Filter Controls */} + + Categories + + + + + Impact + + + + + Recommended By + + + + + Compliance Tags + + + + {/* New Standards Toggle */} + setShowOnlyNew(e.target.checked)} + /> + } + label="New (30 days)" + sx={{ ml: 1 }} + /> + + {/* Clear Button */} + {activeFiltersCount > 0 && ( + + )} @@ -980,7 +1210,9 @@ const CippStandardDialog = ({ key={category} label={category} size="small" - onDelete={() => setSelectedCategories(prev => prev.filter(c => c !== category))} + onDelete={() => + setSelectedCategories((prev) => prev.filter((c) => c !== category)) + } color="primary" variant="outlined" /> @@ -990,7 +1222,7 @@ const CippStandardDialog = ({ key={impact} label={impact} size="small" - onDelete={() => setSelectedImpacts(prev => prev.filter(i => i !== impact))} + onDelete={() => setSelectedImpacts((prev) => prev.filter((i) => i !== impact))} color="secondary" variant="outlined" /> @@ -1000,7 +1232,9 @@ const CippStandardDialog = ({ key={rec} label={rec} size="small" - onDelete={() => setSelectedRecommendedBy(prev => prev.filter(r => r !== rec))} + onDelete={() => + setSelectedRecommendedBy((prev) => prev.filter((r) => r !== rec)) + } color="success" variant="outlined" /> @@ -1010,7 +1244,9 @@ const CippStandardDialog = ({ key={framework} label={framework} size="small" - onDelete={() => setSelectedTagFrameworks(prev => prev.filter(f => f !== framework))} + onDelete={() => + setSelectedTagFrameworks((prev) => prev.filter((f) => f !== framework)) + } color="warning" variant="outlined" /> @@ -1043,14 +1279,28 @@ const CippStandardDialog = ({ Try adjusting your search terms or clearing some filters - + ) : ( - - - Showing {processedItems.length} standard{processedItems.length !== 1 ? 's' : ''} + + + Showing {processedItems.length} standard{processedItems.length !== 1 ? "s" : ""} - + {viewMode === "card" ? ( + + + + ) : ( + + + + )} )} diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 5339f0bf4ad9..fd2cd79afa2c 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import { Button, Typography } from "@mui/material"; +import { Button, Link, Typography } from "@mui/material"; import { Save as SaveIcon, Delete, @@ -558,6 +558,7 @@ const CippGraphExplorerFilter = ({ )} placeholder="Select a preset" + helperText="Select an existing preset to load its parameters" /> @@ -580,6 +581,19 @@ const CippGraphExplorerFilter = ({ formControl={formControl} disabled={endpointFilter ? true : false} placeholder="Enter Graph API endpoint" + helperText={ + <> + The{" "} + + Graph endpoint + {" "} + to query (e.g. https://graph.microsoft.com/beta/$Endpoint) + + } /> @@ -601,7 +615,7 @@ const CippGraphExplorerFilter = ({ ] } placeholder="Columns to select" - helperText="Comma-separated list of columns to include in the response" + helperText="List of object properties to include in the response" /> @@ -613,6 +627,17 @@ const CippGraphExplorerFilter = ({ label="Filter" formControl={formControl} placeholder="OData filter" + helperText={ + + Graph $filter query + + } /> diff --git a/src/components/CippTable/util-columnsFromAPI.js b/src/components/CippTable/util-columnsFromAPI.js index 058db11048d3..5ec7dd733740 100644 --- a/src/components/CippTable/util-columnsFromAPI.js +++ b/src/components/CippTable/util-columnsFromAPI.js @@ -1,85 +1,85 @@ -import { getCippFilterVariant } from "../../utils/get-cipp-filter-variant"; -import { getCippFormatting } from "../../utils/get-cipp-formatting"; -import { getCippTranslation } from "../../utils/get-cipp-translation"; - -const skipRecursion = ["location", "ScheduledBackupValues"]; -// Function to merge keys from all objects in the array -const mergeKeys = (dataArray) => { - return dataArray.reduce((acc, item) => { - const mergeRecursive = (obj, base = {}) => { - Object.keys(obj).forEach((key) => { - if ( - typeof obj[key] === "object" && - obj[key] !== null && - !Array.isArray(obj[key]) && - !skipRecursion.includes(key) - ) { - if (typeof base[key] === "boolean") { - // Skip merging if base[key] is a boolean - return; - } - if (typeof base[key] !== "object" || Array.isArray(base[key])) { - // Re-initialize base[key] if it's not an object - base[key] = {}; - } - base[key] = mergeRecursive(obj[key], base[key]); - } else if (typeof obj[key] === "boolean") { - base[key] = obj[key]; - } else if (typeof obj[key] === "string" && obj[key].toUpperCase() === "FAILED") { - base[key] = base[key]; // Keep existing value if it's 'FAILED' - } else if (obj[key] !== undefined && obj[key] !== null) { - base[key] = obj[key]; // Assign valid primitive values - } - }); - return base; - }; - - return mergeRecursive(item, acc); - }, {}); -}; - -export const utilColumnsFromAPI = (dataArray) => { - const dataSample = mergeKeys(dataArray); - - const generateColumns = (obj, parentKey = "") => { - return Object.keys(obj) - .map((key) => { - const accessorKey = parentKey ? `${parentKey}.${key}` : key; - if ( - typeof obj[key] === "object" && - obj[key] !== null && - !Array.isArray(obj[key]) && - !skipRecursion.includes(key) - ) { - return generateColumns(obj[key], accessorKey); - } - - return { - header: getCippTranslation(accessorKey), - id: accessorKey, - accessorFn: (row) => { - let value; - if (accessorKey.includes("@odata")) { - value = row[accessorKey]; - } else { - value = accessorKey.split(".").reduce((acc, part) => acc && acc[part], row); - } - return getCippFormatting(value, accessorKey, "text"); - }, - ...getCippFilterVariant(key), - Cell: ({ row }) => { - let value; - if (accessorKey.includes("@odata")) { - value = row.original[accessorKey]; - } else { - value = accessorKey.split(".").reduce((acc, part) => acc && acc[part], row.original); - } - return getCippFormatting(value, accessorKey); - }, - }; - }) - .flat(); - }; - - return generateColumns(dataSample); -}; +import { getCippFilterVariant } from "../../utils/get-cipp-filter-variant"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; + +const skipRecursion = ["location", "ScheduledBackupValues", "Tenant"]; +// Function to merge keys from all objects in the array +const mergeKeys = (dataArray) => { + return dataArray.reduce((acc, item) => { + const mergeRecursive = (obj, base = {}) => { + Object.keys(obj).forEach((key) => { + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) && + !skipRecursion.includes(key) + ) { + if (typeof base[key] === "boolean") { + // Skip merging if base[key] is a boolean + return; + } + if (typeof base[key] !== "object" || Array.isArray(base[key])) { + // Re-initialize base[key] if it's not an object + base[key] = {}; + } + base[key] = mergeRecursive(obj[key], base[key]); + } else if (typeof obj[key] === "boolean") { + base[key] = obj[key]; + } else if (typeof obj[key] === "string" && obj[key].toUpperCase() === "FAILED") { + base[key] = base[key]; // Keep existing value if it's 'FAILED' + } else if (obj[key] !== undefined && obj[key] !== null) { + base[key] = obj[key]; // Assign valid primitive values + } + }); + return base; + }; + + return mergeRecursive(item, acc); + }, {}); +}; + +export const utilColumnsFromAPI = (dataArray) => { + const dataSample = mergeKeys(dataArray); + + const generateColumns = (obj, parentKey = "") => { + return Object.keys(obj) + .map((key) => { + const accessorKey = parentKey ? `${parentKey}.${key}` : key; + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) && + !skipRecursion.includes(key) + ) { + return generateColumns(obj[key], accessorKey); + } + + return { + header: getCippTranslation(accessorKey), + id: accessorKey, + accessorFn: (row) => { + let value; + if (accessorKey.includes("@odata")) { + value = row[accessorKey]; + } else { + value = accessorKey.split(".").reduce((acc, part) => acc && acc[part], row); + } + return getCippFormatting(value, accessorKey, "text"); + }, + ...getCippFilterVariant(key), + Cell: ({ row }) => { + let value; + if (accessorKey.includes("@odata")) { + value = row.original[accessorKey]; + } else { + value = accessorKey.split(".").reduce((acc, part) => acc && acc[part], row.original); + } + return getCippFormatting(value, accessorKey); + }, + }; + }) + .flat(); + }; + + return generateColumns(dataSample); +}; diff --git a/src/components/CippWizard/CippCAForm.jsx b/src/components/CippWizard/CippCAForm.jsx index 8a80f39b00d2..d0ee1a657186 100644 --- a/src/components/CippWizard/CippCAForm.jsx +++ b/src/components/CippWizard/CippCAForm.jsx @@ -9,7 +9,7 @@ import { useWatch } from "react-hook-form"; export const CippCAForm = (props) => { const { formControl, onPreviousStep, onNextStep, currentStep } = props; const values = formControl.getValues(); - const CATemplates = ApiGetCall({ url: "/api/ListCATemplates" }); + const CATemplates = ApiGetCall({ url: "/api/ListCATemplates", queryKey: "CATemplates" }); const [JSONData, setJSONData] = useState(); const watcher = useWatch({ control: formControl.control, name: "TemplateList" }); useEffect(() => { @@ -82,6 +82,13 @@ export const CippCAForm = (props) => { label="Overwrite Existing Policy" formControl={formControl} /> + + { return null; } return uniquePlaceholders.map((placeholder) => ( - + {selectedTenants.map((tenant, idx) => ( { const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props; - // Watch for the selected template to access permissions + // Watch for the selected template to access permissions and type const selectedTemplate = useWatch({ control: formControl.control, name: "selectedTemplate", @@ -56,9 +56,13 @@ export const CippWizardAppApproval = (props) => { addedField: { AppId: "AppId", AppName: "AppName", + AppType: "AppType", + GalleryTemplateId: "GalleryTemplateId", + GalleryInformation: "GalleryInformation", PermissionSetId: "PermissionSetId", PermissionSetName: "PermissionSetName", Permissions: "Permissions", + ApplicationManifest: "ApplicationManifest", }, showRefresh: true, }} @@ -75,19 +79,75 @@ export const CippWizardAppApproval = (props) => { propertyItems={[ { label: "App Name", value: selectedTemplate.addedFields.AppName }, { label: "App ID", value: selectedTemplate.addedFields.AppId }, + { + label: "Template Type", + value: + (selectedTemplate.addedFields.AppType || "EnterpriseApp") === + "GalleryTemplate" + ? "Gallery Template" + : (selectedTemplate.addedFields.AppType || "EnterpriseApp") === + "ApplicationManifest" + ? "Application Manifest" + : "Enterprise App", + }, { label: "Permission Set", - value: selectedTemplate.addedFields.PermissionSetName, + value: + (selectedTemplate.addedFields.AppType || "EnterpriseApp") === + "GalleryTemplate" + ? "Auto-Consent" + : (selectedTemplate.addedFields.AppType || "EnterpriseApp") === + "ApplicationManifest" + ? "Defined in Manifest" + : selectedTemplate.addedFields.PermissionSetName, }, ]} title="Template Details" /> - + {(selectedTemplate.addedFields.AppType || "EnterpriseApp") === "EnterpriseApp" ? ( + + ) : (selectedTemplate.addedFields.AppType || "EnterpriseApp") === + "ApplicationManifest" ? ( + + ) : ( + + )} )} diff --git a/src/components/CippWizard/CippWizardAutopilotImport.jsx b/src/components/CippWizard/CippWizardAutopilotImport.jsx index 2c3384686ada..fc7876e34698 100644 --- a/src/components/CippWizard/CippWizardAutopilotImport.jsx +++ b/src/components/CippWizard/CippWizardAutopilotImport.jsx @@ -1,10 +1,7 @@ import { Button, - Grid, Link, Stack, - Card, - CardContent, Box, Typography, Dialog, @@ -14,13 +11,12 @@ import { TextField, Alert, } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippWizardStepButtons } from "./CippWizardStepButtons"; -import CippFormComponent from "../CippComponents/CippFormComponent"; import { CippDataTable } from "../CippTable/CippDataTable"; import { useWatch } from "react-hook-form"; import { Delete, FileDownload, Upload, Add } from "@mui/icons-material"; import { useEffect, useState } from "react"; -import { getCippTranslation } from "../../utils/get-cipp-translation"; import React from "react"; export const CippWizardAutopilotImport = (props) => { diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js index 40f6034aa67b..764a568e7959 100644 --- a/src/components/ExecutiveReportButton.js +++ b/src/components/ExecutiveReportButton.js @@ -35,7 +35,6 @@ const ExecutiveReportDocument = ({ month: "long", day: "numeric", }); - const brandColor = brandingSettings?.colour || "#F77F00"; // ENTERPRISE DESIGN SYSTEM - JOBS/RAMS/IVE PRINCIPLES @@ -686,7 +685,6 @@ const ExecutiveReportDocument = ({ standardDef.tag && Array.isArray(standardDef.tag) && standardDef.tag.length > 0 ? standardDef.tag.slice(0, 2).join(", ") // Show first 2 tags : "No tags"; - processedStandards.push({ name: standardDef.label, description: @@ -905,7 +903,17 @@ const ExecutiveReportDocument = ({ {control.description} - {control.tags} + {(() => { + if (typeof control.tags === "object") { + console.log( + "DEBUG: control.tags is an object:", + control.tags, + "for control:", + control.name + ); + } + return control.tags; + })()} {control.status} @@ -1269,7 +1277,18 @@ const ExecutiveReportDocument = ({ {licensingData.map((license, index) => ( - {license.License || license.license || "N/A"} + {(() => { + const licenseValue = license.License || license.license || "N/A"; + if (typeof licenseValue === "object") { + console.log( + "DEBUG: license name is an object:", + licenseValue, + "full license:", + license + ); + } + return licenseValue; + })()} - {license.CountUsed || license.countUsed || "0"} + {(() => { + const countUsed = license.CountUsed || license.countUsed || "0"; + if (typeof countUsed === "object") { + console.log( + "DEBUG: license.CountUsed is an object:", + countUsed, + "full license:", + license + ); + } + return countUsed; + })()} - {license.CountAvailable || license.countAvailable || "0"} + {(() => { + const countAvailable = + license.CountAvailable || license.countAvailable || "0"; + if (typeof countAvailable === "object") { + console.log( + "DEBUG: license.CountAvailable is an object:", + countAvailable, + "full license:", + license + ); + } + return countAvailable; + })()} - {license.TotalLicenses || license.totalLicenses || "0"} + {(() => { + const totalLicenses = license.TotalLicenses || license.totalLicenses || "0"; + if (typeof totalLicenses === "object") { + console.log( + "DEBUG: license.TotalLicenses is an object:", + totalLicenses, + "full license:", + license + ); + } + return totalLicenses; + })()} ))} @@ -1396,8 +1449,8 @@ const ExecutiveReportDocument = ({ { deviceData.filter( (device) => - device.complianceState === "Compliant" || - device.ComplianceState === "Compliant" + device.complianceState === "compliant" || + device.ComplianceState === "compliant" ).length } @@ -1408,8 +1461,8 @@ const ExecutiveReportDocument = ({ { deviceData.filter( (device) => - device.complianceState !== "Compliant" && - device.ComplianceState !== "Compliant" + device.complianceState !== "compliant" && + device.ComplianceState !== "compliant" ).length } @@ -1451,10 +1504,32 @@ const ExecutiveReportDocument = ({ return ( - {device.deviceName || "N/A"} + {(() => { + const deviceName = device.deviceName || "N/A"; + if (typeof deviceName === "object") { + console.log( + "DEBUG: device.deviceName is an object:", + deviceName, + "full device:", + device + ); + } + return deviceName; + })()} - {device.operatingSystem || "N/A"} + {(() => { + const operatingSystem = device.operatingSystem || "N/A"; + if (typeof operatingSystem === "object") { + console.log( + "DEBUG: device.operatingSystem is an object:", + operatingSystem, + "full device:", + device + ); + } + return operatingSystem; + })()} - {device.complianceState || "Unknown"} + {(() => { + const complianceState = device.complianceState || "Unknown"; + if (typeof complianceState === "object") { + console.log( + "DEBUG: device.complianceState is an object:", + complianceState, + "full device:", + device + ); + } + return complianceState; + })()} {lastSync} @@ -1527,240 +1613,266 @@ const ExecutiveReportDocument = ({ )} {/* CONDITIONAL ACCESS POLICIES PAGE - Only show if data is available */} - {conditionalAccessData && Array.isArray(conditionalAccessData) && conditionalAccessData.length > 0 && ( - <> - {/* STATISTIC PAGE 5 - CHAPTER SPLITTER */} - - - - 277 - days - - average time to identify and{"\n"} - contain a data breach - - - - Early detection minimizes{"\n"} - business impact - - - - - - Conditional Access Policies - - Identity and access management security controls + {conditionalAccessData && + Array.isArray(conditionalAccessData) && + conditionalAccessData.length > 0 && ( + <> + {/* STATISTIC PAGE 5 - CHAPTER SPLITTER */} + + + + 277 + days + + average time to identify and{"\n"} + contain a data breach - {brandingSettings?.logo && ( - - )} - - - - - Access control policies help protect your business by ensuring only the right people - can access sensitive information under appropriate circumstances. These smart - security measures automatically evaluate each access request and apply additional - verification when needed, balancing security with employee productivity. + + Early detection minimizes{"\n"} + business impact - + + + + + Conditional Access Policies + + Identity and access management security controls + + + {brandingSettings?.logo && ( + + )} + - - How Access Controls Protect Your Business - - These policies work like intelligent security guards, making decisions based on who - is trying to access what, from where, and when. For example, accessing email from - the office might be seamless, but accessing it from an unusual location might - require additional verification. This approach protects your data while minimizing - disruption to daily work. - - + + + Access control policies help protect your business by ensuring only the right + people can access sensitive information under appropriate circumstances. These + smart security measures automatically evaluate each access request and apply + additional verification when needed, balancing security with employee + productivity. + + - - Current Policy Configuration + + How Access Controls Protect Your Business + + These policies work like intelligent security guards, making decisions based on + who is trying to access what, from where, and when. For example, accessing email + from the office might be seamless, but accessing it from an unusual location might + require additional verification. This approach protects your data while minimizing + disruption to daily work. + + - - - Policy Name - State - Applications - Controls - + + Current Policy Configuration - {conditionalAccessData.slice(0, 8).map((policy, index) => { - const getStateStyle = (state) => { - switch (state) { - case "enabled": - return styles.statusCompliant; - case "enabledForReportingButNotEnforced": - return styles.statusPartial; - case "disabled": - return styles.statusReview; - default: - return styles.statusText; - } - }; - - const getStateDisplay = (state) => { - switch (state) { - case "enabled": - return "Enabled"; - case "enabledForReportingButNotEnforced": - return "Report Only"; - case "disabled": - return "Disabled"; - default: - return state || "Unknown"; - } - }; - - const getControlsText = (policy) => { - const controls = []; - if (policy.builtInControls) { - if (policy.builtInControls.includes("mfa")) controls.push("MFA"); - if (policy.builtInControls.includes("block")) controls.push("Block"); - if (policy.builtInControls.includes("compliantDevice")) - controls.push("Compliant Device"); - } - return controls.length > 0 ? controls.join(", ") : "Custom"; - }; + + + Policy Name + State + Applications + Controls + - return ( - - - {policy.displayName || "N/A"} - - - - {getStateDisplay(policy.state)} + {conditionalAccessData.slice(0, 8).map((policy, index) => { + const getStateStyle = (state) => { + switch (state) { + case "enabled": + return styles.statusCompliant; + case "enabledForReportingButNotEnforced": + return styles.statusPartial; + case "disabled": + return styles.statusReview; + default: + return styles.statusText; + } + }; + + const getStateDisplay = (state) => { + switch (state) { + case "enabled": + return "Enabled"; + case "enabledForReportingButNotEnforced": + return "Report Only"; + case "disabled": + return "Disabled"; + default: + return state || "Unknown"; + } + }; + + const getControlsText = (policy) => { + const controls = []; + if (policy.builtInControls) { + if (policy.builtInControls.includes("mfa")) controls.push("MFA"); + if (policy.builtInControls.includes("block")) controls.push("Block"); + if (policy.builtInControls.includes("compliantDevice")) + controls.push("Compliant Device"); + } + return controls.length > 0 ? controls.join(", ") : "Custom"; + }; + + return ( + + + {(() => { + const displayName = policy.displayName || "N/A"; + if (typeof displayName === "object") { + console.log( + "DEBUG: policy.displayName is an object:", + displayName, + "full policy:", + policy + ); + } + return displayName; + })()} + + + + {getStateDisplay(policy.state)} + + + + {(() => { + const includeApplications = policy.includeApplications || "All"; + if (typeof includeApplications === "object") { + console.log( + "DEBUG: policy.includeApplications is an object:", + includeApplications, + "full policy:", + policy + ); + } + return includeApplications; + })()} + + + {getControlsText(policy)} - - {policy.includeApplications || "All"} - - - {getControlsText(policy)} - - - ); - })} + ); + })} + - - - Policy Overview + + Policy Overview - - - {conditionalAccessData.length} - Total Policies - - - - {conditionalAccessData.filter((policy) => policy.state === "enabled").length} - - Enabled - - - - { - conditionalAccessData.filter( - (policy) => policy.state === "enabledForReportingButNotEnforced" - ).length - } - - Report Only - - - - { - conditionalAccessData.filter( - (policy) => policy.builtInControls && policy.builtInControls.includes("mfa") - ).length - } - - MFA Policies + + + {conditionalAccessData.length} + Total Policies + + + + {conditionalAccessData.filter((policy) => policy.state === "enabled").length} + + Enabled + + + + { + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + } + + Report Only + + + + { + conditionalAccessData.filter( + (policy) => + policy.builtInControls && policy.builtInControls.includes("mfa") + ).length + } + + MFA Policies + - - - Policy Analysis + + Policy Analysis - - - - - Policy Coverage:{" "} - {conditionalAccessData.length} conditional access policies configured - - - - - - Enforcement Status:{" "} - {conditionalAccessData.filter((policy) => policy.state === "enabled").length}{" "} - policies actively enforced - - - - - - Testing Phase:{" "} - { - conditionalAccessData.filter( - (policy) => policy.state === "enabledForReportingButNotEnforced" - ).length - }{" "} - policies in report-only mode - - - - - - Security Controls: Multi-factor - authentication and access blocking implemented - + + + + + Policy Coverage:{" "} + {conditionalAccessData.length} conditional access policies configured + + + + + + Enforcement Status:{" "} + {conditionalAccessData.filter((policy) => policy.state === "enabled").length}{" "} + policies actively enforced + + + + + + Testing Phase:{" "} + { + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + }{" "} + policies in report-only mode + + + + + + Security Controls:{" "} + Multi-factor authentication and access blocking implemented + + - - - Access Control Recommendations - - {conditionalAccessData.filter( - (policy) => policy.state === "enabledForReportingButNotEnforced" - ).length > 0 - ? `Consider activating ${ - conditionalAccessData.filter( - (policy) => policy.state === "enabledForReportingButNotEnforced" - ).length - } policies currently in testing mode after ensuring they don't disrupt business operations. ` - : "Your access controls are properly configured. "} - Regularly review how these policies affect employee productivity and adjust as - needed. Consider additional location-based protections for enhanced security without - impacting daily operations. - - + + Access Control Recommendations + + {conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length > 0 + ? `Consider activating ${ + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + } policies currently in testing mode after ensuring they don't disrupt business operations. ` + : "Your access controls are properly configured. "} + Regularly review how these policies affect employee productivity and adjust as + needed. Consider additional location-based protections for enhanced security + without impacting daily operations. + + - - `Page ${pageNumber} of ${totalPages}`} - /> - - - - )} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} ); }; export const ExecutiveReportButton = (props) => { const { tenantName, tenantId, userStats, standardsData, organizationData, ...other } = props; - + console.log(props); const settings = useSettings(); const brandingSettings = settings.customBranding; @@ -1809,7 +1921,7 @@ export const ExecutiveReportButton = (props) => { deviceData.isFetching || conditionalAccessData.isFetching || standardsCompareData.isFetching; - + const hasAllDataFinished = (secureScore.isSuccess || secureScore.isError) && (licenseData.isSuccess || licenseData.isError) && @@ -1860,7 +1972,9 @@ export const ExecutiveReportButton = (props) => { secureScoreData={secureScore.isSuccess ? secureScore : null} licensingData={licenseData.isSuccess ? licenseData?.data : null} deviceData={deviceData.isSuccess ? deviceData?.data : null} - conditionalAccessData={conditionalAccessData.isSuccess ? conditionalAccessData?.data : null} + conditionalAccessData={ + conditionalAccessData.isSuccess ? conditionalAccessData?.data : null + } standardsCompareData={standardsCompareData.isSuccess ? standardsCompareData?.data : null} /> } diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 43b53d80d12d..c39642c0fa91 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -4,12 +4,6 @@ import LoadingPage from "../pages/loading.js"; import ApiOfflinePage from "../pages/api-offline.js"; export const PrivateRoute = ({ children, routeType }) => { - const apiRoles = ApiGetCall({ - url: "/api/me", - queryKey: "authmecipp", - retry: 2, // Reduced retry count to show offline message sooner - }); - const session = ApiGetCall({ url: "/.auth/me", queryKey: "authmeswa", @@ -17,6 +11,13 @@ export const PrivateRoute = ({ children, routeType }) => { staleTime: 120000, // 2 minutes }); + const apiRoles = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + retry: 2, // Reduced retry count to show offline message sooner + waiting: !session.isSuccess || session.data?.clientPrincipal === null, + }); + // Check if the session is still loading before determining authentication status if ( session.isLoading || diff --git a/src/components/linearProgressWithLabel.jsx b/src/components/linearProgressWithLabel.jsx index 1dea47fa4890..55b4db2967bd 100644 --- a/src/components/linearProgressWithLabel.jsx +++ b/src/components/linearProgressWithLabel.jsx @@ -1,12 +1,65 @@ import { Box, LinearProgress } from "@mui/material"; export const LinearProgressWithLabel = (props) => { + const { value, colourLevels, addedLabel, ...otherProps } = props; + + // Function to determine color based on value and colourLevels + const getProgressColor = (value, colourLevels) => { + if (!colourLevels) { + return undefined; // Use default MUI color + } + + // Check if flipped mode is enabled + const isFlipped = colourLevels === 'flipped' || colourLevels.flipped === true; + + if (isFlipped) { + // Flipped color order: green -> yellow -> orange -> red + if (value >= 0 && value < 25) { + return "#4caf50"; // Green for low values when flipped + } else if (value >= 25 && value < 50) { + return "#ffeb3b"; // Yellow + } else if (value >= 50 && value < 75) { + return "#ff9800"; // Orange + } else if (value >= 75 && value <= 100) { + return "#f44336"; // Red for high values when flipped + } + } else { + // Normal color order: red -> orange -> yellow -> green + if (value >= 0 && value < 25) { + return colourLevels.level0to25 || "#f44336"; // Default red + } else if (value >= 25 && value < 50) { + return colourLevels.level25to50 || "#ff9800"; // Default orange + } else if (value >= 50 && value < 75) { + return colourLevels.level50to75 || "#ffeb3b"; // Default yellow + } else if (value >= 75 && value <= 100) { + return colourLevels.level75to100 || "#4caf50"; // Default green + } + } + + return undefined; // Fallback to default + }; + + const progressColor = getProgressColor(value, colourLevels); + return ( - + - {`${Math.round(props.value)}% ${props?.addedLabel ?? ""}`} + {`${Math.round(value)}% ${addedLabel ?? ""}`} ); }; diff --git a/src/data/GDAPRoles.json b/src/data/GDAPRoles.json index 26d4fc6e5eea..1d3ca9b38094 100644 --- a/src/data/GDAPRoles.json +++ b/src/data/GDAPRoles.json @@ -279,6 +279,14 @@ "Name": "Dynamics 365 Administrator", "ObjectId": "44367163-eba1-44c3-98af-f5787879f96a" }, + { + "ExtensionData": {}, + "Description": "Access and perform all administrative tasks on Dynamics 365 Business Central environments.", + "IsEnabled": true, + "IsSystem": true, + "Name": "Dynamics 365 Business Central Administrator", + "ObjectId": "963797fb-eb3b-4cde-8ce3-5878b3f32a3f" + }, { "ExtensionData": {}, "Description": "Manage all aspects of Microsoft Edge.", diff --git a/src/data/M365Licenses.json b/src/data/M365Licenses.json index d206d18e2f67..7774dc82f0aa 100644 --- a/src/data/M365Licenses.json +++ b/src/data/M365Licenses.json @@ -167,6 +167,14 @@ "Service_Plan_Id": "f7e5b77d-f293-410a-bae8-f941f19fe680", "Service_Plans_Included_Friendly_Names": "OneDrive for Business (Clipchamp)" }, + { + "Product_Display_Name": "Clipchamp Premium Add-on", + "String_Id": "Clipchamp_Premium_Add_on", + "GUID": "4b2c20e4-939d-4bf4-9dd8-6870240cfe19", + "Service_Plan_Name": "CLIPCHAMP_PREMIUM", + "Service_Plan_Id": "430b908f-78e1-4812-b045-cf83320e7d5d", + "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp Premium" + }, { "Product_Display_Name": "Microsoft 365 Audio Conferencing", "String_Id": "MCOMEETADV", @@ -3351,6 +3359,62 @@ "Service_Plan_Id": "26fa8a18-2812-4b3d-96b4-864818ce26be", "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365 Mixed Reality" }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "Forms_Pro_Talent", + "Service_Plan_Id": "1c4ae475-5608-43fa-b3f7-d20e07cf24b4", + "Service_Plans_Included_Friendly_Names": "Microsoft Dynamics 365 Customer Voice for Talent" + }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "Power_Pages_Internal_User", + "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941", + "Service_Plans_Included_Friendly_Names": "Power Pages Internal User" + }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "D365_HR_SELF_SERVICE_OPS", + "Service_Plan_Id": "835b837b-63c1-410e-bf6b-bdef201ad129", + "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resource Self Service" + }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "D365_HR_OPS", + "Service_Plan_Id": "8b21a5dc-5485-49ed-a2d4-0e772c830f6d", + "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resources" + }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "D365_HR_Attach", + "Service_Plan_Id": "3219525a-4064-45ec-9c35-a33ea6b39a49", + "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resources Attach" + }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "D365_HR_ATTACH_OPS", + "Service_Plan_Id": "90d8cb62-e98a-4639-8342-8c7d2c8215ba", + "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resources Attach License" + }, + { + "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer", + "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH", + "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621", + "Service_Plan_Name": "EXCHANGE_S_FOUNDATION", + "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", + "Service_Plans_Included_Friendly_Names": "Exchange Foundation" + }, { "Product_Display_Name": "Dynamics 365 Hybrid Connector", "String_Id": "CRM_HYBRIDCONNECTOR", @@ -7744,15 +7808,15 @@ "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "AAD_BASIC_EDU", "Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education" + "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "RMS_S_ENTERPRISE", @@ -7760,7 +7824,7 @@ "Service_Plans_Included_Friendly_Names": "Azure Rights Management" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "CDS_O365_P3", @@ -7768,7 +7832,7 @@ "Service_Plans_Included_Friendly_Names": "Common Data Service for Teams" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "LOCKBOX_ENTERPRISE", @@ -7776,7 +7840,15 @@ "Service_Plans_Included_Friendly_Names": "Customer Lockbox" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "CustomerLockboxA_Enterprise", + "Service_Plan_Id": "3ec18638-bd4c-4d3b-8905-479ed636b83e", + "Service_Plans_Included_Friendly_Names": "Customer Lockbox (A)" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MIP_S_Exchange", @@ -7784,7 +7856,15 @@ "Service_Plans_Included_Friendly_Names": "Data Classification in Microsoft 365" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "COMMON_DEFENDER_PLATFORM_FOR_OFFICE", + "Service_Plan_Id": "a312bdeb-1e21-40d0-84b1-0e73f128144f", + "Service_Plans_Included_Friendly_Names": "Defender Platform for Office 365" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "EducationAnalyticsP1", @@ -7792,7 +7872,7 @@ "Service_Plans_Included_Friendly_Names": "Education Analytics" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "EXCHANGE_S_ENTERPRISE", @@ -7800,7 +7880,15 @@ "Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX", + "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757", + "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "INFORMATION_BARRIERS", @@ -7808,7 +7896,7 @@ "Service_Plans_Included_Friendly_Names": "Information Barriers" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "Content_Explorer", @@ -7816,7 +7904,7 @@ "Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics - Premium" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "ContentExplorer_Standard", @@ -7824,7 +7912,7 @@ "Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics – Standard" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MIP_S_CLP2", @@ -7832,7 +7920,7 @@ "Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Premium" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MIP_S_CLP1", @@ -7840,7 +7928,7 @@ "Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Standard" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "M365_ADVANCED_AUDITING", @@ -7848,15 +7936,15 @@ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Advanced Auditing" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "OFFICESUBSCRIPTION", "Service_Plan_Id": "43de0ff5-c92c-492b-9116-175376d08c38", - "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for Enterprise" + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for enterprise" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MCOMEETADV", @@ -7864,7 +7952,15 @@ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audio Conferencing" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "M365_AUDIT_PLATFORM", + "Service_Plan_Id": "f6de4823-28fa-440b-b886-4783fa86ddba", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audit Platform" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MICROSOFT_COMMUNICATION_COMPLIANCE", @@ -7872,7 +7968,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Communication Compliance" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MTP", @@ -7880,7 +7976,15 @@ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Defender" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1", + "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MCOEV", @@ -7888,7 +7992,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Phone System" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MICROSOFTBOOKINGS", @@ -7896,7 +8000,15 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Bookings" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "CLIPCHAMP", + "Service_Plan_Id": "a1ace008-72f3-4ea0-8dac-33b3a23a2472", + "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "COMMUNICATIONS_DLP", @@ -7904,7 +8016,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Communications DLP" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "CUSTOMER_KEY", @@ -7912,15 +8024,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Customer Key" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", - "String_Id": "M365EDU_A5_FACULTY", - "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", - "Service_Plan_Name": "DATA_INVESTIGATIONS", - "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9", - "Service_Plans_Included_Friendly_Names": "Microsoft Data Investigations" - }, - { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "ATP_ENTERPRISE", @@ -7928,7 +8032,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 1)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "THREAT_INTELLIGENCE", @@ -7936,7 +8040,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 2)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "EXCEL_PREMIUM", @@ -7944,7 +8048,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Excel Advanced Analytics" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "OFFICE_FORMS_PLAN_3", @@ -7952,7 +8056,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Forms (Plan 3)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "INFO_GOVERNANCE", @@ -7960,7 +8064,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Information Governance" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "INSIDER_RISK", @@ -7968,7 +8072,15 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT", + "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34", + "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management - Exchange" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "KAIZALA_STANDALONE", @@ -7976,7 +8088,15 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "MICROSOFT_LOOP", + "Service_Plan_Id": "c4b8c31a-fb44-4c65-9837-a21f55fcabda", + "Service_Plans_Included_Friendly_Names": "Microsoft Loop" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "ML_CLASSIFICATION", @@ -7984,7 +8104,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft ML-Based Classification" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "EXCHANGE_ANALYTICS", @@ -7992,7 +8112,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft MyAnalytics (Full)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "PROJECTWORKMANAGEMENT", @@ -8000,7 +8120,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Planner" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "RECORDS_MANAGEMENT", @@ -8008,7 +8128,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Records Management" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MICROSOFT_SEARCH", @@ -8016,7 +8136,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Search" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "Deskless", @@ -8024,7 +8144,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft StaffHub" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "STREAM_O365_E5", @@ -8032,7 +8152,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Stream for Office 365 E5" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "TEAMS1", @@ -8040,7 +8160,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Teams" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MINECRAFT_EDUCATION_EDITION", @@ -8048,7 +8168,7 @@ "Service_Plans_Included_Friendly_Names": "Minecraft Education Edition" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "INTUNE_O365", @@ -8056,7 +8176,7 @@ "Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "Nucleus", @@ -8064,7 +8184,7 @@ "Service_Plans_Included_Friendly_Names": "Nucleus" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "EQUIVIO_ANALYTICS", @@ -8072,7 +8192,7 @@ "Service_Plans_Included_Friendly_Names": "Office 365 Advanced eDiscovery" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "ADALLOM_S_O365", @@ -8080,7 +8200,7 @@ "Service_Plans_Included_Friendly_Names": "Office 365 Cloud App Security" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "PAM_ENTERPRISE", @@ -8088,7 +8208,7 @@ "Service_Plans_Included_Friendly_Names": "Office 365 Privileged Access Management" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "SAFEDOCS", @@ -8096,7 +8216,7 @@ "Service_Plans_Included_Friendly_Names": "Office 365 SafeDocs" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "SHAREPOINTWAC_EDU", @@ -8104,7 +8224,7 @@ "Service_Plans_Included_Friendly_Names": "Office for the Web for Education" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "POWERAPPS_O365_P3", @@ -8112,7 +8232,7 @@ "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365 (Plan 3)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "BI_AZURE_P2", @@ -8120,7 +8240,7 @@ "Service_Plans_Included_Friendly_Names": "Power BI Pro" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "PREMIUM_ENCRYPTION", @@ -8128,7 +8248,7 @@ "Service_Plans_Included_Friendly_Names": "Premium Encryption in Office 365" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "PROJECT_O365_P3", @@ -8136,23 +8256,39 @@ "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E5)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "PURVIEW_DISCOVERY", + "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522", + "Service_Plans_Included_Friendly_Names": "Purview Discovery" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "Bing_Chat_Enterprise", + "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba", + "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "COMMUNICATIONS_COMPLIANCE", "Service_Plan_Id": "41fcdd7d-4733-4863-9cf4-c65b83ce2df4", - "Service_Plans_Included_Friendly_Names": "Microsoft Communications Compliance" + "Service_Plans_Included_Friendly_Names": "RETIRED - Microsoft Communications Compliance" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", - "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT", - "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34", - "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management" + "Service_Plan_Name": "DATA_INVESTIGATIONS", + "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9", + "Service_Plans_Included_Friendly_Names": "Retired - Microsoft Data Investigations" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "SCHOOL_DATA_SYNC_P2", @@ -8160,7 +8296,7 @@ "Service_Plans_Included_Friendly_Names": "School Data Sync (Plan 2)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "SHAREPOINTENTERPRISE_EDU", @@ -8168,7 +8304,7 @@ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2) for Education" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MCOSTANDARD", @@ -8176,7 +8312,7 @@ "Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "SWAY", @@ -8184,7 +8320,7 @@ "Service_Plans_Included_Friendly_Names": "Sway" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "BPOS_S_TODO_3", @@ -8192,7 +8328,7 @@ "Service_Plans_Included_Friendly_Names": "To-Do (Plan 3)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "VIVA_LEARNING_SEEDED", @@ -8200,7 +8336,7 @@ "Service_Plans_Included_Friendly_Names": "Viva Learning Seeded" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "WHITEBOARD_PLAN3", @@ -8208,7 +8344,7 @@ "Service_Plans_Included_Friendly_Names": "Whiteboard (Plan 3)" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "YAMMER_EDU", @@ -8216,7 +8352,7 @@ "Service_Plans_Included_Friendly_Names": "Yammer for Academic" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "WINDEFATP", @@ -8224,7 +8360,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Endpoint" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MICROSOFTENDPOINTDLP", @@ -8232,7 +8368,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Endpoint DLP" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "UNIVERSAL_PRINT_01", @@ -8240,7 +8376,7 @@ "Service_Plans_Included_Friendly_Names": "Universal Print" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "Virtualization Rights for Windows 10 (E3/E5+VDA)", @@ -8248,7 +8384,7 @@ "Service_Plans_Included_Friendly_Names": "Windows 10/11 Enterprise" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "WINDOWSUPDATEFORBUSINESS_DEPLOYMENTSERVICE", @@ -8256,23 +8392,7 @@ "Service_Plans_Included_Friendly_Names": "Windows Update for Business Deployment Service" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", - "String_Id": "M365EDU_A5_FACULTY", - "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", - "Service_Plan_Name": "AAD_PREMIUM", - "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1" - }, - { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", - "String_Id": "M365EDU_A5_FACULTY", - "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", - "Service_Plan_Name": "AAD_PREMIUM_P2", - "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2" - }, - { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "RMS_S_PREMIUM", @@ -8280,7 +8400,7 @@ "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P1" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "RMS_S_PREMIUM2", @@ -8288,7 +8408,7 @@ "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P2" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "DYN365_CDS_O365_P3", @@ -8296,7 +8416,15 @@ "Service_Plans_Included_Friendly_Names": "Common Data Service" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "Intune_ServiceNow", + "Service_Plan_Id": "3eeb8536-fecf-41bf-a3f8-d6f17a9f3efc", + "Service_Plans_Included_Friendly_Names": "Intune ServiceNow Integration" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "MFA_PREMIUM", @@ -8304,7 +8432,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Azure Multi-Factor Authentication" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "ADALLOM_S_STANDALONE", @@ -8312,7 +8440,7 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Cloud Apps" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "ATA", @@ -8320,23 +8448,39 @@ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Identity" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "AAD_PREMIUM", + "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "AAD_PREMIUM_P2", + "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "INTUNE_A", "Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5", - "Service_Plans_Included_Friendly_Names": "Microsoft Intune" + "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "INTUNE_EDU", "Service_Plan_Id": "da24caf9-af8e-485c-b7c8-e73336da2693", - "Service_Plans_Included_Friendly_Names": "Microsoft Intune for Education" + "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1 for Education" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "FLOW_O365_P3", @@ -8344,20 +8488,28 @@ "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" }, { - "Product_Display_Name": "Microsoft 365 A5 for Faculty", + "Product_Display_Name": "Microsoft 365 A5 for faculty", "String_Id": "M365EDU_A5_FACULTY", "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_O365_P3", "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014", "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365" }, + { + "Product_Display_Name": "Microsoft 365 A5 for faculty", + "String_Id": "M365EDU_A5_FACULTY", + "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370", + "Service_Plan_Name": "REMOTE_HELP", + "Service_Plan_Id": "a4c6cf29-1168-4076-ba5c-e8fe0e62b17e", + "Service_Plans_Included_Friendly_Names": "Remote help" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", "Service_Plan_Name": "AAD_BASIC_EDU", "Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education" + "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education" }, { "Product_Display_Name": "Microsoft 365 A5 for Students", @@ -8383,6 +8535,14 @@ "Service_Plan_Id": "9f431833-0334-42de-a7dc-70aa40db46db", "Service_Plans_Included_Friendly_Names": "Customer Lockbox" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "CustomerLockboxA_Enterprise", + "Service_Plan_Id": "3ec18638-bd4c-4d3b-8905-479ed636b83e", + "Service_Plans_Included_Friendly_Names": "Customer Lockbox (A)" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8391,6 +8551,14 @@ "Service_Plan_Id": "cd31b152-6326-4d1b-ae1b-997b625182e6", "Service_Plans_Included_Friendly_Names": "Data Classification in Microsoft 365" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "COMMON_DEFENDER_PLATFORM_FOR_OFFICE", + "Service_Plan_Id": "a312bdeb-1e21-40d0-84b1-0e73f128144f", + "Service_Plans_Included_Friendly_Names": "Defender Platform for Office 365" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8407,6 +8575,14 @@ "Service_Plan_Id": "efb87545-963c-4e0d-99df-69c6916d9eb0", "Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2)" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX", + "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757", + "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8461,7 +8637,7 @@ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", "Service_Plan_Name": "OFFICESUBSCRIPTION", "Service_Plan_Id": "43de0ff5-c92c-492b-9116-175376d08c38", - "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for Enterprise" + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for enterprise" }, { "Product_Display_Name": "Microsoft 365 A5 for Students", @@ -8471,6 +8647,14 @@ "Service_Plan_Id": "3e26ee1f-8a5f-4d52-aee2-b81ce45c8f40", "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audio Conferencing" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "M365_AUDIT_PLATFORM", + "Service_Plan_Id": "f6de4823-28fa-440b-b886-4783fa86ddba", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audit Platform" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8487,6 +8671,14 @@ "Service_Plan_Id": "bf28f719-7844-4079-9c78-c1307898e192", "Service_Plans_Included_Friendly_Names": "Microsoft 365 Defender" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1", + "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8503,6 +8695,14 @@ "Service_Plan_Id": "199a5c09-e0ca-4e37-8f7c-b05d533e1ea2", "Service_Plans_Included_Friendly_Names": "Microsoft Bookings" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "CLIPCHAMP", + "Service_Plan_Id": "a1ace008-72f3-4ea0-8dac-33b3a23a2472", + "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8519,14 +8719,6 @@ "Service_Plan_Id": "6db1f1db-2b46-403f-be40-e39395f08dbb", "Service_Plans_Included_Friendly_Names": "Microsoft Customer Key" }, - { - "Product_Display_Name": "Microsoft 365 A5 for Students", - "String_Id": "M365EDU_A5_STUDENT", - "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", - "Service_Plan_Name": "DATA_INVESTIGATIONS", - "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9", - "Service_Plans_Included_Friendly_Names": "Microsoft Data Investigations" - }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8575,6 +8767,14 @@ "Service_Plan_Id": "d587c7a3-bda9-4f99-8776-9bcf59c84f75", "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT", + "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34", + "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management - Exchange" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8583,6 +8783,14 @@ "Service_Plan_Id": "0898bdbb-73b0-471a-81e5-20f1fe4dd66e", "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "MICROSOFT_LOOP", + "Service_Plan_Id": "c4b8c31a-fb44-4c65-9837-a21f55fcabda", + "Service_Plans_Included_Friendly_Names": "Microsoft Loop" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8735,6 +8943,22 @@ "Service_Plan_Id": "b21a6b06-1988-436e-a07b-51ec6d9f52ad", "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E5)" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "PURVIEW_DISCOVERY", + "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522", + "Service_Plans_Included_Friendly_Names": "Purview Discovery" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "Bing_Chat_Enterprise", + "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba", + "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8747,9 +8971,9 @@ "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", - "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT", - "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34", - "Service_Plans_Included_Friendly_Names": "RETIRED - Microsoft Insider Risk Management" + "Service_Plan_Name": "DATA_INVESTIGATIONS", + "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9", + "Service_Plans_Included_Friendly_Names": "Retired - Microsoft Data Investigations" }, { "Product_Display_Name": "Microsoft 365 A5 for Students", @@ -8847,22 +9071,6 @@ "Service_Plan_Id": "7bf960f6-2cd9-443a-8046-5dbff9558365", "Service_Plans_Included_Friendly_Names": "Windows Update for Business Deployment Service" }, - { - "Product_Display_Name": "Microsoft 365 A5 for Students", - "String_Id": "M365EDU_A5_STUDENT", - "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", - "Service_Plan_Name": "AAD_PREMIUM", - "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1" - }, - { - "Product_Display_Name": "Microsoft 365 A5 for Students", - "String_Id": "M365EDU_A5_STUDENT", - "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", - "Service_Plan_Name": "AAD_PREMIUM_P2", - "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2" - }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8887,6 +9095,14 @@ "Service_Plan_Id": "28b0fa46-c39a-4188-89e2-58e979a6b014", "Service_Plans_Included_Friendly_Names": "Common Data Service" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "Intune_ServiceNow", + "Service_Plan_Id": "3eeb8536-fecf-41bf-a3f8-d6f17a9f3efc", + "Service_Plans_Included_Friendly_Names": "Intune ServiceNow Integration" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", @@ -8911,13 +9127,29 @@ "Service_Plan_Id": "14ab5db5-e6c4-4b20-b4bc-13e36fd2227f", "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Identity" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "AAD_PREMIUM", + "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1" + }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "AAD_PREMIUM_P2", + "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2" + }, { "Product_Display_Name": "Microsoft 365 A5 for Students", "String_Id": "M365EDU_A5_STUDENT", "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", "Service_Plan_Name": "INTUNE_A", "Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5", - "Service_Plans_Included_Friendly_Names": "Microsoft Intune" + "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1" }, { "Product_Display_Name": "Microsoft 365 A5 for Students", @@ -8925,7 +9157,7 @@ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", "Service_Plan_Name": "INTUNE_EDU", "Service_Plan_Id": "da24caf9-af8e-485c-b7c8-e73336da2693", - "Service_Plans_Included_Friendly_Names": "Microsoft Intune for Education" + "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1 for Education" }, { "Product_Display_Name": "Microsoft 365 A5 for Students", @@ -8943,6 +9175,14 @@ "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014", "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365" }, + { + "Product_Display_Name": "Microsoft 365 A5 for Students", + "String_Id": "M365EDU_A5_STUDENT", + "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e", + "Service_Plan_Name": "REMOTE_HELP", + "Service_Plan_Id": "a4c6cf29-1168-4076-ba5c-e8fe0e62b17e", + "Service_Plans_Included_Friendly_Names": "Remote help" + }, { "Product_Display_Name": "Microsoft 365 A5 for students use benefit", "String_Id": "M365EDU_A5_STUUSEBNFT", @@ -29071,6 +29311,38 @@ "Service_Plan_Id": "e866a266-3cff-43a3-acca-0c90a7e00c8b", "Service_Plans_Included_Friendly_Names": "Entra Identity Governance" }, + { + "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2", + "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2", + "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37", + "Service_Plan_Name": "Entra_Premium_Internet_Access", + "Service_Plan_Id": "8d23cb83-ab07-418f-8517-d7aca77307dc", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra Internet Access" + }, + { + "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2", + "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2", + "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37", + "Service_Plan_Name": "Entra_Premium_Private_Access", + "Service_Plan_Id": "f057aab1-b184-49b2-85c0-881b02a405c5", + "Service_Plans_Included_Friendly_Names": "Microsoft Entra Private Access" + }, + { + "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2", + "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2", + "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37", + "Service_Plan_Name": "Verifiable_Credentials_Service_Request", + "Service_Plan_Id": "aae826b7-14cd-4691-8178-2b312f7072ea", + "Service_Plans_Included_Friendly_Names": "Verifiable Credentials Service Request" + }, + { + "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2", + "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2", + "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37", + "Service_Plan_Name": "Entra_Identity_Governance", + "Service_Plan_Id": "e866a266-3cff-43a3-acca-0c90a7e00c8b", + "Service_Plans_Included_Friendly_Names": "Entra Identity Governance" + }, { "Product_Display_Name": "Microsoft Fabric (Free)", "String_Id": "POWER_BI_STANDARD", @@ -29087,6 +29359,14 @@ "Service_Plan_Id": "2049e525-b859-401b-b2a0-e0a31c4b1fe4", "Service_Plans_Included_Friendly_Names": "Power BI (free)" }, + { + "Product_Display_Name": "Microsoft Fabric (Free)", + "String_Id": "POWER_BI_STANDARD", + "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235", + "Service_Plan_Name": "PURVIEW_DISCOVERY", + "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522", + "Service_Plans_Included_Friendly_Names": "Purview Discovery" + }, { "Product_Display_Name": "Microsoft Fabric (Free) for faculty", "String_Id": "POWER_BI_STANDARD_FACULTY", @@ -29127,6 +29407,14 @@ "Service_Plan_Id": "d736def0-1fde-43f0-a5be-e3f8b2de6e41", "Service_Plans_Included_Friendly_Names": "MS IMAGINE ACADEMY" }, + { + "Product_Display_Name": "Microsoft Intune Advanced Analytics", + "String_Id": "Microsoft_Intune_Advanced_Analytics", + "GUID": "5e36d0d4-e9e5-4052-aba0-0257465c9b86", + "Service_Plan_Name": "Intune_AdvancedEA", + "Service_Plan_Id": "2a4baa0e-5e99-4c38-b1f2-6864960f1bd1", + "Service_Plans_Included_Friendly_Names": "Microsoft Intune Advanced Analytics" + }, { "Product_Display_Name": "Microsoft Intune Device", "String_Id": "INTUNE_A_D", @@ -29575,6 +29863,38 @@ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", "Service_Plans_Included_Friendly_Names": "Exchange Foundation" }, + { + "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus", + "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON", + "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a", + "Service_Plan_Name": "MCS_BIZAPPS_CLOUD_FOR_SUSTAINABILITY_USL_PLUS", + "Service_Plan_Id": "beaf5b5c-d11c-4417-b5cb-cd9f9e6719b0", + "Service_Plans_Included_Friendly_Names": "MCS - BizApps Cloud for Sustainability USL Plus" + }, + { + "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus", + "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON", + "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a", + "Service_Plan_Name": "POWER_APPS_FOR_MCS_USL_PLUS", + "Service_Plan_Id": "c5502fe7-406d-442a-827f-4948b821ba08", + "Service_Plans_Included_Friendly_Names": "Power Apps for Cloud for Sustainability USL Plus" + }, + { + "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus", + "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON", + "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a", + "Service_Plan_Name": "POWER_AUTOMATE_FOR_MCS_USL_PLUS", + "Service_Plan_Id": "1c22bb50-96fb-49e5-baa6-195cab19eee2", + "Service_Plans_Included_Friendly_Names": "Power Automate for Cloud for Sustainability USL Plus" + }, + { + "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus", + "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON", + "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a", + "Service_Plan_Name": "EXCHANGE_S_FOUNDATION", + "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", + "Service_Plans_Included_Friendly_Names": "Exchange Foundation" + }, { "Product_Display_Name": "Microsoft Sustainability Manager USL Essentials", "String_Id": "Microsoft_Cloud_for_Sustainability_USL", @@ -30477,7 +30797,7 @@ "GUID": "c25e2b36-e161-4946-bef2-69239729f690", "Service_Plan_Name": "AAD_BASIC_EDU", "Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education" + "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education" }, { "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU", @@ -30519,6 +30839,14 @@ "Service_Plan_Id": "0feaeb32-d00e-4d66-bd5a-43b5b83db82c", "Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)" }, + { + "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU", + "String_Id": "Microsoft_Teams_Rooms_Pro_FAC", + "GUID": "c25e2b36-e161-4946-bef2-69239729f690", + "Service_Plan_Name": "Teams_Rooms_Pro", + "Service_Plan_Id": "0374d34c-6be4-4dbb-b3f0-26105db0b28a", + "Service_Plans_Included_Friendly_Names": "Teams Rooms Pro" + }, { "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU", "String_Id": "Microsoft_Teams_Rooms_Pro_FAC", @@ -30567,6 +30895,14 @@ "Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5", "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1" }, + { + "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU", + "String_Id": "Microsoft_Teams_Rooms_Pro_FAC", + "GUID": "c25e2b36-e161-4946-bef2-69239729f690", + "Service_Plan_Name": "SPECIALTY_DEVICES", + "Service_Plan_Id": "cfce7ae3-4b41-4438-999c-c0e91f3b7fb9", + "Service_Plans_Included_Friendly_Names": "Specialty devices" + }, { "Product_Display_Name": "Microsoft Teams Rooms Pro for GCC", "String_Id": "Microsoft_Teams_Rooms_Pro_GCC", @@ -31325,15 +31661,15 @@ "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", "Service_Plan_Name": "AAD_BASIC_EDU", "Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education" + "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education" }, { "Product_Display_Name": "Office 365 A1 for faculty", "String_Id": "STANDARDWOFFPACK_FACULTY", "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", - "Service_Plan_Name": "DYN365_CDS_O365_P1", - "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4", - "Service_Plans_Included_Friendly_Names": "Common Data Service - O365 P1" + "Service_Plan_Name": "RMS_S_ENTERPRISE", + "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90", + "Service_Plans_Included_Friendly_Names": "Azure Rights Management" }, { "Product_Display_Name": "Office 365 A1 for faculty", @@ -31363,9 +31699,9 @@ "Product_Display_Name": "Office 365 A1 for faculty", "String_Id": "STANDARDWOFFPACK_FACULTY", "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", - "Service_Plan_Name": "RMS_S_ENTERPRISE", - "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90", - "Service_Plans_Included_Friendly_Names": "Microsoft Microsoft Entra Rights" + "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1", + "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)" }, { "Product_Display_Name": "Office 365 A1 for faculty", @@ -31381,7 +31717,7 @@ "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", "Service_Plan_Name": "KAIZALA_O365_P2", "Service_Plan_Id": "54fc630f-5a40-48ee-8965-af0503c1386e", - "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro Plan 2" + "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro" }, { "Product_Display_Name": "Office 365 A1 for faculty", @@ -31459,25 +31795,17 @@ "Product_Display_Name": "Office 365 A1 for faculty", "String_Id": "STANDARDWOFFPACK_FACULTY", "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", - "Service_Plan_Name": "POWERAPPS_O365_P2", - "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792", - "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365" - }, - { - "Product_Display_Name": "Office 365 A1 for faculty", - "String_Id": "STANDARDWOFFPACK_FACULTY", - "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", - "Service_Plan_Name": "FLOW_O365_P2", - "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9", - "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" + "Service_Plan_Name": "PROJECT_O365_P1", + "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2", + "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)" }, { "Product_Display_Name": "Office 365 A1 for faculty", "String_Id": "STANDARDWOFFPACK_FACULTY", "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", - "Service_Plan_Name": "PROJECT_O365_P1", - "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2", - "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)" + "Service_Plan_Name": "Bing_Chat_Enterprise", + "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba", + "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot" }, { "Product_Display_Name": "Office 365 A1 for faculty", @@ -31543,6 +31871,30 @@ "Service_Plan_Id": "2078e8df-cff6-4290-98cb-5408261a760a", "Service_Plans_Included_Friendly_Names": "Yammer for Academic" }, + { + "Product_Display_Name": "Office 365 A1 for faculty", + "String_Id": "STANDARDWOFFPACK_FACULTY", + "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", + "Service_Plan_Name": "DYN365_CDS_O365_P1", + "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4", + "Service_Plans_Included_Friendly_Names": "Common Data Service" + }, + { + "Product_Display_Name": "Office 365 A1 for faculty", + "String_Id": "STANDARDWOFFPACK_FACULTY", + "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", + "Service_Plan_Name": "POWERAPPS_O365_P2", + "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792", + "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365" + }, + { + "Product_Display_Name": "Office 365 A1 for faculty", + "String_Id": "STANDARDWOFFPACK_FACULTY", + "GUID": "94763226-9b3c-4e75-a931-5c89701abe66", + "Service_Plan_Name": "FLOW_O365_P2", + "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9", + "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" + }, { "Product_Display_Name": "Office 365 A1 Plus for faculty", "String_Id": "STANDARDWOFFPACK_IW_FACULTY", @@ -31765,15 +32117,15 @@ "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", "Service_Plan_Name": "AAD_BASIC_EDU", "Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426", - "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education" + "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education" }, { "Product_Display_Name": "Office 365 A1 for students", "String_Id": "STANDARDWOFFPACK_STUDENT", "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", - "Service_Plan_Name": "DYN365_CDS_O365_P1", - "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4", - "Service_Plans_Included_Friendly_Names": "Common Data Service - O365 P1" + "Service_Plan_Name": "RMS_S_ENTERPRISE", + "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90", + "Service_Plans_Included_Friendly_Names": "Azure Rights Management" }, { "Product_Display_Name": "Office 365 A1 for students", @@ -31803,9 +32155,9 @@ "Product_Display_Name": "Office 365 A1 for students", "String_Id": "STANDARDWOFFPACK_STUDENT", "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", - "Service_Plan_Name": "RMS_S_ENTERPRISE", - "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90", - "Service_Plans_Included_Friendly_Names": "Microsoft Microsoft Entra Rights" + "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1", + "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487", + "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)" }, { "Product_Display_Name": "Office 365 A1 for students", @@ -31821,7 +32173,7 @@ "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", "Service_Plan_Name": "KAIZALA_O365_P2", "Service_Plan_Id": "54fc630f-5a40-48ee-8965-af0503c1386e", - "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro Plan 2" + "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro" }, { "Product_Display_Name": "Office 365 A1 for students", @@ -31891,25 +32243,17 @@ "Product_Display_Name": "Office 365 A1 for students", "String_Id": "STANDARDWOFFPACK_STUDENT", "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", - "Service_Plan_Name": "POWERAPPS_O365_P2", - "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792", - "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365" - }, - { - "Product_Display_Name": "Office 365 A1 for students", - "String_Id": "STANDARDWOFFPACK_STUDENT", - "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", - "Service_Plan_Name": "FLOW_O365_P2", - "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9", - "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" + "Service_Plan_Name": "PROJECT_O365_P1", + "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2", + "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)" }, { "Product_Display_Name": "Office 365 A1 for students", "String_Id": "STANDARDWOFFPACK_STUDENT", "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", - "Service_Plan_Name": "PROJECT_O365_P1", - "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2", - "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)" + "Service_Plan_Name": "Bing_Chat_Enterprise", + "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba", + "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot" }, { "Product_Display_Name": "Office 365 A1 for students", @@ -31967,6 +32311,30 @@ "Service_Plan_Id": "2078e8df-cff6-4290-98cb-5408261a760a", "Service_Plans_Included_Friendly_Names": "Yammer for Academic" }, + { + "Product_Display_Name": "Office 365 A1 for students", + "String_Id": "STANDARDWOFFPACK_STUDENT", + "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", + "Service_Plan_Name": "DYN365_CDS_O365_P1", + "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4", + "Service_Plans_Included_Friendly_Names": "Common Data Service" + }, + { + "Product_Display_Name": "Office 365 A1 for students", + "String_Id": "STANDARDWOFFPACK_STUDENT", + "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", + "Service_Plan_Name": "POWERAPPS_O365_P2", + "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792", + "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365" + }, + { + "Product_Display_Name": "Office 365 A1 for students", + "String_Id": "STANDARDWOFFPACK_STUDENT", + "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91", + "Service_Plan_Name": "FLOW_O365_P2", + "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9", + "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365" + }, { "Product_Display_Name": "Office 365 A1 Plus for students", "String_Id": "STANDARDWOFFPACK_IW_STUDENT", @@ -40483,9 +40851,9 @@ "Product_Display_Name": "Power Apps Premium", "String_Id": "POWERAPPS_PER_USER", "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579", - "Service_Plan_Name": "DYN365_CDS_P2", - "Service_Plan_Id": "6ea4c1ef-c259-46df-bce2-943342cd3cb2", - "Service_Plans_Included_Friendly_Names": "Common Data Service - P2" + "Service_Plan_Name": "Power_Pages_Internal_User", + "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941", + "Service_Plans_Included_Friendly_Names": "Power Pages Internal User" }, { "Product_Display_Name": "Power Apps Premium", @@ -40495,6 +40863,30 @@ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318", "Service_Plans_Included_Friendly_Names": "Exchange Foundation" }, + { + "Product_Display_Name": "Power Apps Premium", + "String_Id": "POWERAPPS_PER_USER", + "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579", + "Service_Plan_Name": "CDSAICAPACITY_PERUSER_NEW", + "Service_Plan_Id": "74d93933-6f22-436e-9441-66d205435abb", + "Service_Plans_Included_Friendly_Names": "AI Builder capacity Per User add-on" + }, + { + "Product_Display_Name": "Power Apps Premium", + "String_Id": "POWERAPPS_PER_USER", + "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579", + "Service_Plan_Name": "DYN365_CDS_P2", + "Service_Plan_Id": "6ea4c1ef-c259-46df-bce2-943342cd3cb2", + "Service_Plans_Included_Friendly_Names": "Common Data Service" + }, + { + "Product_Display_Name": "Power Apps Premium", + "String_Id": "POWERAPPS_PER_USER", + "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579", + "Service_Plan_Name": "CDSAICAPACITY_PERUSER", + "Service_Plan_Id": "91f50f7b-2204-4803-acac-5cf5668b8b39", + "Service_Plans_Included_Friendly_Names": "DO NOT USE - AI Builder capacity Per User add-on" + }, { "Product_Display_Name": "Power Apps Premium", "String_Id": "POWERAPPS_PER_USER", @@ -41047,6 +41439,22 @@ "Service_Plan_Id": "0bf3c642-7bb5-4ccc-884e-59d09df0266c", "Service_Plans_Included_Friendly_Names": "Power BI Premium Per User" }, + { + "Product_Display_Name": "Power BI Premium Per User Add-On for Faculty", + "String_Id": "PBI_PREMIUM_PER_USER_ADDON_FACULTY", + "GUID": "c05b235f-be75-4029-8851-6a4170758eef", + "Service_Plan_Name": "BI_AZURE_P3", + "Service_Plan_Id": "0bf3c642-7bb5-4ccc-884e-59d09df0266c", + "Service_Plans_Included_Friendly_Names": "Power BI Premium Per User" + }, + { + "Product_Display_Name": "Power BI Premium Per User Add-On for Faculty", + "String_Id": "PBI_PREMIUM_PER_USER_ADDON_FACULTY", + "GUID": "c05b235f-be75-4029-8851-6a4170758eef", + "Service_Plan_Name": "PURVIEW_DISCOVERY", + "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522", + "Service_Plans_Included_Friendly_Names": "Purview Discovery" + }, { "Product_Display_Name": "Power BI Premium Per User Add-On for GCC", "String_Id": "PBI_PREMIUM_PER_USER_ADDON_CE_GCC", @@ -43415,6 +43823,70 @@ "Service_Plan_Id": "78b58230-ec7e-4309-913c-93a45cc4735b", "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Webinar" }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "MICROSOFT_ECDN", + "Service_Plan_Id": "85704d55-2e73-47ee-93b4-4b8ea14db92b", + "Service_Plans_Included_Friendly_Names": "Microsoft eCDN" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "TEAMSPRO_MGMT", + "Service_Plan_Id": "0504111f-feb8-4a3c-992a-70280f9a2869", + "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Intelligent" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "TEAMSPRO_CUST", + "Service_Plan_Id": "cc8c0802-a325-43df-8cba-995d0c6cb373", + "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Personalized" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "TEAMSPRO_PROTECTION", + "Service_Plan_Id": "f8b44f54-18bb-46a3-9658-44ab58712968", + "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Secure" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "TEAMSPRO_VIRTUALAPPT", + "Service_Plan_Id": "9104f592-f2a7-4f77-904c-ca5a5715883f", + "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Virtual Appointment" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "MCO_VIRTUAL_APPT", + "Service_Plan_Id": "711413d0-b36e-4cd4-93db-0a50a4ab7ea3", + "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Virtual Appointments" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "TEAMSPRO_WEBINAR", + "Service_Plan_Id": "78b58230-ec7e-4309-913c-93a45cc4735b", + "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Webinar" + }, + { + "Product_Display_Name": "Teams Premium for Faculty", + "String_Id": "Teams_Premium_for_Faculty", + "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d", + "Service_Plan_Name": "QUEUES_APP", + "Service_Plan_Id": "ab2d4fb5-f80a-4bf1-a11d-7f1da254041b", + "Service_Plans_Included_Friendly_Names": "Queues app for Microsoft Teams" + }, { "Product_Display_Name": "Teams Rooms Premium", "String_Id": "MTR_PREM", diff --git a/src/data/alerts.json b/src/data/alerts.json index af0270248982..9de9c369f85b 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -209,5 +209,15 @@ "label": "Alert on new risky users (P2 License Required)", "recommendedRunInterval": "30m", "description": "Monitors for new risky users in the tenant. Risky users are defined as users who have performed actions that are considered risky, such as password resets, MFA failures, or suspicious activity." + }, + { + "name": "LowTenantAlignment", + "label": "Alert on low tenant alignment percentage", + "requiresInput": true, + "inputType": "number", + "inputLabel": "Alert when alignment is below % (default: 99)", + "inputName": "InputValue", + "recommendedRunInterval": "1d", + "description": "Monitors tenant alignment scores against standards templates and alerts when the alignment percentage falls below the specified threshold. This helps ensure compliance across all managed tenants." } ] diff --git a/src/data/standards.json b/src/data/standards.json index 6be68e4a46e0..a9a336b70b3a 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -451,6 +451,21 @@ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy", "recommendedBy": [] }, + { + "name": "standards.AuthMethodsPolicyMigration", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Completes the migration of authentication methods policy to the new format", + "docsDescription": "Sets the authentication methods policy migration state to complete. This is required when migrating from legacy authentication policies to the new unified authentication methods policy.", + "executiveText": "Completes the transition from legacy authentication policies to Microsoft's modern unified authentication methods policy, ensuring the organization benefits from the latest security features and management capabilities. This migration enables enhanced security controls and simplified policy management.", + "addedComponent": [], + "label": "Complete Authentication Methods Policy Migration", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-07-07", + "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy", + "recommendedBy": ["CIPP"] + }, { "name": "standards.AppDeploy", "cat": "Entra (AAD) Standards", @@ -535,7 +550,6 @@ "cat": "Entra (AAD) Standards", "tag": [ "CIS M365 5.0 (2.3.1)", - "CIS M365 5.0 (5.2.3.2)", "EIDSCA.AM03", "EIDSCA.AM04", "EIDSCA.AM06", @@ -723,6 +737,28 @@ "powershellEquivalent": "Update-MgDomain", "recommendedBy": ["CIS", "CIPP"] }, + { + "name": "standards.CustomBannedPasswordList", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 5.0 (5.2.3.2)"], + "helpText": "**Requires Entra ID P1.** Updates and enables the Entra ID custom banned password list with the supplied words. Enter words separated by commas or semicolons. Each word must be 4-16 characters long. Maximum 1,000 words allowed.", + "docsDescription": "Updates and enables the Entra ID custom banned password list with the supplied words. This supplements the global banned password list maintained by Microsoft. The custom list is limited to 1,000 key base terms of 4-16 characters each. Entra ID will [block variations and common substitutions](https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection#configure-custom-banned-passwords) of these words in user passwords. [How are passwords evaluated?](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad#score-calculation)", + "addedComponent": [ + { + "type": "textField", + "name": "standards.CustomBannedPasswordList.BannedWords", + "label": "Banned Words", + "placeholder": "Banned words separated by commas or semicolons", + "required": true + } + ], + "label": "Set Entra ID Custom Banned Password List", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-06-28", + "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", + "recommendedBy": ["CIS"] + }, { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", @@ -1040,7 +1076,7 @@ "disabledFeatures": { "report": false, "warn": false, - "remediate": true + "remediate": false }, "label": "Cleanup stale Entra devices", "impact": "High Impact", @@ -3149,13 +3185,13 @@ "name": "standards.intuneDeviceRetirementDays", "cat": "Intune Standards", "tag": [], - "helpText": "A value between 0 and 270 is supported. A value of 0 disables retirement, retired devices are removed from Intune after the specified number of days.", + "helpText": "A value between 31 and 365 is supported. retired devices are removed from Intune after the specified number of days.", "executiveText": "Automatically removes inactive devices from management after a specified period, helping maintain a clean device inventory and reducing security risks from abandoned or lost devices. This policy ensures that only actively used corporate devices remain in the management system.", "addedComponent": [ { "type": "number", "name": "standards.intuneDeviceRetirementDays.days", - "label": "Maximum days (0 equals disabled)" + "label": "Maximum days" } ], "label": "Set inactive device retirement days", @@ -3564,8 +3600,8 @@ { "name": "standards.SPDirectSharing", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.7)", "CISA (MS.SPO.1.4v1)"], - "helpText": "Ensure default link sharing is set to Direct in SharePoint and OneDrive", + "tag": [], + "helpText": "This standard has been deprecated in favor of the Default Sharing Link standard. ", "executiveText": "Configures SharePoint and OneDrive to share files directly with specific people rather than creating anonymous links, improving security by ensuring only intended recipients can access shared documents. This reduces the risk of accidental data exposure through link sharing.", "addedComponent": [], "label": "Default sharing to Direct users", @@ -3615,6 +3651,40 @@ "powershellEquivalent": "Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15", "recommendedBy": ["CIS", "CIPP"] }, + { + "name": "standards.DefaultSharingLink", + "cat": "SharePoint Standards", + "tag": ["CIS M365 5.0 (7.2.7)", "CIS M365 5.0 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", + "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", + "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "required": true, + "label": "Default Sharing Link Type", + "name": "standards.DefaultSharingLink.SharingLinkType", + "options": [ + { + "label": "Direct - Only the people the user specifies", + "value": "Direct" + }, + { + "label": "Internal - Only people in your organization", + "value": "Internal" + } + ] + } + ], + "label": "Set Default Sharing Link Settings", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2025-06-13", + "powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType [Direct|Internal] -DefaultLinkPermission View", + "recommendedBy": ["CIS", "CIPP"] + }, { "name": "standards.DisableAddShortcutsToOneDrive", "cat": "SharePoint Standards", @@ -4616,6 +4686,11 @@ "label": "Set to report only" } ] + }, + { + "type": "switch", + "name": "DisableSD", + "label": "Disable Security Defaults when deploying policy" } ] }, diff --git a/src/hooks/use-permissions.js b/src/hooks/use-permissions.js new file mode 100644 index 000000000000..6b7973c164f8 --- /dev/null +++ b/src/hooks/use-permissions.js @@ -0,0 +1,96 @@ +import { useCallback } from "react"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { hasAccess, hasPermission, hasRole } from "/src/utils/permissions"; + +/** + * Hook for checking user permissions and roles + * Integrates with the existing CIPP authentication system + */ +export const usePermissions = () => { + const currentRole = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + }); + + const userRoles = currentRole.data?.clientPrincipal?.userRoles || []; + const userPermissions = currentRole.data?.permissions || []; + const isLoading = currentRole.isLoading; + const isAuthenticated = currentRole.isSuccess && userRoles.length > 0; + + /** + * Check if user has specific permissions + * @param {string[]} requiredPermissions - Array of required permissions (supports wildcards) + * @returns {boolean} - True if user has at least one of the required permissions + */ + const checkPermissions = useCallback( + (requiredPermissions) => { + if (!isAuthenticated) return false; + return hasPermission(userPermissions, requiredPermissions); + }, + [userPermissions, isAuthenticated] + ); + + /** + * Check if user has specific roles + * @param {string[]} requiredRoles - Array of required roles + * @returns {boolean} - True if user has at least one of the required roles + */ + const checkRoles = useCallback( + (requiredRoles) => { + if (!isAuthenticated) return false; + return hasRole(userRoles, requiredRoles); + }, + [userRoles, isAuthenticated] + ); + + /** + * Check if user has access based on both permissions and roles + * @param {Object} config - Configuration object + * @param {string[]} config.requiredPermissions - Array of required permissions + * @param {string[]} config.requiredRoles - Array of required roles + * @returns {boolean} - True if user has access + */ + const checkAccess = useCallback( + (config = {}) => { + if (!isAuthenticated) return false; + + const { requiredPermissions = [], requiredRoles = [] } = config; + + return hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + }, + [userPermissions, userRoles, isAuthenticated] + ); + + return { + userPermissions, + userRoles, + isLoading, + isAuthenticated, + checkPermissions, + checkRoles, + checkAccess, + }; +}; + +/** + * Hook specifically for checking permissions with a simpler API + * @param {string[]} requiredPermissions - Array of required permissions + * @param {string[]} requiredRoles - Array of required roles + * @returns {Object} - Object containing hasAccess boolean and loading state + */ +export const useHasPermission = (requiredPermissions = [], requiredRoles = []) => { + const { checkAccess, isLoading, isAuthenticated } = usePermissions(); + + const hasAccess = checkAccess({ requiredPermissions, requiredRoles }); + + return { + hasAccess, + isLoading, + isAuthenticated, + }; +}; diff --git a/src/layouts/config.js b/src/layouts/config.js index 3e00ca524df3..f0991ef3fff5 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -17,6 +17,7 @@ export const nativeMenuItems = [ ), + permissions: ["CIPP.Core.*"], }, { title: "Identity Management", @@ -26,40 +27,96 @@ export const nativeMenuItems = [ ), + permissions: ["Identity.*"], items: [ { title: "Administration", path: "/identity/administration", + permissions: ["Identity.User.*"], items: [ - { title: "Users", path: "/identity/administration/users" }, - { title: "Risky Users", path: "/identity/administration/risky-users" }, - { title: "Groups", path: "/identity/administration/groups" }, + { + title: "Users", + path: "/identity/administration/users", + permissions: ["Identity.User.*"], + }, + { + title: "Risky Users", + path: "/identity/administration/risky-users", + permissions: ["Identity.User.*"], + }, + { + title: "Groups", + path: "/identity/administration/groups", + permissions: ["Identity.Group.*"], + }, { title: "Group Templates", path: "/identity/administration/group-templates", + permissions: ["Identity.Group.*"], + }, + { + title: "Devices", + path: "/identity/administration/devices", + permissions: ["Identity.Device.*"], + }, + { + title: "Deleted Items", + path: "/identity/administration/deleted-items", + permissions: ["Identity.User.*"], + }, + { + title: "Roles", + path: "/identity/administration/roles", + permissions: ["Identity.Role.*"], + }, + { + title: "JIT Admin", + path: "/identity/administration/jit-admin", + permissions: ["Identity.Role.*"], }, - { title: "Devices", path: "/identity/administration/devices" }, - { title: "Deleted Items", path: "/identity/administration/deleted-items" }, - { title: "Roles", path: "/identity/administration/roles" }, - { title: "JIT Admin", path: "/identity/administration/jit-admin" }, { title: "Offboarding Wizard", path: "/identity/administration/offboarding-wizard", + permissions: ["Identity.User.*"], }, ], }, { title: "Reports", path: "/identity/reports", + permissions: [ + "Identity.User.*", + "Identity.Group.*", + "Identity.Device.*", + "Identity.Role.*", + "Identity.AuditLog.*", + ], items: [ - { title: "MFA Report", path: "/identity/reports/mfa-report" }, - { title: "Inactive Users", path: "/identity/reports/inactive-users-report" }, - { title: "Sign-in Report", path: "/identity/reports/signin-report" }, + { + title: "MFA Report", + path: "/identity/reports/mfa-report", + permissions: ["Identity.User.*"], + }, + { + title: "Inactive Users", + path: "/identity/reports/inactive-users-report", + permissions: ["Identity.User.*"], + }, + { + title: "Sign-in Report", + path: "/identity/reports/signin-report", + permissions: ["Identity.User.*"], + }, { title: "AAD Connect Report", path: "/identity/reports/azure-ad-connect-report", + permissions: ["Identity.User.*"], + }, + { + title: "Risk Detections", + path: "/identity/reports/risk-detections", + permissions: ["Identity.User.*"], }, - { title: "Risk Detections", path: "/identity/reports/risk-detections" }, ], }, ], @@ -72,94 +129,155 @@ export const nativeMenuItems = [ ), + permissions: ["Tenant.*", "Identity.AuditLog.*", "CIPP.Backup.*", "Scheduler.Billing.*"], items: [ { title: "Administration", path: "/tenant/administration", + permissions: ["Tenant.Administration.*"], items: [ - { title: "Tenants", path: "/tenant/administration/tenants" }, + { + title: "Tenants", + path: "/tenant/administration/tenants", + permissions: ["Tenant.Administration.*"], + }, { title: "Alert Configuration", path: "/tenant/administration/alert-configuration", + permissions: ["Tenant.Alert.*"], + }, + { + title: "Audit Logs", + path: "/tenant/administration/audit-logs", + permissions: ["Identity.AuditLog.*"], }, - { title: "Audit Logs", path: "/tenant/administration/audit-logs" }, { title: "Applications", path: "/tenant/administration/applications/enterprise-apps", + permissions: ["Tenant.Application.*"], + }, + { + title: "Secure Score", + path: "/tenant/administration/securescore", + permissions: ["Tenant.Administration.*"], }, - { title: "Secure Score", path: "/tenant/administration/securescore" }, { title: "App Consent Requests", path: "/tenant/administration/app-consent-requests", + permissions: ["Tenant.Application.*"], }, { title: "Authentication Methods", path: "/tenant/administration/authentication-methods", + permissions: ["Tenant.Config.*"], }, { title: "Partner Relationships", path: "/tenant/administration/partner-relationships", + permissions: ["Tenant.Relationship.*"], }, ], }, { title: "GDAP Management", path: "/tenant/gdap-management/", + permissions: ["Tenant.Relationship.*"], }, { title: "Configuration Backup", path: "/tenant/backup", - items: [{ title: "Backups", path: "/tenant/backup/backup-wizard" }], + permissions: ["CIPP.Backup.*"], + items: [ + { + title: "Backups", + path: "/tenant/backup/backup-wizard", + permissions: ["CIPP.Backup.*"], + }, + ], }, { title: "Standards", path: "/tenant/standards", + permissions: [ + "Tenant.Standards.*", + "Tenant.BestPracticeAnalyser.*", + "Tenant.DomainAnalyser.*", + ], items: [ - { title: "Standards", path: "/tenant/standards/list-standards" }, + { + title: "Standard Templates", + path: "/tenant/standards/list-standards", + permissions: ["Tenant.Standards.*"], + }, + { + title: "Tenant Alignment", + path: "/tenant/standards/tenant-alignment", + permissions: ["Tenant.Standards.*"], + }, { title: "Best Practice Analyser", path: "/tenant/standards/bpa-report", + permissions: ["Tenant.BestPracticeAnalyser.*"], }, { title: "Domains Analyser", path: "/tenant/standards/domains-analyser", + permissions: ["Tenant.DomainAnalyser.*"], }, ], }, { title: "Conditional Access", path: "/tenant/conditional", + permissions: ["Tenant.ConditionalAccess.*"], items: [ - { title: "CA Policies", path: "/tenant/conditional/list-policies" }, + { + title: "CA Policies", + path: "/tenant/conditional/list-policies", + permissions: ["Tenant.ConditionalAccess.*"], + }, { title: "CA Vacation Mode", path: "/tenant/conditional/deploy-vacation", + permissions: ["Tenant.ConditionalAccess.*"], }, { title: "CA Templates", path: "/tenant/conditional/list-template", + permissions: ["Tenant.ConditionalAccess.*"], }, { title: "Named Locations", path: "/tenant/conditional/list-named-locations", + permissions: ["Tenant.ConditionalAccess.*"], }, ], }, { title: "Reports", path: "/tenant/reports", + permissions: [ + "Tenant.Administration.*", + "Scheduler.Billing.*", + "Tenant.Application.*", + ], items: [ { title: "Licence Report", path: "/tenant/reports/list-licenses", + permissions: ["Tenant.Administration.*"], }, { title: "Sherweb Licence Report", path: "/tenant/reports/list-csp-licenses", + permissions: [ + "Tenant.Directory.*" + ], }, { title: "Consented Applications", path: "/tenant/reports/application-consent", + permissions: ["Tenant.Application.*"], }, ], }, @@ -173,46 +291,79 @@ export const nativeMenuItems = [ ), + permissions: [ + "Security.Incident.*", + "Security.Alert.*", + "Tenant.DeviceCompliance.*", + "Security.SafeLinksPolicy.*", + ], items: [ { title: "Incidents & Alerts", path: "/security/incidents", + permissions: ["Security.Incident.*"], items: [ - { title: "Incidents", path: "/security/incidents/list-incidents" }, - { title: "Alerts", path: "/security/incidents/list-alerts" }, + { + title: "Incidents", + path: "/security/incidents/list-incidents", + permissions: ["Security.Incident.*"], + }, + { + title: "Alerts", + path: "/security/incidents/list-alerts", + permissions: ["Security.Alert.*"], + }, ], }, { title: "Defender", path: "/security/defender", + permissions: ["Security.Alert.*"], items: [ - { title: "Defender Status", path: "/security/defender/list-defender" }, + { + title: "Defender Status", + path: "/security/defender/list-defender", + permissions: ["Security.Alert.*"], + }, { title: "Defender Deployment", path: "/security/defender/deployment", + permissions: ["Security.Alert.*"], }, { title: "Vulnerabilities", path: "/security/defender/list-defender-tvm", + permissions: ["Security.Alert.*"], }, ], }, { title: "Reports", path: "/security/reports", + permissions: ["Tenant.DeviceCompliance.*"], items: [ { title: "Device Compliance", path: "/security/reports/list-device-compliance", + permissions: ["Tenant.DeviceCompliance.*"], }, ], }, { title: "Safe Links", path: "/security/safelinks", + permissions: ["Security.SafeLinksPolicy.*"], items: [ - { title: "Safe Links Policies", path: "/security/safelinks/safelinks" }, - { title: "Safe Links Templates", path: "/security/safelinks/safelinks-template" }, + { + title: "Safe Links Policies", + path: "/security/safelinks/safelinks", + permissions: ["Security.SafeLinksPolicy.*"], + }, + { + title: "Safe Links Templates", + path: "/security/safelinks/safelinks-template", + permissions: ["Security.SafeLinksPolicy.*"], + }, ], }, ], @@ -225,46 +376,128 @@ export const nativeMenuItems = [ ), + permissions: [ + "Endpoint.Application.*", + "Endpoint.Autopilot.*", + "Endpoint.MEM.*", + "Endpoint.Device.*", + "Endpoint.Device.Read", + ], items: [ { title: "Applications", path: "/endpoint/applications", + permissions: ["Endpoint.Application.*"], items: [ - { title: "Applications", path: "/endpoint/applications/list" }, - { title: "Application Queue", path: "/endpoint/applications/queue" }, + { + title: "Applications", + path: "/endpoint/applications/list", + permissions: ["Endpoint.Application.*"], + }, + { + title: "Application Queue", + path: "/endpoint/applications/queue", + permissions: ["Endpoint.Application.*"], + }, ], }, { title: "Autopilot", path: "/endpoint/autopilot", + permissions: ["Endpoint.Autopilot.*"], items: [ - { title: "Autopilot Devices", path: "/endpoint/autopilot/list-devices" }, - { title: "Add Autopilot Device", path: "/endpoint/autopilot/add-device" }, - { title: "Profiles", path: "/endpoint/autopilot/list-profiles" }, - { title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" }, - { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" }, + { + title: "Autopilot Devices", + path: "/endpoint/autopilot/list-devices", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Add Autopilot Device", + path: "/endpoint/autopilot/add-device", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Profiles", + path: "/endpoint/autopilot/list-profiles", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Status Pages", + path: "/endpoint/autopilot/list-status-pages", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Add Status Page", + path: "/endpoint/autopilot/add-status-page", + permissions: ["Endpoint.Autopilot.*"], + }, ], }, { title: "Device Management", path: "/endpoint/MEM", + permissions: ["Endpoint.MEM.*"], items: [ - { title: "Devices", path: "/endpoint/MEM/devices" }, - { title: "Configuration Policies", path: "/endpoint/MEM/list-policies" }, - { title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" }, - { title: "Protection Policies", path: "/endpoint/MEM/list-appprotection-policies" }, - { title: "Apply Policy", path: "/endpoint/MEM/add-policy" }, - { title: "Policy Templates", path: "/endpoint/MEM/list-templates" }, - { title: "Scripts", path: "/endpoint/MEM/list-scripts" }, + { + title: "Devices", + path: "/endpoint/MEM/devices", + permissions: ["Endpoint.Device.*"], + }, + { + title: "Configuration Policies", + path: "/endpoint/MEM/list-policies", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Compliance Policies", + path: "/endpoint/MEM/list-compliance-policies", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Protection Policies", + path: "/endpoint/MEM/list-appprotection-policies", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Apply Policy", + path: "/endpoint/MEM/add-policy", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Policy Templates", + path: "/endpoint/MEM/list-templates", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Scripts", + path: "/endpoint/MEM/list-scripts", + permissions: ["Endpoint.MEM.*"], + }, ], }, { title: "Reports", path: "/endpoint/reports", + permissions: [ + "Endpoint.Device.*", + "Endpoint.Autopilot.*", + ], items: [ - { title: "Analytics Device Score", path: "/endpoint/reports/analyticsdevicescore" }, - { title: "Work from anywhere", path: "/endpoint/reports/workfromanywhere" }, - { title: "Autopilot Deployments", path: "/endpoint/reports/autopilot-deployment" }, + { + title: "Analytics Device Score", + path: "/endpoint/reports/analyticsdevicescore", + permissions: ["Endpoint.Device.*"], + }, + { + title: "Work from anywhere", + path: "/endpoint/reports/workfromanywhere", + permissions: ["Endpoint.Device.*"], + }, + { + title: "Autopilot Deployments", + path: "/endpoint/reports/autopilot-deployment", + permissions: ["Endpoint.Autopilot.*"], + }, ], }, ], @@ -277,22 +510,44 @@ export const nativeMenuItems = [ ), + permissions: [ + "Sharepoint.Site.*", + "Sharepoint.Admin.*", + "Teams.Group.*", + "Teams.Activity.*", + "Teams.Voice.*", + ], items: [ { title: "OneDrive", path: "/teams-share/onedrive", + permissions: ["Sharepoint.Site.*"], }, { title: "SharePoint", path: "/teams-share/sharepoint", + permissions: ["Sharepoint.Admin.*"], }, { title: "Teams", path: "/teams-share/teams", + permissions: ["Teams.Group.*"], items: [ - { title: "Teams", path: "/teams-share/teams/list-team" }, - { title: "Teams Activity", path: "/teams-share/teams/teams-activity" }, - { title: "Business Voice", path: "/teams-share/teams/business-voice" }, + { + title: "Teams", + path: "/teams-share/teams/list-team", + permissions: ["Teams.Group.*"], + }, + { + title: "Teams Activity", + path: "/teams-share/teams/teams-activity", + permissions: ["Teams.Activity.*"], + }, + { + title: "Business Voice", + path: "/teams-share/teams/business-voice", + permissions: ["Teams.Voice.*"], + }, ], }, ], @@ -305,97 +560,191 @@ export const nativeMenuItems = [ ), + permissions: [ + "Exchange.Mailbox.*", + "Exchange.Contact.*", + "Exchange.SpamFilter.*", + "Exchange.TransportRule.*", + "Exchange.Connector.*", + "Exchange.ConnectionFilter.*", + "Exchange.Equipment.*", + "Exchange.Room.*", + "Exchange.SafeLinks.*", + "Exchange.Group.*", + ], items: [ { title: "Administration", path: "/email/administration", + permissions: ["Exchange.Mailbox.*"], items: [ - { title: "Mailboxes", path: "/email/administration/mailboxes" }, - { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" }, - { title: "Mailbox Rules", path: "/email/administration/mailbox-rules" }, - { title: "Contacts", path: "/email/administration/contacts" }, - { title: "Contact Templates", path: "/email/administration/contacts-template" }, - { title: "Quarantine", path: "/email/administration/quarantine" }, + { + title: "Mailboxes", + path: "/email/administration/mailboxes", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Deleted Mailboxes", + path: "/email/administration/deleted-mailboxes", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Mailbox Rules", + path: "/email/administration/mailbox-rules", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Contacts", + path: "/email/administration/contacts", + permissions: ["Exchange.Contact.*"], + }, + { + title: "Contact Templates", + path: "/email/administration/contacts-template", + permissions: ["Exchange.Contact.*"], + }, + { + title: "Quarantine", + path: "/email/administration/quarantine", + permissions: ["Exchange.SpamFilter.*"], + }, { title: "Tenant Allow/Block Lists", path: "/email/administration/tenant-allow-block-lists", + permissions: ["Exchange.SpamFilter.*"], }, ], }, { title: "Transport", path: "/email/transport", + permissions: ["Exchange.TransportRule.*"], items: [ - { title: "Transport rules", path: "/email/transport/list-rules" }, + { + title: "Transport rules", + path: "/email/transport/list-rules", + permissions: ["Exchange.TransportRule.*"], + }, { title: "Transport Templates", path: "/email/transport/list-templates", + permissions: ["Exchange.TransportRule.*"], + }, + { + title: "Connectors", + path: "/email/transport/list-connectors", + permissions: ["Exchange.Connector.*"], }, - { title: "Connectors", path: "/email/transport/list-connectors" }, { title: "Connector Templates", path: "/email/transport/list-connector-templates", + permissions: ["Exchange.Connector.*"], }, ], }, { title: "Spamfilter", path: "/email/spamfilter", + permissions: ["Exchange.SpamFilter.*"], items: [ - { title: "Spamfilter", path: "/email/spamfilter/list-spamfilter" }, - { title: "Spamfilter templates", path: "/email/spamfilter/list-templates" }, - { title: "Connection filter", path: "/email/spamfilter/list-connectionfilter" }, + { + title: "Spamfilter", + path: "/email/spamfilter/list-spamfilter", + permissions: ["Exchange.SpamFilter.*"], + }, + { + title: "Spamfilter templates", + path: "/email/spamfilter/list-templates", + permissions: ["Exchange.SpamFilter.*"], + }, + { + title: "Connection filter", + path: "/email/spamfilter/list-connectionfilter", + permissions: ["Exchange.ConnectionFilter.*"], + }, { title: "Connection filter templates", path: "/email/spamfilter/list-connectionfilter-templates", + permissions: ["Exchange.ConnectionFilter.*"], }, { title: "Quarantine Policies", path: "/email/spamfilter/list-quarantine-policies", + permissions: ["Exchange.SpamFilter.*"], }, ], }, { title: "Resource Management", path: "/email/resources/management", + permissions: ["Exchange.Equipment.*"], items: [ - { title: "Equipment", path: "/email/resources/management/equipment" }, - { title: "Rooms", path: "/email/resources/management/list-rooms" }, - { title: "Room Lists", path: "/email/resources/management/room-lists" }, + { + title: "Equipment", + path: "/email/resources/management/equipment", + permissions: ["Exchange.Equipment.*"], + }, + { + title: "Rooms", + path: "/email/resources/management/list-rooms", + permissions: ["Exchange.Room.*"], + }, + { + title: "Room Lists", + path: "/email/resources/management/room-lists", + permissions: ["Exchange.Room.*"], + }, ], }, { title: "Reports", path: "/email/reports", + permissions: [ + "Exchange.Mailbox.*", + "Exchange.SpamFilter.*", + "Exchange.SafeLinks.*", + "Exchange.Group.*", + ], items: [ { title: "Mailbox Statistics", path: "/email/reports/mailbox-statistics", + permissions: ["Exchange.Mailbox.*"], }, { title: "Mailbox Client Access Settings", path: "/email/reports/mailbox-cas-settings", + permissions: ["Exchange.Mailbox.*"], }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters", + permissions: ["Exchange.SpamFilter.*"], + }, + { + title: "Malware Filters", + path: "/email/reports/malware-filters", + permissions: ["Exchange.SpamFilter.*"], }, - { title: "Malware Filters", path: "/email/reports/malware-filters" }, { title: "Safe Links Filters", path: "/email/reports/safelinks-filters", + permissions: ["Exchange.SafeLinks.*"], }, { title: "Safe Attachments Filters", path: "/email/reports/safeattachments-filters", + permissions: ["Exchange.SafeLinks.*"], }, { title: "Shared Mailbox with Enabled Account", path: "/email/reports/SharedMailboxEnabledAccount", + permissions: ["Exchange.Mailbox.*"], }, { title: "Global Address List", path: "/email/reports/global-address-list", + permissions: ["Exchange.Group.*"], }, ], }, @@ -409,60 +758,104 @@ export const nativeMenuItems = [ ), + permissions: [ + "CIPP.*", + "Tenant.Administration.*", + "Tenant.Application.*", + "Tenant.DomainAnalyser.*", + "Exchange.Mailbox.*", + ], items: [ { title: "Tenant Tools", path: "/tenant/tools", + permissions: ["Tenant.Administration.*"], items: [ { title: "Graph Explorer", path: "/tenant/tools/graph-explorer", + permissions: ["Tenant.Administration.*"], }, { title: "Application Approval", path: "/tenant/tools/appapproval", + permissions: ["Tenant.Application.*"], + }, + { + title: "Tenant Lookup", + path: "/tenant/tools/tenantlookup", + permissions: ["Tenant.Administration.*"], }, - { title: "Tenant Lookup", path: "/tenant/tools/tenantlookup" }, - { title: "IP Database", path: "/tenant/tools/geoiplookup" }, + { + title: "IP Database", + path: "/tenant/tools/geoiplookup", + permissions: ["CIPP.Core.*"], + }, { title: "Individual Domain Check", path: "/tenant/tools/individual-domains", + permissions: ["Tenant.DomainAnalyser.*"], }, ], }, { title: "Email Tools", path: "/email/tools", + permissions: ["Exchange.Mailbox.*"], items: [ - { title: "Message Trace", path: "/email/tools/message-trace" }, - { title: "Mailbox Restores", path: "/email/tools/mailbox-restores" }, - { title: "Message Viewer", path: "/email/tools/message-viewer" }, + { + title: "Message Trace", + path: "/email/tools/message-trace", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Mailbox Restores", + path: "/email/tools/mailbox-restores", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Message Viewer", + path: "/email/tools/message-viewer", + permissions: ["Exchange.Mailbox.*"], + }, ], }, { title: "Dark Web Tools", path: "/tools/darkweb", + permissions: ["CIPP.Core.*"], items: [ - { title: "Tenant Breach Lookup", path: "/tools/tenantbreachlookup" }, - { title: "Breach Lookup", path: "/tools/breachlookup" }, + { + title: "Tenant Breach Lookup", + path: "/tools/tenantbreachlookup", + permissions: ["CIPP.Core.*"], + }, + { + title: "Breach Lookup", + path: "/tools/breachlookup", + permissions: ["CIPP.Core.*"], + }, ], }, { title: "Template Library", path: "/tools/templatelib", roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Core.*"], }, { title: "Community Repositories", path: "/tools/community-repos", roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Core.*"], }, { title: "Scheduler", path: "/cipp/scheduler", roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Scheduler.*"], }, ], }, @@ -474,35 +867,68 @@ export const nativeMenuItems = [ ), + permissions: [ + "CIPP.*", // Pattern matching - matches any CIPP permission + ], items: [ - { title: "Application Settings", path: "/cipp/settings", roles: ["admin", "superadmin"] }, - { title: "Logbook", path: "/cipp/logs", roles: ["editor", "admin", "superadmin"] }, - { title: "Setup Wizard", path: "/onboardingv2", roles: ["admin", "superadmin"] }, - { title: "Integrations", path: "/cipp/integrations", roles: ["admin", "superadmin"] }, + { + title: "Application Settings", + path: "/cipp/settings", + roles: ["admin", "superadmin"], + permissions: ["CIPP.AppSettings.*"], + }, + { + title: "Logbook", + path: "/cipp/logs", + roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Core.*"], + }, + { + title: "Setup Wizard", + path: "/onboardingv2", + roles: ["admin", "superadmin"], + permissions: ["CIPP.Core.*"], + }, + { + title: "Integrations", + path: "/cipp/integrations", + roles: ["admin", "superadmin"], + permissions: ["CIPP.Extension.*"], + }, { title: "Custom Data", path: "/cipp/custom-data/directory-extensions", roles: ["admin", "superadmin"], + permissions: ["CIPP.Core.*"], }, { title: "Advanced", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], items: [ - { title: "Super Admin", path: "/cipp/super-admin/tenant-mode", roles: ["superadmin"] }, + { + title: "Super Admin", + path: "/cipp/super-admin/tenant-mode", + roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], + }, { title: "Exchange Cmdlets", path: "/cipp/advanced/exchange-cmdlets", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], }, { title: "Timers", path: "/cipp/advanced/timers", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], }, { title: "Table Maintenance", path: "/cipp/advanced/table-maintenance", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], }, ], }, diff --git a/src/layouts/index.js b/src/layouts/index.js index b99f427af9b5..add691f24bf0 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; import { usePathname } from "next/navigation"; import { Alert, Button, Dialog, DialogContent, DialogTitle, useMediaQuery } from "@mui/material"; import { styled } from "@mui/material/styles"; @@ -75,11 +75,8 @@ export const Layout = (props) => { const [userSettingsComplete, setUserSettingsComplete] = useState(false); const [fetchingVisible, setFetchingVisible] = useState([]); const [menuItems, setMenuItems] = useState(nativeMenuItems); + const lastUserSettingsUpdate = useRef(null); const currentTenant = settings?.currentTenant; - const currentRole = ApiGetCall({ - url: "/api/me", - queryKey: "authmecipp", - }); const [hideSidebar, setHideSidebar] = useState(false); const swaStatus = ApiGetCall({ @@ -89,9 +86,16 @@ export const Layout = (props) => { refetchOnWindowFocus: true, }); + const currentRole = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + waiting: !swaStatus.isSuccess || swaStatus.data?.clientPrincipal === null, + }); + useEffect(() => { if (currentRole.isSuccess && !currentRole.isFetching) { const userRoles = currentRole.data?.clientPrincipal?.userRoles; + const userPermissions = currentRole.data?.permissions; if (!userRoles) { setMenuItems([]); setHideSidebar(true); @@ -100,12 +104,41 @@ 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) => { + return item.permissions.some((requiredPerm) => { + // Exact match + if (userPerm === requiredPerm) { + return true; + } + + // Pattern matching - check if required permission contains wildcards + if (requiredPerm.includes("*")) { + // Convert wildcard pattern to regex + const regexPattern = requiredPerm + .replace(/\./g, "\\.") // Escape dots + .replace(/\*/g, ".*"); // Convert * to .* + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(userPerm); + } + + return false; + }); + }); + if (!hasPermission) { + return null; + } + } + // check sub-items if (item.items && item.items.length > 0) { const filteredSubItems = filterItemsByRole(item.items).filter(Boolean); return { ...item, items: filteredSubItems }; @@ -142,33 +175,41 @@ export const Layout = (props) => { }); useEffect(() => { - if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching && !userSettingsComplete) { - console.log("User Settings API Data:", userSettingsAPI.data); - //if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key. - if (userSettingsAPI.data.offboardingDefaults?.user) { - delete userSettingsAPI.data.offboardingDefaults.user; - } - if (userSettingsAPI?.data?.currentTheme) { - delete userSettingsAPI.data.currentTheme; - } - // get current devtools settings - var showDevtools = settings.showDevtools; - // get current bookmarks - var bookmarks = settings.bookmarks; + if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching) { + // Only update if the data has actually changed (using dataUpdatedAt as a proxy) + const dataUpdatedAt = userSettingsAPI.dataUpdatedAt; + if (dataUpdatedAt && dataUpdatedAt !== lastUserSettingsUpdate.current) { + //if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key. + if (userSettingsAPI.data.offboardingDefaults?.user) { + delete userSettingsAPI.data.offboardingDefaults.user; + } + if (userSettingsAPI.data.offboardingDefaults?.keepCopy) { + delete userSettingsAPI.data.offboardingDefaults.keepCopy; + } + if (userSettingsAPI?.data?.currentTheme) { + delete userSettingsAPI.data.currentTheme; + } + // get current devtools settings + var showDevtools = settings.showDevtools; + // get current bookmarks + var bookmarks = settings.bookmarks; - settings.handleUpdate({ - ...userSettingsAPI.data, - bookmarks, - showDevtools, - }); - setUserSettingsComplete(true); + settings.handleUpdate({ + ...userSettingsAPI.data, + bookmarks, + showDevtools, + }); + + // Track this update and set completion status + lastUserSettingsUpdate.current = dataUpdatedAt; + setUserSettingsComplete(true); + } } }, [ userSettingsAPI.isSuccess, userSettingsAPI.data, userSettingsAPI.isFetching, - userSettingsComplete, - settings, + userSettingsAPI.dataUpdatedAt, ]); const version = ApiGetCall({ diff --git a/src/pages/_app.js b/src/pages/_app.js index 6a74f65f2e5d..0b47acc52be7 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -239,8 +239,8 @@ const App = (props) => { id: "documentation", icon: , name: "Check the Documentation", - href: `https://docs.cipp.app/user-documentation/${pathname}`, - onClick: () => window.open(`https://docs.cipp.app/user-documentation/${pathname}`, "_blank"), + href: `https://docs.cipp.app/user-documentation${pathname}`, + onClick: () => window.open(`https://docs.cipp.app/user-documentation${pathname}`, "_blank"), }, ]; diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js index c3506228a6d6..38f6fe99bd27 100644 --- a/src/pages/cipp/preferences.js +++ b/src/pages/cipp/preferences.js @@ -14,7 +14,14 @@ import { getCippFormatting } from "../../utils/get-cipp-formatting"; const Page = () => { const settings = useSettings(); - const formcontrol = useForm({ mode: "onChange", defaultValues: settings }); + const cleanedSettings = { ...settings }; + + if (cleanedSettings.offboardingDefaults?.keepCopy) { + delete cleanedSettings.offboardingDefaults.keepCopy; + settings.handleUpdate(cleanedSettings); + } + + const formcontrol = useForm({ mode: "onChange", defaultValues: cleanedSettings }); const auth = ApiGetCall({ url: "/api/me", @@ -32,8 +39,6 @@ const Page = () => { { value: "officeLocation", label: "officeLocation" }, { value: "otherMails", label: "otherMails" }, { value: "showInAddressList", label: "showInAddressList" }, - { value: "state", label: "state" }, - { value: "city", label: "city" }, { value: "sponsor", label: "sponsor" }, ]; @@ -261,6 +266,16 @@ const Page = () => { /> ), }, + { + label: "Clear Immutable ID", + value: ( + + ), + }, ]} /> diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 6bd64ff2d6fd..b7071474cad1 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -2,51 +2,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button } from "@mui/material"; import Link from "next/link"; -import { CalendarDaysIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; -import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; import ScheduledTaskDetails from "../../../components/CippComponents/ScheduledTaskDetails"; +import { CippScheduledTaskActions } from "../../../components/CippComponents/CippScheduledTaskActions"; const Page = () => { - const actions = [ - { - label: "View Task Details", - link: "/cipp/scheduler/task?id=[RowKey]", - icon: , - }, - { - label: "Run Now", - type: "POST", - url: "/api/AddScheduledItem", - data: { RowKey: "RowKey", RunNow: true }, - icon: , - confirmText: "Are you sure you want to run [Name]?", - allowResubmit: true, - }, - { - label: "Edit Job", - link: "/cipp/scheduler/job?id=[RowKey]", - multiPost: false, - icon: , - color: "success", - }, - { - label: "Clone and Edit Job", - link: "/cipp/scheduler/job?id=[RowKey]&Clone=True", - multiPost: false, - icon: , - color: "success", - }, - { - label: "Delete Job", - icon: , - type: "POST", - url: "/api/RemoveScheduledItem", - data: { id: "RowKey" }, - confirmText: "Are you sure you want to delete this job?", - multiPost: false, - }, - ]; + const actions = CippScheduledTaskActions(); const filterList = [ { @@ -72,7 +34,7 @@ const Page = () => { ]; const offCanvas = { - children: (extendedData) => , + children: (extendedData) => , size: "xl", actions: actions, }; diff --git a/src/pages/email/administration/contacts-template/edit.jsx b/src/pages/email/administration/contacts-template/edit.jsx index 7b7d9a5840fe..100eebe46731 100644 --- a/src/pages/email/administration/contacts-template/edit.jsx +++ b/src/pages/email/administration/contacts-template/edit.jsx @@ -2,43 +2,45 @@ import { useEffect, useMemo, useCallback } from "react"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import ContactFormLayout from "/src/components/CippFormPages/CippAddEditContact"; import { ApiGetCall } from "../../../../api/ApiCall"; import countryList from "/src/data/countryList.json"; import { useRouter } from "next/router"; -const countryLookup = new Map( - countryList.map(country => [country.Name, country.Code]) -); +const countryLookup = new Map(countryList.map((country) => [country.Name, country.Code])); const EditContactTemplate = () => { const router = useRouter(); const { id } = router.query; - + const contactTemplateInfo = ApiGetCall({ url: `/api/ListContactTemplates?id=${id}`, queryKey: `ListContactTemplates-${id}`, waiting: !!id, }); - const defaultFormValues = useMemo(() => ({ - displayName: "", - firstName: "", - lastName: "", - email: "", - hidefromGAL: false, - streetAddress: "", - postalCode: "", - city: "", - state: "", - country: "", - companyName: "", - mobilePhone: "", - businessPhone: "", - jobTitle: "", - website: "", - mailTip: "", - }), []); + const defaultFormValues = useMemo( + () => ({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }), + [] + ); const formControl = useForm({ mode: "onChange", @@ -52,12 +54,14 @@ const EditContactTemplate = () => { } // Handle both single object (when fetching by ID) and array responses - const contact = Array.isArray(contactTemplateInfo.data) ? contactTemplateInfo.data[0] : contactTemplateInfo.data; + const contact = Array.isArray(contactTemplateInfo.data) + ? contactTemplateInfo.data[0] + : contactTemplateInfo.data; const address = contact.addresses?.[0] || {}; const phones = contact.phones || []; - + // Use Map for O(1) phone lookup - const phoneMap = new Map(phones.map(p => [p.type, p.number])); + const phoneMap = new Map(phones.map((p) => [p.type, p.number])); return { ContactTemplateID: id || "", @@ -70,9 +74,7 @@ const EditContactTemplate = () => { postalCode: address.postalCode || "", city: address.city || "", state: address.state || "", - country: address.countryOrRegion - ? countryLookup.get(address.countryOrRegion) || "" - : "", + country: address.countryOrRegion ? countryLookup.get(address.countryOrRegion) || "" : "", companyName: contact.companyName || "", mobilePhone: phoneMap.get("mobile") || "", businessPhone: phoneMap.get("business") || "", @@ -114,9 +116,11 @@ const EditContactTemplate = () => { website: values.website, mailTip: values.mailTip, }; - },); - - const contactTemplate = Array.isArray(contactTemplateInfo.data) ? contactTemplateInfo.data[0] : contactTemplateInfo.data; + }); + + const contactTemplate = Array.isArray(contactTemplateInfo.data) + ? contactTemplateInfo.data[0] + : contactTemplateInfo.data; return ( { data={contactTemplate} customDataformatter={customDataFormatter} > - + {contactTemplateInfo.isLoading && } + {!contactTemplateInfo.isLoading && ( + + )} ); }; diff --git a/src/pages/email/administration/contacts/edit.jsx b/src/pages/email/administration/contacts/edit.jsx index bc8220ef7719..963b0be981d0 100644 --- a/src/pages/email/administration/contacts/edit.jsx +++ b/src/pages/email/administration/contacts/edit.jsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import { useSettings } from "../../../../hooks/use-settings"; import { ApiGetCall } from "../../../../api/ApiCall"; import countryList from "/src/data/countryList.json"; @@ -10,39 +11,40 @@ import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { Divider } from "@mui/material"; -const countryLookup = new Map( - countryList.map(country => [country.Name, country.Code]) -); +const countryLookup = new Map(countryList.map((country) => [country.Name, country.Code])); const EditContact = () => { const tenantDomain = useSettings().currentTenant; const router = useRouter(); const { id } = router.query; - + const contactInfo = ApiGetCall({ url: `/api/ListContacts?tenantFilter=${tenantDomain}&id=${id}`, queryKey: `ListContacts-${id}`, waiting: !!id, }); - const defaultFormValues = useMemo(() => ({ - displayName: "", - firstName: "", - lastName: "", - email: "", - hidefromGAL: false, - streetAddress: "", - postalCode: "", - city: "", - state: "", - country: "", - companyName: "", - mobilePhone: "", - businessPhone: "", - jobTitle: "", - website: "", - mailTip: "", - }), []); + const defaultFormValues = useMemo( + () => ({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }), + [] + ); const formControl = useForm({ mode: "onChange", @@ -58,9 +60,9 @@ const EditContact = () => { const contact = contactInfo.data; const address = contact.addresses?.[0] || {}; const phones = contact.phones || []; - + // Use Map for O(1) phone lookup - const phoneMap = new Map(phones.map(p => [p.type, p.number])); + const phoneMap = new Map(phones.map((p) => [p.type, p.number])); return { displayName: contact.displayName || "", @@ -72,9 +74,7 @@ const EditContact = () => { postalCode: address.postalCode || "", city: address.city || "", state: address.state || "", - country: address.countryOrRegion - ? countryLookup.get(address.countryOrRegion) || "" - : "", + country: address.countryOrRegion ? countryLookup.get(address.countryOrRegion) || "" : "", companyName: contact.companyName || "", mobilePhone: phoneMap.get("mobile") || "", businessPhone: phoneMap.get("business") || "", @@ -96,30 +96,33 @@ const EditContact = () => { }, [resetForm]); // Memoize custom data formatter - const customDataFormatter = useCallback((values) => { - const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; - return { - tenantID: tenantDomain, - ContactID: contact?.id, - DisplayName: values.displayName, - hidefromGAL: values.hidefromGAL, - email: values.email, - FirstName: values.firstName, - LastName: values.lastName, - Title: values.jobTitle, - StreetAddress: values.streetAddress, - PostalCode: values.postalCode, - City: values.city, - State: values.state, - CountryOrRegion: values.country?.value || values.country, - Company: values.companyName, - mobilePhone: values.mobilePhone, - phone: values.businessPhone, - website: values.website, - mailTip: values.mailTip, - }; - }, [tenantDomain, contactInfo.data]); - + const customDataFormatter = useCallback( + (values) => { + const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; + return { + tenantID: tenantDomain, + ContactID: contact?.id, + DisplayName: values.displayName, + hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, + }; + }, + [tenantDomain, contactInfo.data] + ); + const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; return ( @@ -133,142 +136,150 @@ const EditContact = () => { data={contact} customDataformatter={customDataFormatter} > - - {/* Display Name */} - - - + {contactInfo.isLoading && } + {!contactInfo.isLoading && ( + + {/* Display Name */} + + + - {/* First Name and Last Name */} - - - - - - + {/* First Name and Last Name */} + + + + + + - + - {/* Email */} - - - + {/* Email */} + + + - {/* Hide from GAL */} - - - + {/* Hide from GAL */} + + + - + - {/* Company Information */} - - - - - - + {/* Company Information */} + + + + + + - + - {/* Address Information */} - - - - - - - - - - - ({ - label: Name, - value: Code, - }))} - formControl={formControl} - /> - + {/* Address Information */} + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + - + - {/* Phone Numbers */} - - - - - + {/* Phone Numbers */} + + + + + + - + )} ); }; diff --git a/src/pages/email/resources/management/equipment/edit.jsx b/src/pages/email/resources/management/equipment/edit.jsx index 046aaa04cb07..79b55a4a33af 100644 --- a/src/pages/email/resources/management/equipment/edit.jsx +++ b/src/pages/email/resources/management/equipment/edit.jsx @@ -5,6 +5,7 @@ import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; @@ -151,282 +152,292 @@ const EditEquipmentMailbox = () => { workingHoursTimeZone: values.workingHoursTimeZone?.value || values.workingHoursTimeZone, })} > - - {/* Basic Information */} - - - Basic Information - + {equipmentInfo.isLoading && ( + + )} + {equipmentInfo.isSuccess && ( + + {/* Basic Information */} + + + Basic Information + + + + + + + + + + + + + + {/* Booking Information */} + + + Booking Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Working Hours */} + + + Working Hours + + + + + + + + + + + + + ({ + value: tz.standardTime, + label: `${tz.standardTime} - ${tz.timezone}`, + }))} + multiple={false} + creatable={false} + formControl={formControl} + /> + + + + + + + + + + + + + {/* Equipment & Location Details */} + + + Equipment & Location Details + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + + + + + - - - - - - - - - - - - {/* Booking Information */} - - - Booking Information - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Working Hours */} - - - Working Hours - - - - - - - - - - - - - ({ - value: tz.standardTime, - label: `${tz.standardTime} - ${tz.timezone}`, - }))} - multiple={false} - creatable={false} - formControl={formControl} - /> - - - - - - - - - - - - - {/* Equipment & Location Details */} - - - Equipment & Location Details - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ - label: Name, - value: Code, - }))} - formControl={formControl} - /> - - - - - - - - + )} ); }; diff --git a/src/pages/email/resources/management/list-rooms/edit.jsx b/src/pages/email/resources/management/list-rooms/edit.jsx index 35e3901a7e10..97cb3f35078e 100644 --- a/src/pages/email/resources/management/list-rooms/edit.jsx +++ b/src/pages/email/resources/management/list-rooms/edit.jsx @@ -5,6 +5,7 @@ import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; @@ -173,322 +174,332 @@ const EditRoomMailbox = () => { WorkingHoursTimeZone: values.WorkingHoursTimeZone?.value || values.WorkingHoursTimeZone, })} > - - {/* Basic Information */} - - Basic Information - + {roomInfo.isLoading && ( + + )} + {roomInfo.isSuccess && ( + + {/* Basic Information */} + + + Basic Information + + - - - - - - - - {/* Booking Settings */} - - Booking Settings - + + + + + + + + {/* Booking Settings */} + + + Booking Settings + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Working Hours */} - - Working Hours - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Working Hours */} + + + Working Hours + + - - - - - - + + + + + + - - ({ - value: tz.standardTime, - label: `${tz.standardTime} - ${tz.timezone}`, - }))} - multiple={false} - creatable={false} - formControl={formControl} - /> - - - - - - - - - {/* Room Facilities */} - - Room Facilities & Equipment - + + ({ + value: tz.standardTime, + label: `${tz.standardTime} - ${tz.timezone}`, + }))} + multiple={false} + creatable={false} + formControl={formControl} + /> + + + + + + + + + {/* Room Facilities */} + + + Room Facilities & Equipment + + - - - - - - - - - - - - - - - - - - - - {/* Location Information */} - - Location Information - + + + + + + + + + + + + + + + + + + + + {/* Location Information */} + + + Location Information + + - - - + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - ({ - label: Name, - value: Code, - }))} - formControl={formControl} - /> - - + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + )} ); }; diff --git a/src/pages/email/resources/management/room-lists/add.jsx b/src/pages/email/resources/management/room-lists/add.jsx new file mode 100644 index 000000000000..606259d728e0 --- /dev/null +++ b/src/pages/email/resources/management/room-lists/add.jsx @@ -0,0 +1,50 @@ +import { Box } from "@mui/material"; +import CippFormPage from "../../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../../hooks/use-settings"; +import CippAddRoomListForm from "../../../../../components/CippFormPages/CippAddRoomListForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const tenantDomain = userSettingsDefaults.currentTenant; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + username: "", + primDomain: null, + }, + }); + + return ( + <> + { + return { + tenantFilter: tenantDomain, + displayName: values.displayName?.trim(), + username: values.username?.trim(), + primDomain: values.primDomain, + }; + }} + > + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/email/resources/management/room-lists/edit.jsx b/src/pages/email/resources/management/room-lists/edit.jsx new file mode 100644 index 000000000000..b1d77015f017 --- /dev/null +++ b/src/pages/email/resources/management/room-lists/edit.jsx @@ -0,0 +1,345 @@ +import { useEffect, useState } from "react"; +import { Box, Button, Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../../api/ApiCall"; +import { useSettings } from "../../../../../hooks/use-settings"; +import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; + +const EditRoomList = () => { + const router = useRouter(); + const { groupId } = router.query; + const [groupIdReady, setGroupIdReady] = useState(false); + const [showMembershipTable, setShowMembershipTable] = useState(false); + const [combinedData, setCombinedData] = useState([]); + const [originalAllowExternal, setOriginalAllowExternal] = useState(null); + const tenantFilter = useSettings().currentTenant; + + const groupInfo = ApiGetCall({ + url: `/api/ListRoomLists?groupID=${groupId}&tenantFilter=${tenantFilter}&members=true&owners=true`, + queryKey: `ListRoomLists-${groupId}`, + waiting: groupIdReady, + }); + + useEffect(() => { + if (groupId) { + setGroupIdReady(true); + groupInfo.refetch(); + } + }, [router.query, groupId, tenantFilter]); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: tenantFilter, + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + }, + }); + + useEffect(() => { + if (groupInfo.isSuccess) { + const group = groupInfo.data?.groupInfo; + if (group) { + // Create combined data for the table + const owners = Array.isArray(groupInfo.data?.owners) ? groupInfo.data.owners : []; + const members = Array.isArray(groupInfo.data?.members) ? groupInfo.data.members : []; + + const combinedData = [ + ...owners.map((o) => ({ + type: "Owner", + userPrincipalName: o.userPrincipalName, + displayName: o.displayName, + })), + ...members.map((m) => ({ + type: "Room", + userPrincipalName: m.PrimarySmtpAddress || m.userPrincipalName || m.mail, + displayName: m.DisplayName || m.displayName, + })), + ]; + setCombinedData(combinedData); + + // Store original allowExternal value for comparison + const allowExternalValue = groupInfo?.data?.allowExternal; + setOriginalAllowExternal(allowExternalValue); + + // Reset the form with all values + formControl.reset({ + tenantFilter: tenantFilter, + mail: group.PrimarySmtpAddress || group.mail, + mailNickname: group.Alias || group.mailNickname || "", + allowExternal: allowExternalValue, + displayName: group.DisplayName || group.displayName, + description: group.Description || group.description || "", + groupId: group.Guid || group.id, + groupType: "Room List", + // Initialize empty arrays for add/remove actions + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + }); + } + } + }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]); + + return ( + <> + { + // Only include allowExternal if it has changed from the original value + const modifiedValues = { ...values }; + if (originalAllowExternal !== null && values.allowExternal === originalAllowExternal) { + delete modifiedValues.allowExternal; + } + return modifiedValues; + }} + titleButton={ + <> + + + } + > + {showMembershipTable ? ( + + + + ) : ( + + + + Room List Properties + + + + + + + + + + + + + + Add Members + + + + + `${room.displayName || "Unknown"} (${ + room.mail || room.userPrincipalName || "No email" + })`, + valueField: "mail", + addedField: { + roomType: "bookingType", + capacity: "capacity", + id: "id", + }, + queryKey: `rooms-${tenantFilter}`, + showRefresh: true, + dataFilter: (rooms) => { + // Get current member emails to filter out + const members = Array.isArray(groupInfo.data?.members) + ? groupInfo.data.members + : []; + const currentMemberEmails = members + .map((m) => m.mail || m.userPrincipalName) + .filter(Boolean); + + // Filter out rooms that are already members + // rooms here have been transformed to {label, value, addedFields} format + const filteredRooms = rooms.filter((room) => { + const roomEmail = room.value; // email is in the value field + const isAlreadyMember = currentMemberEmails.includes(roomEmail); + return !isAlreadyMember; + }); + + return filteredRooms; + }, + }} + /> + + + + + `${user.displayName || "Unknown"} (${ + user.userPrincipalName || user.mail || "No email" + })`, + valueField: "userPrincipalName", + addedField: { + id: "id", + }, + queryKey: `users-${tenantFilter}`, + showRefresh: true, + dataFilter: (users) => { + // Get current owner userPrincipalNames to filter out + const owners = Array.isArray(groupInfo.data?.owners) + ? groupInfo.data.owners + : []; + const currentOwnerEmails = owners + .map((o) => o.userPrincipalName) + .filter(Boolean); + + // Filter out users that are already owners + // users here have been transformed to {label, value, addedFields} format + const filteredUsers = users.filter((user) => { + const userEmail = user.value; // userPrincipalName is in the value field + const isAlreadyOwner = currentOwnerEmails.includes(userEmail); + return !isAlreadyOwner; + }); + + return filteredUsers; + }, + }} + /> + + + + + Remove Members + + + + ({ + label: `${m.DisplayName || m.displayName || "Unknown"} (${ + m.PrimarySmtpAddress || m.userPrincipalName || m.mail + })`, + value: m.PrimarySmtpAddress || m.userPrincipalName || m.mail, + addedFields: { id: m.ExternalDirectoryObjectId || m.id }, + })) + : [] + } + /> + + + + ({ + label: `${o.displayName} (${o.userPrincipalName})`, + value: o.userPrincipalName, + addedFields: { id: o.id }, + })) + : [] + } + /> + + + + + Room List Settings + + + + + + + + )} + + + ); +}; + +EditRoomList.getLayout = (page) => {page}; + +export default EditRoomList; diff --git a/src/pages/email/resources/management/room-lists/index.js b/src/pages/email/resources/management/room-lists/index.js index e73dcc50490e..b29198a98e9a 100644 --- a/src/pages/email/resources/management/room-lists/index.js +++ b/src/pages/email/resources/management/room-lists/index.js @@ -1,6 +1,9 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Visibility } from "@mui/icons-material"; +import { Visibility, ListAlt, Edit } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import Link from "next/link"; const Page = () => { const pageTitle = "Room Lists"; @@ -8,33 +11,46 @@ const Page = () => { const actions = [ { - label: "View included Rooms", - link: `/email/resources/management/room-lists/list/view?roomAddress=[emailAddress]`, - color: "info", - icon: , + label: "Edit Room List", + link: "/email/resources/management/room-lists/edit?groupId=[PrimarySmtpAddress]", + multiPost: false, + icon: , + color: "success", + }, + { + label: "Delete Room List", + type: "POST", + url: "/api/ExecGroupsDelete", + icon: , + data: { + id: "Guid", + displayName: "DisplayName", + GroupType: "!Distribution List", + }, + confirmText: "Are you sure you want to delete this room list?", + multiPost: false, }, ]; const offCanvas = { extendedInfoFields: [ - "id", - "emailAddress", - "displayName", - "phone", - "placeId", - "geoCoordinates", - "address.city", - "address.countryOrRegion", + "Guid", + "PrimarySmtpAddress", + "DisplayName", + "Phone", + "Identity", + "Notes", + "MailNickname", ], actions: actions, }; const simpleColumns = [ - "displayName", - "geoCoordinates", - "placeId", - "address.city", - "address.countryOrRegion", + "DisplayName", + "PrimarySmtpAddress", + "Identity", + "Phone", + "Notes", ]; return ( @@ -42,9 +58,20 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} actions={actions} - apiDataKey="ListRoomListsResults" + apiDataKey="Results" offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + <> + + + } /> ); }; diff --git a/src/pages/email/resources/management/room-lists/list/view.jsx b/src/pages/email/resources/management/room-lists/list/view.jsx deleted file mode 100644 index 64bd4a5f07ce..000000000000 --- a/src/pages/email/resources/management/room-lists/list/view.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useRouter } from "next/router"; -import { useSettings } from "/src/hooks/use-settings"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { roomAddress } = router.query; - const pageTitle = `Rooms included in ${roomAddress}`; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/spamfilter/list-quarantine-policies/add.jsx b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx index 3af31bce1e9e..bd2e36fc3764 100644 --- a/src/pages/email/spamfilter/list-quarantine-policies/add.jsx +++ b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx @@ -1,5 +1,6 @@ -import React, { useEffect } from "react"; -import { Grid, Divider } from "@mui/material"; +import { useEffect } from "react"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx index a5fecd62a878..d55737b0f3a0 100644 --- a/src/pages/endpoint/MEM/list-scripts/index.jsx +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -12,7 +12,7 @@ import { DialogActions, } from "@mui/material"; import { CippCodeBlock } from "/src/components/CippComponents/CippCodeBlock"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useDispatch } from "react-redux"; import { Close, Save } from "@mui/icons-material"; import { useSettings } from "../../../../hooks/use-settings"; @@ -31,6 +31,11 @@ const Page = () => { const dispatch = useDispatch(); + const language = useMemo(() => { + return currentScript?.scriptType?.toLowerCase() === ("macos" || "linux") ? "shell" : "powershell"; + }, [currentScript?.scriptType]); + + const tenantFilter = useSettings().currentTenant; const { isLoading: scriptIsLoading, @@ -240,7 +245,7 @@ const Page = () => { type="editor" code={codeContent} onChange={codeChange} - language="powershell" + language={language} /> )} diff --git a/src/pages/endpoint/MEM/list-templates/edit.jsx b/src/pages/endpoint/MEM/list-templates/edit.jsx new file mode 100644 index 000000000000..71eb1bc6c00c --- /dev/null +++ b/src/pages/endpoint/MEM/list-templates/edit.jsx @@ -0,0 +1,127 @@ +import { Alert, Box } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import { ApiGetCall } from "/src/api/ApiCall"; +import CippTemplateFieldRenderer from "/src/components/CippComponents/CippTemplateFieldRenderer"; + +const EditIntuneTemplate = () => { + const router = useRouter(); + const { id } = router.query; + const formControl = useForm({ mode: "onChange" }); + + const templateQuery = ApiGetCall({ + url: `/api/ListIntuneTemplates?id=${id}`, + queryKey: `IntuneTemplate-${id}`, + enabled: !!id, + }); + + const templateData = Array.isArray(templateQuery.data) + ? templateQuery.data.find((t) => t.id === id) + : templateQuery.data; + + // Custom data formatter to convert autoComplete objects to values + const customDataFormatter = (values) => { + // Recursively extract values from autoComplete objects and fix @odata issues + const extractValues = (obj) => { + if (!obj) return obj; + + // If this is an autoComplete object with label/value, return just the value + if ( + obj && + typeof obj === "object" && + obj.hasOwnProperty("value") && + obj.hasOwnProperty("label") + ) { + return obj.value; + } + + // If it's an array, process each item + if (Array.isArray(obj)) { + return obj.map((item) => extractValues(item)); + } + + // If it's an object, process each property + if (typeof obj === "object") { + const result = {}; + Object.keys(obj).forEach((key) => { + const value = extractValues(obj[key]); + + // Handle @odata objects created by React Hook Form's dot notation interpretation + if (key.endsWith("@odata") && value && typeof value === "object") { + // Convert @odata objects back to dot notation properties + Object.keys(value).forEach((odataKey) => { + // Always try to restore the original @odata property, regardless of form value + const baseKey = key.replace("@odata", ""); + const originalKey = `${baseKey}@odata.${odataKey}`; + const originalValue = getOriginalValueByPath(templateData, originalKey); + if (originalValue !== undefined) { + result[originalKey] = originalValue; + } + }); + } else { + result[key] = value; + } + }); + return result; + } + + // For primitive values, return as-is + return obj; + }; + + // Helper function to get original value by dot-notation path + const getOriginalValueByPath = (obj, path) => { + const keys = path.split("."); + let current = obj; + for (const key of keys) { + if (current && typeof current === "object" && key in current) { + current = current[key]; + } else { + return undefined; + } + } + return current; + }; + + // Extract values from the entire form data and include id + const processedValues = extractValues(values); + + return { + id, + ...processedValues, + }; + }; + + return ( + + + {templateQuery.isLoading ? ( + + ) : templateQuery.isError || !templateData ? ( + Error loading template or template not found. + ) : ( + + )} + + + ); +}; + +EditIntuneTemplate.getLayout = (page) => {page}; + +export default EditIntuneTemplate; diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index d9beda8689ae..0bc90907c397 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { GitHub } from "@mui/icons-material"; +import { Edit, GitHub } from "@mui/icons-material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { ApiGetCall } from "/src/api/ApiCall"; @@ -14,6 +14,12 @@ const Page = () => { refetchOnReconnect: false, }); const actions = [ + { + label: "Edit Template", + link: `/endpoint/MEM/list-templates/edit?id=[GUID]`, + icon: , + color: "info", + }, { label: "Edit Template Name and Description", type: "POST", diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index f9a6a42a9aa2..d894dfd18433 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Box, Button, Divider, Typography } from "@mui/material"; +import { Box, Button, Divider, Typography, Alert } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; @@ -18,6 +18,7 @@ const EditGroup = () => { const [groupIdReady, setGroupIdReady] = useState(false); const [showMembershipTable, setShowMembershipTable] = useState(false); const [combinedData, setCombinedData] = useState([]); + const [initialValues, setInitialValues] = useState({}); const tenantFilter = useSettings().currentTenant; const groupInfo = ApiGetCall({ @@ -65,13 +66,14 @@ const EditGroup = () => { ]; setCombinedData(combinedData); - // Reset the form with all values - formControl.reset({ + // Create initial values object + const formValues = { tenantFilter: tenantFilter, mail: group.mail, mailNickname: group.mailNickname || "", allowExternal: groupInfo?.data?.allowExternal, sendCopies: groupInfo?.data?.sendCopies, + hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, displayName: group.displayName, description: group.description || "", membershipRules: group.membershipRule || "", @@ -103,11 +105,37 @@ const EditGroup = () => { RemoveOwner: [], AddContact: [], RemoveContact: [], + }; + + // Store initial values for comparison + setInitialValues({ + allowExternal: groupInfo?.data?.allowExternal, + sendCopies: groupInfo?.data?.sendCopies, + hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, }); + + // Reset the form with all values + formControl.reset(formValues); } } }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]); + // Custom data formatter to only send changed values + const customDataFormatter = (formData) => { + const cleanedData = { ...formData }; + + // Properties that should only be sent if they've changed from initial values + const changeDetectionProperties = ["allowExternal", "sendCopies", "hideFromOutlookClients"]; + + changeDetectionProperties.forEach((property) => { + if (formData[property] === initialValues[property]) { + delete cleanedData[property]; + } + }); + + return cleanedData; + }; + return ( <> { formPageType="Edit" backButtonTitle="Group Overview" postUrl="/api/EditGroup" + customDataformatter={customDataFormatter} titleButton={ <> - - - - } - apiData={{ - Endpoint: "users", - manualPagination: true, - $select: - "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", - $count: true, - $orderby: "displayName", - $top: 999, - }} - apiDataKey="Results" - actions={CippUserActions()} - offCanvas={offCanvas} - simpleColumns={[ - "accountEnabled", - "userPrincipalName", - "displayName", - "mail", - "businessPhones", - "proxyAddresses", - "assignedLicenses", - ]} - filters={filters} - /> - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Send, GroupAdd, PersonAdd } from "@mui/icons-material"; +import Link from "next/link"; +import { useSettings } from "/src/hooks/use-settings.js"; +import { PermissionButton } from "../../../../utils/permissions"; +import { CippUserActions } from "/src/components/CippComponents/CippUserActions.jsx"; + +const Page = () => { + const pageTitle = "Users"; + const tenant = useSettings().currentTenant; + const cardButtonPermissions = ["Identity.User.ReadWrite"]; + + const filters = [ + { + filterName: "Account Enabled", + value: [{ id: "accountEnabled", value: "Yes" }], + type: "column", + }, + { + filterName: "Account Disabled", + value: [{ id: "accountEnabled", value: "No" }], + type: "column", + }, + { + filterName: "Guest Accounts", + value: [{ id: "userType", value: "Guest" }], + type: "column", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "createdDateTime", // Created Date (UTC) + "id", // Unique ID + "userPrincipalName", // UPN + "givenName", // Given Name + "surname", // Surname + "jobTitle", // Job Title + "assignedLicenses", // Licenses + "businessPhones", // Business Phone + "mobilePhone", // Mobile Phone + "mail", // Mail + "city", // City + "department", // Department + "onPremisesLastSyncDateTime", // OnPrem Last Sync + "onPremisesDistinguishedName", // OnPrem DN + "otherMails", // Alternate Email Addresses + ], + actions: CippUserActions(), + }; + + return ( + + } + > + Add User + + } + > + Bulk Add Users + + } + > + Invite Guest + + + } + apiData={{ + Endpoint: "users", + manualPagination: true, + $select: + "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", + $count: true, + $orderby: "displayName", + $top: 999, + }} + apiDataKey="Results" + actions={CippUserActions()} + offCanvas={offCanvas} + simpleColumns={[ + "accountEnabled", + "userPrincipalName", + "displayName", + "mail", + "businessPhones", + "proxyAddresses", + "assignedLicenses", + ]} + filters={filters} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index aa7f3b9d0ba1..8b3bd33da2b5 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -352,7 +352,7 @@ const Page = () => { becPollingCall.data.NewUsers.length > 0 && ( - {becPollingCall.data.NewUsers.map((user) => ( + {becPollingCall.data.NewUsers.map((user, index) => ( { const { currentTenant } = useSettings(); const [domainVisible, setDomainVisible] = useState(false); + const [standardsDialogOpen, setStandardsDialogOpen] = useState(false); const organization = ApiGetCall({ url: "/api/ListOrg", @@ -254,13 +256,16 @@ const Page = () => { - + + setStandardsDialogOpen(true)} + /> + @@ -357,6 +362,13 @@ const Page = () => { + + setStandardsDialogOpen(false)} + standardsData={standards.data} + currentTenant={currentTenant} + /> ); }; diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 1c40646bc609..d0d81b526bf1 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -6,6 +6,7 @@ import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import { CippFormInputArray } from "/src/components/CippComponents/CippFormInputArray"; const DeployDefenderForm = () => { const formControl = useForm({ @@ -297,6 +298,77 @@ const DeployDefenderForm = () => { + {/* Exclusion Policy Section */} + + + + + + + Exclusion Policy + Configure Defender Exclusions + + + + + + + + + + + + Assign to Group + + + + + + {/* ASR Section */} { alert.RawAlert.PostExecution.split(",").includes(opt.value) ); - // Create the reset object with all the form values - const resetObject = { - tenantFilter: { + // Create tenant filter object - handle both regular tenants and tenant groups + let tenantFilterForForm; + if (alert.RawAlert.TenantGroup) { + try { + const tenantGroupObject = JSON.parse(alert.RawAlert.TenantGroup); + tenantFilterForForm = { + value: tenantGroupObject.value, + label: tenantGroupObject.label, + type: "Group", + addedFields: tenantGroupObject, + }; + } catch (error) { + console.error("Error parsing tenant group:", error); + // Fall back to regular tenant + tenantFilterForForm = { + value: alert.RawAlert.Tenant, + label: alert.RawAlert.Tenant, + type: "Tenant", + }; + } + } else { + tenantFilterForForm = { value: alert.RawAlert.Tenant, label: alert.RawAlert.Tenant, - }, + type: "Tenant", + }; + } + + // Create the reset object with all the form values + const resetObject = { + tenantFilter: tenantFilterForForm, excludedTenants: excludedTenantsFormatted, command: { value: usedCommand, label: usedCommand.label }, recurrence: recurrenceOption, @@ -260,9 +285,9 @@ const AlertWizard = () => { const postObject = { RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined, - tenantFilter: values.tenantFilter?.value, + tenantFilter: values.tenantFilter, excludedTenants: values.excludedTenants, - Name: `${values.tenantFilter.value}: ${values.command.label}`, + Name: `${values.tenantFilter?.label || values.tenantFilter?.value}: ${values.command.label}`, Command: { value: `Get-CIPPAlert${values.command.value.name}` }, Parameters: getInputParams(), ScheduledTime: Math.floor(new Date().getTime() / 1000) + 60, @@ -556,6 +581,7 @@ const AlertWizard = () => { multiple={false} formControl={formControl} label="Included Tenants for alert" + includeGroups={true} validators={{ required: { value: true, message: "This field is required" }, }} diff --git a/src/pages/tenant/administration/alert-configuration/index.js b/src/pages/tenant/administration/alert-configuration/index.js index e665b31b5228..8040f018d37f 100644 --- a/src/pages/tenant/administration/alert-configuration/index.js +++ b/src/pages/tenant/administration/alert-configuration/index.js @@ -1,70 +1,76 @@ -import { Button } from "@mui/material"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. -import Link from "next/link"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material"; - -const Page = () => { - const pageTitle = "Alerts"; - const actions = [ - { - label: "Edit Alert", - link: "/tenant/administration/alert-configuration/alert?id=[RowKey]", - icon: , - color: "success", - target: "_self", - }, - { - label: "Clone & Edit Alert", - link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true", - icon: , - color: "success", - target: "_self", - }, - { - label: "Delete Alert", - type: "POST", - url: "/api/RemoveQueuedAlert", - data: { - ID: "RowKey", - EventType: "EventType", - }, - icon: , - relatedQueryKeys: "ListAlertsQueue", - confirmText: "Are you sure you want to delete this Alert?", - multiPost: false, - }, - ]; - - return ( - } - > - Add Alert - - } - actions={actions} - simpleColumns={[ - "Tenants", - "EventType", - "Conditions", - "RepeatsEvery", - "Actions", - "excludedTenants", - ]} - queryKey="ListAlertsQueue" - /> - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import Link from "next/link"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Alerts"; + const actions = [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + condition: (row) => row?.EventType === "Scheduled Task", + }, + { + label: "Edit Alert", + link: "/tenant/administration/alert-configuration/alert?id=[RowKey]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Clone & Edit Alert", + link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true", + icon: , + color: "success", + target: "_self", + }, + { + label: "Delete Alert", + type: "POST", + url: "/api/RemoveQueuedAlert", + data: { + ID: "RowKey", + EventType: "EventType", + }, + icon: , + relatedQueryKeys: "ListAlertsQueue", + confirmText: "Are you sure you want to delete this Alert?", + multiPost: false, + }, + ]; + + return ( + } + > + Add Alert + + } + actions={actions} + simpleColumns={[ + "Tenants", + "EventType", + "Conditions", + "RepeatsEvery", + "Actions", + "excludedTenants", + ]} + queryKey="ListAlertsQueue" + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/app-consent-requests/index.js b/src/pages/tenant/administration/app-consent-requests/index.js index f382bb3d71cd..5aafe8c75958 100644 --- a/src/pages/tenant/administration/app-consent-requests/index.js +++ b/src/pages/tenant/administration/app-consent-requests/index.js @@ -1,23 +1,22 @@ import { useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; +import { + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + SvgIcon, + Stack, +} from "@mui/material"; import { Grid } from "@mui/system"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Visibility, CheckCircle, ExpandMore, Security } from "@mui/icons-material"; +import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; -const simpleColumns = [ - "Tenant", - "CippStatus", - "appDisplayName", - "requestUser", - "requestReason", - "requestStatus", - "requestDate", -]; - const apiUrl = "/api/ListAppConsentRequests"; const pageTitle = "App Consent Requests"; @@ -25,32 +24,132 @@ const Page = () => { const tenantFilter = useSettings().currentTenant; const formControl = useForm({ defaultValues: { - requestStatus: "All", + requestStatus: "InProgress", }, }); - const [expanded, setExpanded] = useState(false); // Accordion state - const [filterParams, setFilterParams] = useState({}); // Dynamic filter params + const [expanded, setExpanded] = useState(true); // Accordion state - start expanded since we have a default filter + const [filterEnabled, setFilterEnabled] = useState(true); // State for filter toggle - start with filter enabled + const [requestStatus, setRequestStatus] = useState("InProgress"); // State for request status filter - default to InProgress + const [requestStatusLabel, setRequestStatusLabel] = useState("Pending"); // State for displaying filter label - default label const onSubmit = (data) => { - // Handle filter application logic - const { requestStatus } = data; - const filters = {}; + // Handle the case where requestStatus could be an object {label, value} or a string + const statusValue = + typeof data.requestStatus === "object" && data.requestStatus?.value + ? data.requestStatus.value + : data.requestStatus; + const statusLabel = + typeof data.requestStatus === "object" && data.requestStatus?.label + ? data.requestStatus.label + : data.requestStatus; + + // Check if any filter is applied + const hasFilter = statusValue !== "All"; + setFilterEnabled(hasFilter); - if (requestStatus !== "All") { - filters.requestStatus = requestStatus; - } + // Set request status filter if not "All" + setRequestStatus(hasFilter ? statusValue : null); + setRequestStatusLabel(hasFilter ? statusLabel : null); - setFilterParams(filters); + // Close the accordion after applying filters + setExpanded(false); + }; + + const clearFilters = () => { + formControl.reset({ + requestStatus: "All", + }); + setFilterEnabled(false); + setRequestStatus(null); + setRequestStatusLabel(null); + setExpanded(false); // Close the accordion when clearing filters + }; + + const actions = [ + { + label: "Review in Entra", + link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_IAM/StartboardApplicationsMenuBlade/~/AccessRequests`, + color: "info", + icon: , + target: "_blank", + external: true, + }, + { + label: "Approve in Entra", + link: "[consentUrl]", + color: "info", + icon: , + target: "_blank", + external: true, + }, + ]; + + const simpleColumns = [ + "requestUser", // Requester + "appDisplayName", // Application Name + "appId", // Application ID + "requestReason", // Reason + "requestStatus", // Status + "reviewedBy", // Reviewed by + "reviewedJustification", // Reviewed Reason + "consentUrl", // Consent URL + ]; + + const filters = [ + { + filterName: "Pending requests", + value: [{ id: "requestStatus", value: "InProgress" }], + type: "column", + }, + { + filterName: "Expired requests", + value: [{ id: "requestStatus", value: "Expired" }], + type: "column", + }, + { + filterName: "Completed requests", + value: [{ id: "requestStatus", value: "Completed" }], + type: "column", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "requestUser", // Requester + "appDisplayName", // Application Name + "appId", // Application ID + "requestReason", // Reason + "requestStatus", // Status + "reviewedBy", // Reviewed by + "reviewedJustification", // Reviewed Reason + "consentUrl", // Consent URL + ], + actions: actions, }; return ( setExpanded(!expanded)}> - }> - Filters + }> + + + + + + App Consent Request Filters + {filterEnabled ? ( + + ({requestStatusLabel && <>Status: {requestStatusLabel}}) + + ) : ( + + (No filters applied) + + )} + +
@@ -72,11 +171,34 @@ const Page = () => { /> - {/* Submit Button */} + {/* Action Buttons */} - + + + +
@@ -86,45 +208,14 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} simpleColumns={simpleColumns} - filters={[ - // Filter for showing only pending requests - { - filterName: "Pending requests", - value: [{ id: "requestStatus", value: "InProgress" }], - type: "column", - }, - ]} - queryKey={`AppConsentRequests-${JSON.stringify(filterParams)}-${tenantFilter}`} + filters={filters} + queryKey={`AppConsentRequests-${requestStatus}-${filterEnabled}-${tenantFilter}`} apiData={{ - ...filterParams, - }} - offCanvas={{ - extendedInfoFields: [ - "requestUser", // Requester - "appDisplayName", // Application Name - "appId", // Application ID - "requestReason", // Reason - "requestStatus", // Status - "reviewedBy", // Reviewed by - "reviewedJustification", // Reviewed Reason - ], + RequestStatus: requestStatus, // Pass request status filter from state + Filter: filterEnabled, // Pass filter toggle state }} - actions={[ - { - label: "Review in Entra", - link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_IAM/StartboardApplicationsMenuBlade/~/AccessRequests`, - color: "info", - target: "_blank", - external: true, - }, - { - label: "Approve in Entra", - link: "[consentUrl]", - color: "info", - target: "_blank", - external: true, - }, - ]} + offCanvas={offCanvas} + actions={actions} /> ); }; diff --git a/src/pages/tenant/administration/applications/app-registrations.js b/src/pages/tenant/administration/applications/app-registrations.js index ff9a643f1e92..c311e1b56420 100644 --- a/src/pages/tenant/administration/applications/app-registrations.js +++ b/src/pages/tenant/administration/applications/app-registrations.js @@ -2,13 +2,20 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Launch } from "@mui/icons-material"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent.jsx"; +import { CertificateCredentialRemovalForm } from "/src/components/CippComponents/CertificateCredentialRemovalForm.jsx"; +import CippPermissionPreview from "/src/components/CippComponents/CippPermissionPreview.jsx"; +import { Launch, Delete, Edit, Key, Security, Block, CheckCircle, Save } from "@mui/icons-material"; +import { usePermissions } from "/src/hooks/use-permissions.js"; import tabOptions from "./tabOptions"; const Page = () => { const pageTitle = "App Registrations"; const apiUrl = "/api/ListGraphRequest"; + const { checkPermissions } = usePermissions(); + const canWriteApplication = checkPermissions(["Tenant.Application.ReadWrite"]); + const actions = [ { icon: , @@ -28,6 +35,125 @@ const Page = () => { multiPost: false, external: true, }, + { + icon: , + label: "Remove Password Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "applications", + Action: "RemovePassword", + }, + children: ({ formHook, row }) => { + return ( + ({ + label: `${cred.displayName || "Unnamed"} (Expiration: ${new Date( + cred.endDateTime + ).toLocaleDateString()})`, + value: cred.keyId, + })) || [] + } + /> + ); + }, + confirmText: "Are you sure you want to remove the selected password credentials?", + condition: (row) => canWriteApplication && row?.passwordCredentials?.length > 0, + }, + { + icon: , + label: "Remove Certificate Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "applications", + Action: "RemoveKey", + }, + children: ({ formHook, row }) => { + return ; + }, + confirmText: "Are you sure you want to remove the selected certificate credentials?", + condition: (row) => canWriteApplication && row?.keyCredentials?.length > 0, + }, + { + icon: , + label: "Create Template from App Registration", + type: "POST", + color: "success", + multiPost: false, + url: "/api/ExecAppApprovalTemplate", + fields: [ + { + label: "Template Name", + name: "TemplateName", + type: "textField", + placeholder: "Enter a name for the template", + required: true, + validators: { + required: { value: true, message: "Template name is required" }, + }, + }, + ], + customDataformatter: (row, action, formData) => { + const propertiesToRemove = [ + "appId", + "id", + "createdDateTime", + "deletedDateTime", + "publisherDomain", + "servicePrincipalLockConfiguration", + "identifierUris", + "applicationIdUris", + "Tenant", + "CippStatus", + ]; + + const cleanManifest = { ...row }; + propertiesToRemove.forEach((prop) => { + delete cleanManifest[prop]; + }); + + return { + Action: "Save", + TemplateName: formData.TemplateName, + AppType: "ApplicationManifest", + AppName: row.displayName || row.appId, + ApplicationManifest: cleanManifest, + }; + }, + confirmText: "Are you sure you want to create a template from this app registration?", + condition: (row) => canWriteApplication && row.signInAudience === "AzureADMyOrg", + }, + { + icon: , + label: "Delete App Registration", + type: "POST", + color: "error", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "applications", + Action: "Delete", + }, + confirmText: + "Are you sure you want to delete this application registration? This action cannot be undone.", + condition: () => canWriteApplication, + }, ]; const offCanvas = { @@ -37,14 +163,22 @@ const Page = () => { "appId", "createdDateTime", "signInAudience", + "disabledByMicrosoftStatus", "replyUrls", - "requiredResourceAccess", - "web", - "api", "passwordCredentials", "keyCredentials", ], actions: actions, + children: (row) => { + return ( + + ); + }, }; const simpleColumns = [ @@ -60,8 +194,6 @@ const Page = () => { const apiParams = { Endpoint: "applications", - $select: - "id,appId,displayName,createdDateTime,signInAudience,web,api,requiredResourceAccess,publisherDomain,replyUrls,passwordCredentials,keyCredentials", $count: true, $top: 999, }; diff --git a/src/pages/tenant/administration/applications/enterprise-apps.js b/src/pages/tenant/administration/applications/enterprise-apps.js index b7c80840c4e4..9c7cabac6408 100644 --- a/src/pages/tenant/administration/applications/enterprise-apps.js +++ b/src/pages/tenant/administration/applications/enterprise-apps.js @@ -2,13 +2,19 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Launch } from "@mui/icons-material"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent.jsx"; +import { CertificateCredentialRemovalForm } from "/src/components/CippComponents/CertificateCredentialRemovalForm.jsx"; +import { Launch, Delete, Edit, Key, Security, Block, CheckCircle } from "@mui/icons-material"; +import { usePermissions } from "/src/hooks/use-permissions.js"; import tabOptions from "./tabOptions"; const Page = () => { const pageTitle = "Enterprise Applications"; const apiUrl = "/api/ListGraphRequest"; + const { checkPermissions } = usePermissions(); + const canWriteApplication = checkPermissions(["Tenant.Application.ReadWrite"]); + const actions = [ { icon: , @@ -19,16 +25,126 @@ const Page = () => { multiPost: false, external: true, }, + { + icon: , + label: "Remove Password Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "RemovePassword", + }, + children: ({ formHook, row }) => { + return ( + ({ + label: `${cred.displayName || "Unnamed"} (Expiration: ${new Date( + cred.endDateTime + ).toLocaleDateString()})`, + value: cred.keyId, + })) || [] + } + /> + ); + }, + confirmText: "Are you sure you want to remove the selected password credentials?", + condition: (row) => canWriteApplication && row?.passwordCredentials?.length > 0, + }, + { + icon: , + label: "Remove Certificate Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "RemoveKey", + }, + children: ({ formHook, row }) => { + return ; + }, + confirmText: "Are you sure you want to remove the selected certificate credentials?", + condition: (row) => canWriteApplication && row?.keyCredentials?.length > 0, + }, + { + icon: , + label: "Disable Service Principal", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "Update", + Payload: { + accountEnabled: false, + }, + }, + confirmText: + "Are you sure you want to disable this service principal? Users will not be able to sign in to this application.", + condition: (row) => canWriteApplication && row?.accountEnabled === true, + }, + { + icon: , + label: "Enable Service Principal", + type: "POST", + color: "success", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "Update", + Payload: { + accountEnabled: true, + }, + }, + confirmText: "Are you sure you want to enable this service principal?", + condition: (row) => canWriteApplication && row?.accountEnabled === false, + }, + { + icon: , + label: "Delete Service Principal", + type: "POST", + color: "error", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "Delete", + }, + confirmText: + "Are you sure you want to delete this service principal? This will remove the application from this tenant but will not affect the app registration.", + condition: () => canWriteApplication, + }, ]; const offCanvas = { extendedInfoFields: [ "displayName", "createdDateTime", + "accountEnabled", "publisherName", "replyUrls", "appOwnerOrganizationId", "tags", + "passwordCredentials", + "keyCredentials", ], actions: actions, }; @@ -37,6 +153,7 @@ const Page = () => { "info.logoUrl", "displayName", "appId", + "accountEnabled", "createdDateTime", "publisherName", "homepage", diff --git a/src/pages/tenant/administration/applications/templates/index.js b/src/pages/tenant/administration/applications/templates/index.js index c8e7e567a617..980245b47aba 100644 --- a/src/pages/tenant/administration/applications/templates/index.js +++ b/src/pages/tenant/administration/applications/templates/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import CippPermissionPreview from "/src/components/CippComponents/CippPermissionPreview.jsx"; import { Edit, Delete, ContentCopy, Add, GitHub } from "@mui/icons-material"; import tabOptions from "../tabOptions"; import { Button } from "@mui/material"; @@ -92,19 +93,115 @@ const Page = () => { ]; const offCanvas = { - extendedInfoFields: [ - "TemplateName", - "AppId", - "AppName", - "PermissionSetName", - "UpdatedBy", - "Timestamp", - ], + extendedInfoFields: ["TemplateName", "AppType", "AppId", "AppName", "UpdatedBy", "Timestamp"], actions: actions, + children: (row) => { + // Default to EnterpriseApp for backward compatibility with older templates + const appType = row.AppType || "EnterpriseApp"; + + // Determine the title based on app type + let title = "Permission Preview"; + if (appType === "GalleryTemplate") { + title = "Gallery Template Info"; + } else if (appType === "ApplicationManifest") { + title = "Application Manifest"; + } + + return ( + + ); + }, }; + const columns = [ + { + name: "TemplateName", + label: "Template Name", + sortable: true, + }, + { + name: "AppType", + label: "Type", + sortable: true, + formatter: (row) => { + // Default to EnterpriseApp for backward compatibility with older templates + const appType = row.AppType || "EnterpriseApp"; + if (appType === "GalleryTemplate") { + return "Gallery Template"; + } else if (appType === "ApplicationManifest") { + return "Application Manifest"; + } else { + return "Enterprise App"; + } + }, + }, + { + name: "AppId", + label: "App ID", + sortable: true, + }, + { + name: "AppName", + label: "App Name", + sortable: true, + }, + { + name: "PermissionSetName", + label: "Permission Set", + sortable: true, + formatter: (row) => { + // Default to EnterpriseApp for backward compatibility with older templates + const appType = row.AppType || "EnterpriseApp"; + if (appType === "GalleryTemplate") { + return "Auto-Consent"; + } else if (appType === "ApplicationManifest") { + return "Manifest-Defined"; + } else { + return row.PermissionSetName || "-"; + } + }, + }, + { + name: "UpdatedBy", + label: "Updated By", + sortable: true, + }, + { + name: "Timestamp", + label: "Last Updated", + sortable: true, + }, + ]; + const simpleColumns = [ "TemplateName", + "AppType", "AppId", "AppName", "PermissionSetName", @@ -117,6 +214,7 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} queryKey="ListAppApprovalTemplates" + columns={columns} simpleColumns={simpleColumns} tableProps={{ keyField: "TemplateId" }} actions={actions} diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx index 5e1c23e9c808..b4fa24353a71 100644 --- a/src/pages/tenant/conditional/deploy-vacation/add.jsx +++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx @@ -1,25 +1,25 @@ import React from "react"; -import { Box, Divider, Typography } from "@mui/material"; +import { Box, Divider, Stack, Typography } from "@mui/material"; import { Grid } from "@mui/system"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { useSettings } from "/src/hooks/use-settings"; import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; const Page = () => { - const userSettingsDefaults = useSettings(); - const tenantDomain = userSettingsDefaults?.currentTenant; - const formControl = useForm({ mode: "onChange", defaultValues: { - tenantFilter: tenantDomain, vacation: true, }, }); + // Watch the selected tenant to update dependent fields + const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const tenantDomain = selectedTenant?.value || selectedTenant; + return ( <> { postUrl="/api/ExecCAExclusion" customDataformatter={(values) => { const shippedValues = { - tenantFilter: tenantDomain, - UserId: values.UserId?.value, + tenantFilter: values.tenantFilter?.value || values.tenantFilter, + Users: values.Users, PolicyId: values.PolicyId?.value, StartDate: values.startDate, EndDate: values.endDate, @@ -40,25 +40,40 @@ const Page = () => { return shippedValues; }} > - + Vacation mode adds scheduled tasks to add and remove users from Conditional Access (CA) exclusions for a specific period of time. Select the CA policy and the date range. - + + + + {/* User Selector */} @@ -66,14 +81,23 @@ const Page = () => { `${option.displayName}`, - valueField: "id", - }} + api={ + tenantDomain + ? { + queryKey: `ListConditionalAccessPolicies-${tenantDomain}`, + url: "/api/ListConditionalAccessPolicies", + data: { tenantFilter: tenantDomain }, + labelField: (option) => `${option.displayName}`, + valueField: "id", + } + : null + } multiple={false} formControl={formControl} validators={{ @@ -85,6 +109,7 @@ const Page = () => { }, }} required={true} + disabled={!tenantDomain} /> @@ -132,7 +157,7 @@ const Page = () => { />
- +
); diff --git a/src/pages/tenant/conditional/deploy-vacation/index.js b/src/pages/tenant/conditional/deploy-vacation/index.js index e1c24e2a761c..a4a95667cfee 100644 --- a/src/pages/tenant/conditional/deploy-vacation/index.js +++ b/src/pages/tenant/conditional/deploy-vacation/index.js @@ -4,9 +4,15 @@ import { Button } from "@mui/material"; import { EventAvailable } from "@mui/icons-material"; import Link from "next/link"; import { Delete } from "@mui/icons-material"; +import { EyeIcon } from "@heroicons/react/24/outline"; const Page = () => { const actions = [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + }, { label: "Cancel Vacation Mode", type: "POST", @@ -34,18 +40,21 @@ const Page = () => { tenantInTitle={false} actions={actions} simpleColumns={[ + "Tenant", "Name", "TaskState", "ScheduledTime", + "ExecutedTime", "Parameters.ExclusionType", + "Parameters.Users", "Parameters.UserName", - "Parameters.PolicyId", ]} offCanvas={{ extendedInfoFields: [ "Name", "TaskState", "ScheduledTime", + "Parameters.Users", "Parameters.UserName", "Parameters.PolicyId", "Tenant", diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index 311fb0b6f0f8..0feb27282419 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -9,7 +9,7 @@ import { Visibility as VisibilityIcon, Edit as EditIcon, } from "@mui/icons-material"; -import { Button } from "@mui/material"; +import { Box, Button } from "@mui/material"; import Link from "next/link"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; @@ -111,12 +111,25 @@ const Page = () => { }, ], }, + { + label: "Add service provider exception to policy", + type: "POST", + url: "/api/ExecCAServiceExclusion", + data: { + GUID: "id", + }, + confirmText: "Are you sure you want to add the service provider exception to this policy?", + icon: , + color: "warning", + }, ]; // Off-canvas configuration const offCanvas = { children: (row) => ( - + + + ), size: "xl", }; diff --git a/src/pages/tenant/conditional/list-template/edit.jsx b/src/pages/tenant/conditional/list-template/edit.jsx new file mode 100644 index 000000000000..0639fb829417 --- /dev/null +++ b/src/pages/tenant/conditional/list-template/edit.jsx @@ -0,0 +1,142 @@ +import React, { useEffect, useState } from "react"; +import { Alert, Box, Typography } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import { ApiGetCall } from "/src/api/ApiCall"; +import CippTemplateFieldRenderer from "/src/components/CippComponents/CippTemplateFieldRenderer"; + +const EditCATemplate = () => { + const router = useRouter(); + const { GUID } = router.query; + const [templateData, setTemplateData] = useState(null); + const [originalData, setOriginalData] = useState(null); + + const formControl = useForm({ mode: "onChange" }); + + // Fetch the template data + const templateQuery = ApiGetCall({ + url: `/api/ListCATemplates?GUID=${GUID}`, + queryKey: `CATemplate-${GUID}`, + enabled: !!GUID, + }); + + useEffect(() => { + if (templateQuery.isSuccess && templateQuery.data) { + // Find the template with matching GUID + const template = Array.isArray(templateQuery.data) + ? templateQuery.data.find((t) => t.GUID === GUID) + : templateQuery.data; + + if (template) { + setTemplateData(template); + setOriginalData(template); + } + } + }, [templateQuery.isSuccess, templateQuery.data, GUID]); + + // Custom data formatter to convert autoComplete objects to values + const customDataFormatter = (values) => { + // Recursively extract values from autoComplete objects and fix @odata issues + const extractValues = (obj) => { + if (!obj) return obj; + + // If this is an autoComplete object with label/value, return just the value + if ( + obj && + typeof obj === "object" && + obj.hasOwnProperty("value") && + obj.hasOwnProperty("label") + ) { + return obj.value; + } + + // If it's an array, process each item + if (Array.isArray(obj)) { + return obj.map((item) => extractValues(item)); + } + + // If it's an object, process each property + if (typeof obj === "object") { + const result = {}; + Object.keys(obj).forEach((key) => { + const value = extractValues(obj[key]); + + // Handle @odata objects created by React Hook Form's dot notation interpretation + if (key.endsWith("@odata") && value && typeof value === "object") { + // Convert @odata objects back to dot notation properties + Object.keys(value).forEach((odataKey) => { + // Always try to restore the original @odata property, regardless of form value + const baseKey = key.replace("@odata", ""); + const originalKey = `${baseKey}@odata.${odataKey}`; + const originalValue = getOriginalValueByPath(originalData, originalKey); + if (originalValue !== undefined) { + result[originalKey] = originalValue; + } + }); + } else { + result[key] = value; + } + }); + return result; + } + + // For primitive values, return as-is + return obj; + }; + + // Helper function to get original value by dot-notation path + const getOriginalValueByPath = (obj, path) => { + const keys = path.split("."); + let current = obj; + for (const key of keys) { + if (current && typeof current === "object" && key in current) { + current = current[key]; + } else { + return undefined; + } + } + return current; + }; + + // Extract values from the entire form data and include GUID + const processedValues = extractValues(values); + + return { + GUID, + ...processedValues, + }; + }; + + return ( + + + {templateQuery.isLoading ? ( + + ) : templateQuery.isError || !templateData ? ( + Error loading template or template not found. + ) : ( + + )} + + + ); +}; + +EditCATemplate.getLayout = (page) => {page}; + +export default EditCATemplate; diff --git a/src/pages/tenant/conditional/list-template/index.js b/src/pages/tenant/conditional/list-template/index.js index 0ead857b8ea6..e53684b10849 100644 --- a/src/pages/tenant/conditional/list-template/index.js +++ b/src/pages/tenant/conditional/list-template/index.js @@ -2,8 +2,9 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; -import { Delete, GitHub } from "@mui/icons-material"; +import { Delete, GitHub, Edit } from "@mui/icons-material"; import { ApiGetCall } from "/src/api/ApiCall"; +import Link from "next/link"; const Page = () => { const pageTitle = "Available Conditional Access Templates"; @@ -14,6 +15,12 @@ const Page = () => { refetchOnReconnect: false, }); const actions = [ + { + label: "Edit Template", + link: "/tenant/conditional/list-template/edit?GUID=[GUID]", + icon: , + color: "info", + }, { label: "Save to GitHub", type: "POST", diff --git a/src/pages/tenant/gdap-management/index.js b/src/pages/tenant/gdap-management/index.js index e370e2cfa361..372c3422e0b6 100644 --- a/src/pages/tenant/gdap-management/index.js +++ b/src/pages/tenant/gdap-management/index.js @@ -59,7 +59,7 @@ const Page = () => { // check templates for CIPP Defaults if ( roleTemplates?.data?.pages?.[0].Results?.length > 0 && - roleTemplates?.data?.pages?.[0].Results?.find((t) => t.TemplateId === "CIPP Defaults") + roleTemplates?.data?.pages?.[0].Results?.find((t) => t?.TemplateId === "CIPP Defaults") ) { promptCreateDefaults = false; } diff --git a/src/pages/tenant/standards/bpa-report/builder.js b/src/pages/tenant/standards/bpa-report/builder.js index 4a0d2f36b62f..976d79b63c53 100644 --- a/src/pages/tenant/standards/bpa-report/builder.js +++ b/src/pages/tenant/standards/bpa-report/builder.js @@ -255,7 +255,6 @@ const Page = () => { {blockCards.map((block, index) => ( diff --git a/src/pages/tenant/standards/bpa-report/view.js b/src/pages/tenant/standards/bpa-report/view.js index 361e14780442..0af8c51a7011 100644 --- a/src/pages/tenant/standards/bpa-report/view.js +++ b/src/pages/tenant/standards/bpa-report/view.js @@ -157,7 +157,6 @@ const Page = () => { <> {blockCards.map((block, index) => ( diff --git a/src/pages/tenant/standards/compare/index.js b/src/pages/tenant/standards/compare/index.js index f3f66c2c390f..75e7892d81cf 100644 --- a/src/pages/tenant/standards/compare/index.js +++ b/src/pages/tenant/standards/compare/index.js @@ -315,10 +315,17 @@ const Page = () => { const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower); const filteredStandards = groupedStandards[category].filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + const hasLicenseMissing = typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); + const matchesFilter = filter === "all" || (filter === "compliant" && standard.complianceStatus === "Compliant") || - (filter === "nonCompliant" && standard.complianceStatus === "Non-Compliant"); + (filter === "nonCompliant" && standard.complianceStatus === "Non-Compliant") || + (filter === "nonCompliantWithLicense" && + standard.complianceStatus === "Non-Compliant" && !hasLicenseMissing) || + (filter === "nonCompliantWithoutLicense" && + standard.complianceStatus === "Non-Compliant" && hasLicenseMissing); const matchesSearch = !searchQuery || @@ -345,10 +352,38 @@ const Page = () => { const reportingDisabledCount = comparisonData?.filter((standard) => standard.complianceStatus === "Reporting Disabled") .length || 0; + + // Calculate license-related metrics + const missingLicenseCount = comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); + }).length || 0; + + const nonCompliantWithLicenseCount = comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return standard.complianceStatus === "Non-Compliant" && + !(typeof tenantValue === "string" && tenantValue.startsWith("License Missing:")); + }).length || 0; + + const nonCompliantWithoutLicenseCount = comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return standard.complianceStatus === "Non-Compliant" && + (typeof tenantValue === "string" && tenantValue.startsWith("License Missing:")); + }).length || 0; + const compliancePercentage = allCount > 0 ? Math.round((compliantCount / (allCount - reportingDisabledCount || 1)) * 100) : 0; + + const missingLicensePercentage = + allCount > 0 + ? Math.round((missingLicenseCount / (allCount - reportingDisabledCount || 1)) * 100) + : 0; + + // Combined score: compliance percentage + missing license percentage + // This represents the total "addressable" compliance (compliant + could be compliant if licensed) + const combinedScore = compliancePercentage + missingLicensePercentage; return ( @@ -384,7 +419,7 @@ const Page = () => { {comparisonApi.data?.find( (comparison) => comparison.tenantFilter === currentTenant ) && ( - + @@ -403,6 +438,30 @@ const Page = () => { } sx={{ ml: 2 }} /> + + = 80 + ? "success" + : combinedScore >= 60 + ? "warning" + : "error" + } + /> )} @@ -575,6 +634,18 @@ const Page = () => { > Non-Compliant ({nonCompliantCount}) + + {comparisonApi.isError && ( diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index dcaa36640959..0c479d6fa838 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -130,7 +130,7 @@ const Page = () => {
{oldStandards.isSuccess && oldStandards.data.length !== 0 && ( - + { + const pageTitle = "Tenant Alignment"; + + const actions = [ + { + label: "View Tenant Report", + link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + icon: , + color: "info", + target: "_self", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tools/breachlookup/index.js b/src/pages/tools/breachlookup/index.js index 89936574fe0d..53d9e62c071f 100644 --- a/src/pages/tools/breachlookup/index.js +++ b/src/pages/tools/breachlookup/index.js @@ -52,7 +52,7 @@ const Page = () => { > - + @@ -120,7 +120,7 @@ const Page = () => { )} {getGeoIP.data?.map((breach, index) => ( - + {breach.Title}} diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 39833e154c98..5a1d385a4c4c 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -3,7 +3,7 @@ import { Grid } from "@mui/system"; import Head from "next/head"; import { CippImageCard } from "../components/CippCards/CippImageCard"; import { ApiGetCall } from "../api/ApiCall"; -import { useState, useEffect } from "react"; +import { useMemo } from "react"; const Page = () => { const orgData = ApiGetCall({ @@ -19,16 +19,15 @@ const Page = () => { }); const blockedRoles = ["anonymous", "authenticated"]; - const [userRoles, setUserRoles] = useState([]); - - useEffect(() => { - if (orgData.isSuccess) { - const roles = orgData.data?.clientPrincipal?.userRoles.filter( + // Use useMemo to derive userRoles directly + const userRoles = useMemo(() => { + if (orgData.isSuccess && orgData.data?.clientPrincipal?.userRoles) { + return orgData.data.clientPrincipal.userRoles.filter( (role) => !blockedRoles.includes(role) ); - setUserRoles(roles ?? []); } - }, [orgData, blockedRoles]); + return []; + }, [orgData.isSuccess, orgData.data?.clientPrincipal?.userRoles]); return ( <> diff --git a/src/sections/dashboard/components/stats/stats-2.js b/src/sections/dashboard/components/stats/stats-2.js index f58463294fb1..273abb3e1522 100644 --- a/src/sections/dashboard/components/stats/stats-2.js +++ b/src/sections/dashboard/components/stats/stats-2.js @@ -1,4 +1,5 @@ -import { Box, Typography, Unstable_Grid2 as Grid } from '@mui/material'; +import { Box, Typography } from '@mui/material'; +import { Grid } from "@mui/system"; const data = [ { diff --git a/src/sections/dashboard/invoices/invoices-stats.js b/src/sections/dashboard/invoices/invoices-stats.js index 78521a117f2f..895c96d6d883 100644 --- a/src/sections/dashboard/invoices/invoices-stats.js +++ b/src/sections/dashboard/invoices/invoices-stats.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import numeral from 'numeral'; -import { Box, Card, CardContent, Stack, Typography, Unstable_Grid2 as Grid } from '@mui/material'; +import { Box, Card, CardContent, Stack, Typography } from '@mui/material'; +import { Grid } from "@mui/system"; import { alpha, useTheme } from '@mui/material/styles'; import { Chart } from '../../../components/chart'; diff --git a/src/sections/dashboard/products/products-stats.js b/src/sections/dashboard/products/products-stats.js index 8a8b415d1fa5..6e64f9ba23a5 100644 --- a/src/sections/dashboard/products/products-stats.js +++ b/src/sections/dashboard/products/products-stats.js @@ -2,7 +2,8 @@ import CheckCircleIcon from "@heroicons/react/24/outline/CheckCircleIcon"; import CurrencyDollarIcon from "@heroicons/react/24/outline/CurrencyDollarIcon"; import ShoppingCartIcon from "@heroicons/react/24/outline/ShoppingCartIcon"; import XCircleIcon from "@heroicons/react/24/outline/XCircleIcon"; -import { Card, Stack, Typography, Unstable_Grid2 as Grid } from "@mui/material"; +import { Card, Stack, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; const stats = [ { diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 9f1b07f4769d..832201a4898d 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -167,6 +167,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr "LastOccurrence", "NotBefore", "NotAfter", + "latestDataCollection", ]; const matchDateTime = /[dD]ate[tT]ime/; @@ -195,6 +196,22 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? data : data; } + if (cellName === "alignmentScore" || cellName === "combinedAlignmentScore") { + // Handle alignment score, return a percentage with a label + return isText ? ( + `${data}%` + ) : ( + + ); + } + + if (cellName === "LicenseMissingPercentage") { + return isText ? ( + `${data}%` + ) : ( + + ); + } if (cellName === "RepeatsEvery") { //convert 1d to "Every 1 day", 1w to "Every 1 week" etc. const match = data.match(/(\d+)([a-zA-Z]+)/); @@ -266,7 +283,13 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr } //if the cellName is tenantFilter, return a chip with the tenant name. This can sometimes be an array, sometimes be a single item. - if (cellName === "tenantFilter" || cellName === "Tenant") { + if ( + cellName === "tenantFilter" || + cellName === "Tenant" || + cellName === "Tenants" || + cellName === "AllowedTenants" || + cellName === "BlockedTenants" + ) { //check if data is an array. if (Array.isArray(data)) { return isText @@ -298,11 +321,24 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr }) ); } else { - return isText ? ( - data - ) : ( - - ); + const itemText = data?.label ? data.label : data; + let icon = null; + + if (data?.type === "Group") { + icon = ( + + + + ); + } else { + icon = ( + + + + ); + } + + return isText ? itemText : ; } } @@ -650,11 +686,11 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr // into human-readable format (e.g., "1 hour 23 minutes 30 seconds") across all CIPP tables. // This works for any API response property that contains ISO 8601 duration format. const durationArray = [ - "autoExtendDuration", // GDAP page (/tenant/gdap-management/relationships) - "deploymentDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) - "deploymentTotalDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) - "deviceSetupDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) - "accountSetupDuration" // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "autoExtendDuration", // GDAP page (/tenant/gdap-management/relationships) + "deploymentDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "deploymentTotalDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "deviceSetupDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "accountSetupDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) ]; if (durationArray.includes(cellName)) { isoDuration.setLocales( diff --git a/src/utils/permissions.js b/src/utils/permissions.js new file mode 100644 index 000000000000..df66872101d4 --- /dev/null +++ b/src/utils/permissions.js @@ -0,0 +1,227 @@ +import Button from "@mui/material/Button"; +import { usePermissions } from "/src/hooks/use-permissions.js"; +/** + * Permission Helper Utilities + * + * This module provides utilities for checking user permissions with pattern matching support. + * It uses the same logic as the navigation system to ensure consistency across the application. + */ + +/** + * Check if user has permission using pattern matching + * @param {string[]} userPermissions - Array of user permissions + * @param {string[]} requiredPermissions - Array of required permissions (can include wildcards) + * @returns {boolean} - True if user has at least one of the required permissions + */ +export const hasPermission = (userPermissions, requiredPermissions) => { + if (!userPermissions || !requiredPermissions) { + return false; + } + + if (!Array.isArray(userPermissions) || !Array.isArray(requiredPermissions)) { + return false; + } + + if (requiredPermissions.length === 0) { + return true; // No permissions required + } + + return userPermissions.some((userPerm) => { + return requiredPermissions.some((requiredPerm) => { + // Exact match + if (userPerm === requiredPerm) { + return true; + } + + // Pattern matching - check if required permission contains wildcards + if (requiredPerm.includes("*")) { + // Convert wildcard pattern to regex + const regexPattern = requiredPerm + .replace(/\./g, "\\.") // Escape dots + .replace(/\*/g, ".*"); // Convert * to .* + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(userPerm); + } + + return false; + }); + }); +}; + +/** + * Check if user has any of the required roles + * @param {string[]} userRoles - Array of user roles + * @param {string[]} requiredRoles - Array of required roles + * @returns {boolean} - True if user has at least one of the required roles + */ +export const hasRole = (userRoles, requiredRoles) => { + if (!userRoles || !requiredRoles) { + return false; + } + + if (!Array.isArray(userRoles) || !Array.isArray(requiredRoles)) { + return false; + } + + if (requiredRoles.length === 0) { + return true; // No roles required + } + + return requiredRoles.some((requiredRole) => userRoles.includes(requiredRole)); +}; + +/** + * Check if user has access based on both permissions and roles + * @param {Object} config - Configuration object + * @param {string[]} config.userPermissions - Array of user permissions + * @param {string[]} config.userRoles - Array of user roles + * @param {string[]} config.requiredPermissions - Array of required permissions (can include wildcards) + * @param {string[]} config.requiredRoles - Array of required roles + * @returns {boolean} - True if user has access + */ +export const hasAccess = ({ + userPermissions, + userRoles, + requiredPermissions = [], + requiredRoles = [], +}) => { + // Check roles first (if any are required) + if (requiredRoles.length > 0) { + const hasRequiredRole = hasRole(userRoles, requiredRoles); + if (!hasRequiredRole) { + return false; + } + } + + // Check permissions (if any are required) + if (requiredPermissions.length > 0) { + const hasRequiredPermission = hasPermission(userPermissions, requiredPermissions); + if (!hasRequiredPermission) { + return false; + } + } + + return true; +}; + +/** + * Hook for checking permissions in React components + * @param {string[]} requiredPermissions - Array of required permissions (can include wildcards) + * @param {string[]} requiredRoles - Array of required roles + * @returns {boolean} - True if user has access + */ +export const useHasAccess = (requiredPermissions = [], requiredRoles = []) => { + // This would typically use a context or hook to get current user permissions + // For now, we'll return a function that can be called with user data + return (userPermissions, userRoles) => { + return hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + }; +}; + +/** + * Higher-order component to conditionally render based on permissions + * @param {Object} config - Configuration object + * @param {React.Component} config.component - Component to render if user has access + * @param {string[]} config.requiredPermissions - Array of required permissions + * @param {string[]} config.requiredRoles - Array of required roles + * @param {React.Component} config.fallback - Component to render if user doesn't have access + * @returns {React.Component} - Conditional component + */ +export const withPermissions = ({ + component: Component, + requiredPermissions = [], + requiredRoles = [], + fallback = null, +}) => { + return (props) => { + const { userPermissions, userRoles, ...restProps } = props; + + const hasRequiredAccess = hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + + if (hasRequiredAccess) { + return ; + } + + return fallback; + }; +}; + +/** + * Permission-aware Button component + * @param {Object} props - Button props + * @param {string[]} props.requiredPermissions - Array of required permissions + * @param {string[]} props.requiredRoles - Array of required roles + * @param {boolean} props.hideIfNoAccess - Hide button if user doesn't have access (default: false) + * @returns {React.Component} - Permission-aware button + */ +export const PermissionButton = ({ + requiredPermissions = [], + requiredRoles = [], + hideIfNoAccess = false, + children, + ...buttonProps +}) => { + const { userPermissions, userRoles, isAuthenticated } = usePermissions(); + + const hasRequiredAccess = + isAuthenticated && + hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + + if (!hasRequiredAccess && hideIfNoAccess) { + return null; + } + + return ( + + ); +}; + +/** + * Permission-aware conditional rendering component + * @param {Object} props - Component props + * @param {string[]} props.requiredPermissions - Array of required permissions + * @param {string[]} props.requiredRoles - Array of required roles + * @param {React.ReactNode} props.children - Content to render if user has access + * @param {React.ReactNode} props.fallback - Content to render if user doesn't have access + * @returns {React.Component} - Conditional component + */ +export const PermissionCheck = ({ + requiredPermissions = [], + requiredRoles = [], + children, + fallback = null, +}) => { + const { userPermissions, userRoles, isAuthenticated } = usePermissions(); + + const hasRequiredAccess = + isAuthenticated && + hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + + if (hasRequiredAccess) { + return children; + } + + return fallback; +};