Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4448a5
Merge pull request #6140 from KelvinTegelaar/dev
JohnDuprey Jun 9, 2026
0d8ca9d
Merge pull request #6153 from KelvinTegelaar/dev
JohnDuprey Jun 10, 2026
1e5f450
bookmark cleanup
Zacgoose Jun 16, 2026
3c82750
Warning when converting mailbox that is over 49GB
Zacgoose Jun 16, 2026
ca78e05
multi post action for multiple spo site cleanup
Zacgoose Jun 16, 2026
ff81970
required = true
KelvinTegelaar Jun 17, 2026
6a9fe95
Merge branch 'dev' of https://github.com/KelvinTegelaar/CIPP into dev
KelvinTegelaar Jun 17, 2026
383df0a
repair gdap role mapping action
Zacgoose Jun 17, 2026
0e110a2
Colliding query keys
Zacgoose Jun 17, 2026
61ddc97
Change "All Deviations" labels to "Selected Deviations" to prevent co…
TargetCrafter Jun 17, 2026
495f388
fix: tenant metric grid style
JohnDuprey Jun 18, 2026
5e8a4d9
Update PrivateRoute.js
Zacgoose Jun 18, 2026
f6a0132
Update standards.json
Zacgoose Jun 18, 2026
5426c48
spo version cleanup job check
Zacgoose Jun 18, 2026
ff00dbe
Merge pull request #6190 from TargetCrafter/Clarify-All-Deviations-st…
KelvinTegelaar Jun 18, 2026
a79f410
login page tweaks
Zacgoose Jun 18, 2026
539ef35
login tweaks
Zacgoose Jun 18, 2026
927714e
Sensitivity label fixes
Zacgoose Jun 18, 2026
e9e0147
Update PrivateRoute.js
Zacgoose Jun 18, 2026
cacd076
Update cipp-users.js
Zacgoose Jun 18, 2026
6fa16c5
chore: bump version to 10.5.3
JohnDuprey Jun 18, 2026
95c31b8
Update CIPPDBCacheTypes.json
Zacgoose Jun 19, 2026
2403bd4
Update alerts.json
Zacgoose Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cipp",
"version": "10.5.2",
"version": "10.5.3",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
Expand Down Expand Up @@ -116,4 +116,4 @@
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1"
}
}
}
2 changes: 1 addition & 1 deletion public/version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "10.5.2"
"version": "10.5.3"
}
5 changes: 4 additions & 1 deletion src/components/CippComponents/CippAutocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ export const CippAutoComplete = React.forwardRef((props, ref) => {
const currentTenant = api?.tenantFilter ? api.tenantFilter : useSettings().currentTenant
useEffect(() => {
if (actionGetRequest.isSuccess && !actionGetRequest.isFetching) {
const lastPage = actionGetRequest.data?.pages[actionGetRequest.data.pages.length - 1]
// Guard against a non-paginated cache shape (e.g. when a queryKey is accidentally shared
// with a useQuery/ApiGetCall consumer that stores a plain array instead of { pages }).
const pages = actionGetRequest.data?.pages
const lastPage = Array.isArray(pages) ? pages[pages.length - 1] : undefined
const nextLinkExists = lastPage?.Metadata?.nextLink
if (nextLinkExists) {
actionGetRequest.fetchNextPage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,13 @@ const MODE_CONFIG = {
"Tooltip": "Confidential data, do not share externally",
"Comment": "Internal-only confidential classification",
"ContentType": "File, Email",
"ApplyContentMarkingHeaderEnabled": true,
"ApplyContentMarkingHeaderText": "Confidential - Internal Use Only",
"ApplyContentMarkingHeaderFontColor": "#FF0000",
"EncryptionEnabled": true,
"EncryptionProtectionType": "Template",
"ContentMarkingHeaderEnabled": true,
"ContentMarkingHeaderText": "Confidential - Internal Use Only",
"EncryptionProtectionType": "UserDefined",
"EncryptionPromptUser": true,
"EncryptionDoNotForward": true,
"PolicyParams": {
"Name": "Confidential Label Policy",
"ExchangeLocation": "All",
Expand Down
25 changes: 16 additions & 9 deletions src/components/CippComponents/TenantMetricsGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@ export const TenantMetricsGrid = ({ data, isLoading }) => {
sx={{
display: "flex",
alignItems: "center",
gap: 1.5,
p: 2,
gap: { xs: 1, sm: 1.5 },
p: { xs: 1, sm: 1.5, md: 2 },
border: 1,
borderColor: "divider",
borderRadius: 1,
cursor: "pointer",
minWidth: 0,
transition: "all 0.2s ease-in-out",
"&:hover": {
borderColor: `${metric.color}.main`,
Expand All @@ -103,18 +104,24 @@ export const TenantMetricsGrid = ({ data, isLoading }) => {
sx={{
bgcolor: `${metric.color}.main`,
color: `${metric.color}.contrastText`,
width: 34,
height: 34,
width: { xs: 28, sm: 32, md: 34 },
height: { xs: 28, sm: 32, md: 34 },
flexShrink: 0,
}}
>
<IconComponent sx={{ fontSize: 24, color: "inherit" }} />
<IconComponent sx={{ fontSize: { xs: 18, sm: 22, md: 24 }, color: "inherit" }} />
</Avatar>
<Box>
<Typography variant="caption" color="text.secondary" fontSize="0.7rem">
<Box sx={{ minWidth: 0 }}>
<Typography
variant="caption"
color="text.secondary"
fontSize={{ xs: "0.6rem", sm: "0.65rem", md: "0.7rem" }}
noWrap
>
{metric.label}
</Typography>
<Typography variant="h6" fontSize="1.125rem">
{isLoading ? <Skeleton width={50} /> : formatNumber(metric.value)}
<Typography variant="h6" fontSize={{ xs: "0.9rem", sm: "1rem", md: "1.125rem" }}>
{isLoading ? <Skeleton width={40} /> : formatNumber(metric.value)}
</Typography>
</Box>
</Box>
Expand Down
2 changes: 1 addition & 1 deletion src/components/CippFormPages/CippAddEditUser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const CippAddEditUser = (props) => {
// Get all groups for the tenant
const tenantGroups = ApiGetCall({
url: `/api/ListGroups?tenantFilter=${tenantDomain}`,
queryKey: `ListGroups-${tenantDomain}`,
queryKey: `TenantGroupsList-${tenantDomain}`,
refetchOnMount: false,
refetchOnReconnect: false,
})
Expand Down
76 changes: 71 additions & 5 deletions src/components/CippSettings/CippGDAPResults.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { Alert, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material";
import { Alert, Button, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material";
import { Cancel, CheckCircle, Warning } from "@mui/icons-material";
import { CippPropertyList } from "../CippComponents/CippPropertyList";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { WrenchIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { CippOffCanvas } from "../CippComponents/CippOffCanvas";
import { CippDataTable } from "../CippTable/CippDataTable";
import { ApiPostCall } from "../../api/ApiCall";
import { CippApiResults } from "../CippComponents/CippApiResults";
import { useEffect, useState } from "react";

export const CippGDAPResults = (props) => {
const { executeCheck, offcanvasVisible, setOffcanvasVisible, importReport, setCardIcon } = props;
const [results, setResults] = useState({});

const repairRoleMappings = ApiPostCall({
urlFromData: true,
relatedQueryKeys: ["ExecAccessChecks-GDAP"],
});

const handleRepairRoleMappings = () => {
repairRoleMappings.mutate({
url: "/api/ExecGDAPRepairRoleMappings",
data: {},
queryKey: "RepairGDAPRoleMappings",
});
};

const hasRoleMappingIssues = results?.Results?.RoleMappingResults?.some(
(item) => item?.Status === "Stale" || item?.Status === "Missing",
);

useEffect(() => {
if (importReport) {
setResults(importReport);
Expand All @@ -19,7 +38,11 @@ export const CippGDAPResults = (props) => {
}, [executeCheck, importReport]);

useEffect(() => {
if (results?.Results?.GDAPIssues?.length > 0 || results?.Results?.MissingGroups?.length > 0) {
if (
results?.Results?.GDAPIssues?.length > 0 ||
results?.Results?.MissingGroups?.length > 0 ||
hasRoleMappingIssues
) {
setCardIcon(<Cancel />);
} else {
setCardIcon(<CheckCircle />);
Expand Down Expand Up @@ -77,6 +100,15 @@ export const CippGDAPResults = (props) => {
successMessage: "No Global Admin relationships found",
failureMessage: "Global Admin relationships found",
},
{
resultProperty: "RoleMappingResults",
matchProperty: "Status",
match: "^(Stale|Missing)$",
count: 0,
successMessage: "All GDAP role mappings reference existing security groups",
failureMessage:
"One or more GDAP role mappings reference stale or missing security groups. Click Details to repair.",
},
];

const propertyItems = [
Expand Down Expand Up @@ -154,13 +186,16 @@ export const CippGDAPResults = (props) => {
}}
extendedInfo={[]}
>
{results?.Results?.GDAPIssues?.length > 0 && (
{results?.Results?.GDAPIssues?.filter((issue) => issue.Category !== "RoleMapping")
.length > 0 && (
<>
<CippDataTable
title="GDAP Issues"
isFetching={!importReport && executeCheck?.isFetching}
refreshFunction={executeCheck}
data={results?.Results?.GDAPIssues}
data={results?.Results?.GDAPIssues?.filter(
(issue) => issue.Category !== "RoleMapping",
)}
simpleColumns={["Tenant", "Type", "Issue", "Link"]}
/>
</>
Expand All @@ -178,6 +213,37 @@ export const CippGDAPResults = (props) => {
</>
)}

{results?.Results?.RoleMappingResults?.length > 0 && (
<>
<CippApiResults apiObject={repairRoleMappings} />
<CippDataTable
title="Role Mapping Group Check"
isFetching={!importReport && executeCheck?.isFetching}
refreshFunction={executeCheck}
cardButton={
!importReport &&
hasRoleMappingIssues && (
<Button
variant="contained"
color="primary"
size="small"
onClick={handleRepairRoleMappings}
startIcon={
<SvgIcon fontSize="sm">
<WrenchIcon />
</SvgIcon>
}
>
Repair Role Mappings
</Button>
)
}
data={results?.Results?.RoleMappingResults}
simpleColumns={["RoleName", "GroupName", "GroupId", "Status", "Message"]}
/>
</>
)}

{results?.Results?.Memberships?.filter(
(membership) => membership?.["@odata.type"] === "#microsoft.graph.group",
).length > 0 && (
Expand Down
55 changes: 54 additions & 1 deletion src/components/CippWizard/CippWizardOffboarding.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import CippWizardStepButtons from './CippWizardStepButtons'
import CippFormComponent from '../CippComponents/CippFormComponent'
import { CippFormCondition } from '../CippComponents/CippFormCondition'
import { useWatch } from 'react-hook-form'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Grid } from '@mui/system'
import { useSettings } from '../../hooks/use-settings'
import { ApiGetCall } from '../../api/ApiCall'

// Shared mailboxes are capped at 50 GiB without a license; warn at 49 GiB.
const SHARED_MAILBOX_WARN_BYTES = 49 * 1024 ** 3

export const CippWizardOffboarding = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props
Expand All @@ -24,6 +28,40 @@ export const CippWizardOffboarding = (props) => {
const userSettingsDefaults = useSettings().userSettingsDefaults
const disableForwarding = useWatch({ control: formControl.control, name: 'disableForwarding' })
const deleteUser = useWatch({ control: formControl.control, name: 'DeleteUser' })
const convertToShared = useWatch({ control: formControl.control, name: 'ConvertToShared' })

// Pull cached mailbox sizes (storageUsedInBytes, keyed by UPN) only when relevant
const mailboxUsage = ApiGetCall({
url: '/api/ListMailboxes',
data: { tenantFilter: currentTenant?.value, UseReportDB: true },
queryKey: `OffboardingMailboxUsage-${currentTenant?.value}`,
waiting: !!convertToShared && !!currentTenant?.value && selectedUsers?.length > 0,
})

// Selected mailboxes whose cached size would exceed the shared-mailbox limit
const oversizedMailboxes = useMemo(() => {
if (!convertToShared || !mailboxUsage.isSuccess || !Array.isArray(mailboxUsage.data)) {
return []
}
const selectedUpns = (selectedUsers || []).map((u) =>
(u?.value ?? u)?.toString().toLowerCase(),
)
return mailboxUsage.data
.filter((mb) => {
const upn = mb?.UPN?.toString().toLowerCase()
const bytes = Number(mb?.storageUsedInBytes)
return (
upn &&
selectedUpns.includes(upn) &&
Number.isFinite(bytes) &&
bytes >= SHARED_MAILBOX_WARN_BYTES
)
})
.map((mb) => ({
upn: mb.UPN,
sizeGB: (Number(mb.storageUsedInBytes) / 1024 ** 3).toFixed(1),
}))
}, [convertToShared, mailboxUsage.isSuccess, mailboxUsage.data, selectedUsers])

useEffect(() => {
if (selectedUsers.length >= 3) {
Expand Down Expand Up @@ -383,6 +421,21 @@ export const CippWizardOffboarding = (props) => {
formControl={formControl}
/>
</Box>
{convertToShared && oversizedMailboxes.length > 0 && (
<Alert severity="warning" sx={{ mt: 2 }}>
The following mailbox{oversizedMailboxes.length > 1 ? 'es' : ''} exceed or are near
the 50 GB shared mailbox limit. Converting to shared may fail, or the mailbox may
stop receiving mail once unlicensed, unless an Exchange Online Plan 2 license is
retained:
<Box component="ul" sx={{ mt: 1, mb: 0, pl: 2.5 }}>
{oversizedMailboxes.map((mb) => (
<li key={mb.upn}>
{mb.upn} ({mb.sizeGB} GB)
</li>
))}
</Box>
</Alert>
)}
</CardContent>
</Card>
</Grid>
Expand Down
22 changes: 14 additions & 8 deletions src/components/PrivateRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import LoadingPage from "../pages/loading.js";
import ApiOfflinePage from "../pages/api-offline.js";
import { useState, useEffect } from "react";

// EasyAuth exposes the signed-in identity in two shapes depending on the host:
// - Static Web Apps: { clientPrincipal: { userDetails, userRoles, ... } }
// - App Service EasyAuth: [ { user_id, user_claims: [...], access_token, ... } ]
// an authenticated session must be detected from either populated shape.
const hasAuthenticatedSession = (data) =>
Boolean(data?.clientPrincipal) || (Array.isArray(data) && data.length > 0);

export const PrivateRoute = ({ children, routeType }) => {
const [unauthLatched, setUnauthLatched] = useState(false);

Expand All @@ -14,27 +21,26 @@ export const PrivateRoute = ({ children, routeType }) => {
staleTime: 120000, // 2 minutes
});

// Latch the unauthenticated state so refetches from child components
// don't flip us back to loading. Clear the latch when session succeeds (after login).
// Latch the unauthenticated state so refetches from child components don't flip us
// back to loading. Latch on a request error or a settled session with no identity;
// clear it as soon as an authenticated session (either shape) is seen.
useEffect(() => {
if (
!session.isLoading &&
!session.isFetching &&
(session.isError ||
null === session?.data?.clientPrincipal ||
session?.data === undefined)
(session.isError || !hasAuthenticatedSession(session.data))
) {
setUnauthLatched(true);
} else if (session.isSuccess && session.data?.clientPrincipal) {
} else if (hasAuthenticatedSession(session.data)) {
setUnauthLatched(false);
}
}, [session.isLoading, session.isFetching, session.isError, session.isSuccess, session.data]);
}, [session.isLoading, session.isFetching, session.isError, session.data]);

const apiRoles = ApiGetCall({
url: "/api/me",
queryKey: "authmecipp",
retry: 2,
waiting: session.isSuccess && session.data?.clientPrincipal !== null,
waiting: session.isSuccess && hasAuthenticatedSession(session.data),
});

// If latched as unauthenticated, always show unauthenticated page
Expand Down
5 changes: 5 additions & 0 deletions src/data/CIPPDBCacheTypes.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,5 +328,10 @@
"type": "DetectedApps",
"friendlyName": "Detected Apps",
"description": "All detected applications with devices where each app is installed"
},
{
"type": "IntuneAppInstallStatus",
"friendlyName": "Intune App Install Status",
"description": "Per-application install status rollup (failed/installed/pending device counts) from the AppInstallStatusAggregate report"
}
]
2 changes: 1 addition & 1 deletion src/data/alerts.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@
{
"name": "IntunePolicyConflicts",
"label": "Alert on Intune policy or app conflicts/errors",
"recommendedRunInterval": "4h",
"recommendedRunInterval": "1d",
"requiresInput": true,
"multipleInput": true,
"inputs": [
Expand Down
8 changes: 6 additions & 2 deletions src/data/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -8085,8 +8085,12 @@
{
"type": "number",
"name": "standards.SPOVersionControl.ExpireVersionsAfterDays",
"label": "Expire Versions After Days (0 = never, when auto trim is off)",
"default": 0
"label": "Expire Versions After Days (0 = never, otherwise 30-36500, when auto trim is off)",
"default": 0,
"validators": {
"min": { "value": 0, "message": "Use 0 for never, or 30 or more days" },
"max": { "value": 36500, "message": "Maximum value is 36500" }
}
},
{
"type": "switch",
Expand Down
Loading