From 77bffa9a58737d14f89e96317fca765a3a8794fd Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Fri, 16 May 2025 02:08:27 +0200
Subject: [PATCH 001/865] MSAL magic
---
.../CippComponents/CIPPM365OAuthButton.js | 469 ++++++++++++++++++
.../CippWizard/CIPPDeploymentStep.js | 69 +--
.../CippWizard/CIPPDeploymentUpdateTokens.js | 57 +++
src/pages/onboardingv2.js | 107 ++++
4 files changed, 636 insertions(+), 66 deletions(-)
create mode 100644 src/components/CippComponents/CIPPM365OAuthButton.js
create mode 100644 src/components/CippWizard/CIPPDeploymentUpdateTokens.js
create mode 100644 src/pages/onboardingv2.js
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.js
new file mode 100644
index 000000000000..9d162aba8971
--- /dev/null
+++ b/src/components/CippComponents/CIPPM365OAuthButton.js
@@ -0,0 +1,469 @@
+import { useState } from "react";
+import {
+ Alert,
+ Button,
+ Stack,
+ Typography,
+ CircularProgress,
+ Box,
+} from "@mui/material";
+import { ApiGetCall } from "../../api/ApiCall";
+
+/**
+ * CIPPM365OAuthButton - A reusable button component for Microsoft 365 OAuth authentication
+ *
+ * @param {Object} props - Component props
+ * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data
+ * @param {Function} props.onAuthError - Callback function called when authentication fails with error data
+ * @param {string} props.buttonText - Text to display on the button (default: "Login with Microsoft")
+ * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true)
+ * @param {string} props.scope - OAuth scope to request (default: "https://graph.microsoft.com/.default offline_access profile openid")
+ * @returns {JSX.Element} The CIPPM365OAuthButton component
+ */
+export const CIPPM365OAuthButton = ({
+ onAuthSuccess,
+ onAuthError,
+ buttonText = "Login with Microsoft",
+ showResults = true,
+ scope = "https://graph.microsoft.com/.default offline_access profile openid",
+}) => {
+ const [authInProgress, setAuthInProgress] = useState(false);
+ const [authError, setAuthError] = useState(null);
+ const [tokens, setTokens] = useState({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ // Get application ID information
+ const appId = ApiGetCall({
+ url: `/api/ExecListAppId`,
+ queryKey: `ExecListAppId`,
+ waiting: true,
+ });
+
+ // Handle closing the error
+ const handleCloseError = () => {
+ setAuthError(null);
+ };
+
+ // MSAL-like authentication function
+ const handleMsalAuthentication = () => {
+ // Clear previous authentication state when starting a new authentication
+ setAuthInProgress(true);
+ setAuthError(null);
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ // Generate MSAL-like authentication parameters
+ const msalConfig = {
+ auth: {
+ clientId: appId?.data?.applicationId,
+ authority: `https://login.microsoftonline.com/common`,
+ redirectUri: window.location.origin,
+ },
+ };
+
+ // Define the request object similar to MSAL
+ const loginRequest = {
+ scopes: [scope],
+ };
+
+ console.log("MSAL Config:", msalConfig);
+ console.log("Login Request:", loginRequest);
+
+ // Generate PKCE code verifier and challenge
+ const generateCodeVerifier = () => {
+ const array = new Uint8Array(32);
+ window.crypto.getRandomValues(array);
+ return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join("");
+ };
+
+ const base64URLEncode = (str) => {
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+ };
+
+ // Generate code verifier for PKCE
+ const codeVerifier = generateCodeVerifier();
+ // In a real implementation, we would hash the code verifier to create the code challenge
+ // For simplicity, we'll use the same value
+ const codeChallenge = codeVerifier;
+
+ // Note: We're not storing the code verifier in session storage for security reasons
+ // Instead, we'll use it directly in the token exchange
+
+ // Create a random state value for security
+ const state = Math.random().toString(36).substring(2, 15);
+
+ // Create the auth URL with PKCE parameters
+ const authUrl =
+ `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
+ `client_id=${appId?.data?.applicationId}` +
+ `&response_type=code` +
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}` +
+ `&scope=${encodeURIComponent(scope)}` +
+ `&code_challenge=${codeChallenge}` +
+ `&code_challenge_method=plain` +
+ `&state=${state}` +
+ `&prompt=select_account`;
+
+ console.log("MSAL Auth URL:", authUrl);
+
+ // Open popup for authentication
+ const width = 500;
+ const height = 600;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+
+ const popup = window.open(
+ authUrl,
+ "msalAuthPopup",
+ `width=${width},height=${height},left=${left},top=${top}`
+ );
+
+ // Function to actually exchange the authorization code for tokens
+ const handleAuthorizationCode = async (code, receivedState) => {
+ // Verify the state parameter matches what we sent (security check)
+ if (receivedState !== state) {
+ const errorMessage = "State mismatch in auth response - possible CSRF attack";
+ console.error(errorMessage);
+ const error = {
+ errorCode: "state_mismatch",
+ errorMessage: errorMessage,
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+ setAuthInProgress(false);
+ return;
+ }
+
+ console.log("Authorization code received:", code);
+
+ try {
+ // Actually exchange the code for tokens using the token endpoint
+ console.log("Exchanging authorization code for tokens...");
+
+ // Prepare the token request
+ const tokenRequest = {
+ grant_type: "authorization_code",
+ client_id: appId?.data?.applicationId,
+ code: code,
+ redirect_uri: window.location.origin,
+ code_verifier: codeVerifier,
+ };
+
+ // Make the token request
+ const tokenResponse = await fetch(
+ `https://login.microsoftonline.com/common/oauth2/v2.0/token`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams(tokenRequest).toString(),
+ }
+ );
+
+ // Parse the token response
+ const tokenData = await tokenResponse.json();
+
+ if (tokenResponse.ok) {
+ // Extract token information
+ const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000);
+ // Refresh tokens typically last for 90 days, but this can vary
+ // For demonstration, we'll set it to 90 days from now
+ const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
+
+ // Extract information from ID token if available
+ let username = "unknown user";
+ let tenantId = appId?.data?.tenantId || "unknown tenant";
+ let onmicrosoftDomain = null;
+
+ if (tokenData.id_token) {
+ try {
+ const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1]));
+
+ // Extract username
+ username =
+ idTokenPayload.preferred_username ||
+ idTokenPayload.email ||
+ idTokenPayload.upn ||
+ idTokenPayload.name ||
+ "unknown user";
+
+ // Extract tenant ID if available in the token
+ if (idTokenPayload.tid) {
+ tenantId = idTokenPayload.tid;
+ }
+
+ // Try to extract onmicrosoft domain from the username or issuer
+ if (username && username.includes("@") && username.includes(".onmicrosoft.com")) {
+ onmicrosoftDomain = username.split("@")[1];
+ } else if (idTokenPayload.iss) {
+ const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//);
+ if (issuerMatch && issuerMatch[1]) {
+ // We have the tenant ID, but not the domain name
+ // We could potentially make an API call to get the domain, but for now we'll leave it null
+ }
+ }
+ } catch (error) {
+ console.error("Error parsing ID token:", error);
+ }
+ }
+
+ // Create token result object
+ const tokenResult = {
+ accessToken: tokenData.access_token,
+ refreshToken: tokenData.refresh_token,
+ accessTokenExpiresOn: accessTokenExpiresOn,
+ refreshTokenExpiresOn: refreshTokenExpiresOn,
+ username: username,
+ tenantId: tenantId,
+ onmicrosoftDomain: onmicrosoftDomain,
+ };
+
+ // Store tokens in component state
+ setTokens(tokenResult);
+
+ // Log only the necessary token information to console
+ console.log("Access Token:", tokenData.access_token);
+ console.log("Refresh Token:", tokenData.refresh_token);
+
+ // Call the onAuthSuccess callback if provided
+ if (onAuthSuccess) onAuthSuccess(tokenResult);
+ } else {
+ // Handle token error - display in error box instead of throwing
+ const error = {
+ errorCode: tokenData.error || "token_error",
+ errorMessage:
+ tokenData.error_description || "Failed to exchange authorization code for tokens",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+ }
+ } catch (error) {
+ console.error("Error exchanging code for tokens:", error);
+ const errorObj = {
+ errorCode: "token_exchange_error",
+ errorMessage: error.message || "Failed to exchange authorization code for tokens",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(errorObj);
+ if (onAuthError) onAuthError(errorObj);
+ } finally {
+ // Close the popup window if it's still open
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+
+ // Update UI state
+ setAuthInProgress(false);
+ }
+ };
+
+ // Monitor for the redirect with the authorization code
+ // This is what MSAL does internally
+ const checkPopupLocation = setInterval(() => {
+ if (!popup || popup.closed) {
+ clearInterval(checkPopupLocation);
+
+ // If authentication is still in progress when popup closes, it's an error
+ if (authInProgress) {
+ const errorMessage = "Authentication was cancelled. Please try again.";
+ console.error(errorMessage);
+ const error = {
+ errorCode: "user_cancelled",
+ errorMessage: errorMessage,
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+
+ // Ensure we're not showing any previous success state
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+ }
+
+ setAuthInProgress(false);
+ return;
+ }
+
+ try {
+ // Try to access the popup location to check for the authorization code
+ const currentUrl = popup.location.href;
+
+ // Check if the URL contains a code parameter (authorization code)
+ if (currentUrl.includes("code=") && currentUrl.includes("state=")) {
+ clearInterval(checkPopupLocation);
+
+ console.log("Detected authorization code in URL:", currentUrl);
+
+ // Parse the URL to extract the code and state
+ const urlParams = new URLSearchParams(popup.location.search);
+ const code = urlParams.get("code");
+ const receivedState = urlParams.get("state");
+
+ // Process the authorization code
+ handleAuthorizationCode(code, receivedState);
+ }
+
+ // Check for error in the URL
+ if (currentUrl.includes("error=")) {
+ clearInterval(checkPopupLocation);
+
+ console.error("Detected error in authentication response:", currentUrl);
+
+ // Parse the URL to extract the error details
+ const urlParams = new URLSearchParams(popup.location.search);
+ const errorCode = urlParams.get("error");
+ const errorDescription = urlParams.get("error_description");
+
+ // Set the error state
+ const error = {
+ errorCode: errorCode,
+ errorMessage: errorDescription || "Unknown authentication error",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+
+ // Close the popup
+ popup.close();
+ setAuthInProgress(false);
+ }
+ } catch (error) {
+ // This will throw an error when the popup is on a different domain
+ // due to cross-origin restrictions, which is normal during auth flow
+ // Just continue monitoring
+ }
+ }, 500);
+
+ // Also monitor for popup closing as a fallback
+ const checkPopupClosed = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(checkPopupClosed);
+ clearInterval(checkPopupLocation);
+
+ // If authentication is still in progress when popup closes, it's an error
+ if (authInProgress) {
+ const errorMessage = "Authentication was cancelled. Please try again.";
+ console.error(errorMessage);
+ const error = {
+ errorCode: "user_cancelled",
+ errorMessage: errorMessage,
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+
+ // Ensure we're not showing any previous success state
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+ }
+
+ setAuthInProgress(false);
+ }
+ }, 1000);
+ };
+
+ return (
+
+
+ {authInProgress ? (
+ <>
+
+ Authenticating...
+ >
+ ) : (
+ buttonText
+ )}
+
+
+ {!appId.isLoading &&
+ !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
+ appId?.data?.applicationId
+ ) && (
+
+ The Application ID is not valid. Please check your configuration.
+
+ )
+ }
+
+ {showResults && (
+
+ {tokens.accessToken ? (
+
+ Authentication Successful
+
+ You've successfully refreshed your token. The account you're using for authentication
+ is: {tokens.username}
+
+
+ Tenant ID: {tokens.tenantId}
+ {tokens.onmicrosoftDomain && (
+ <> | Domain: {tokens.onmicrosoftDomain} >
+ )}
+
+
+ Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()}
+
+
+ ) : authError ? (
+
+ Authentication Error: {authError.errorCode}
+ {authError.errorMessage}
+
+ Time: {authError.timestamp}
+
+
+
+ Dismiss
+
+
+
+ ) : null}
+
+ )}
+
+ );
+};
+
+export default CIPPM365OAuthButton;
\ No newline at end of file
diff --git a/src/components/CippWizard/CIPPDeploymentStep.js b/src/components/CippWizard/CIPPDeploymentStep.js
index 070b38811dbf..6ecb6d560a4e 100644
--- a/src/components/CippWizard/CIPPDeploymentStep.js
+++ b/src/components/CippWizard/CIPPDeploymentStep.js
@@ -16,10 +16,11 @@ import { CippWizardStepButtons } from "./CippWizardStepButtons";
import { ApiGetCall } from "../../api/ApiCall";
import CippButtonCard from "../CippCards/CippButtonCard";
import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard";
-import { CheckCircle, OpenInNew, Sync } from "@mui/icons-material";
+import { CheckCircle } from "@mui/icons-material";
import CippPermissionCheck from "../CippSettings/CippPermissionCheck";
import { useQueryClient } from "@tanstack/react-query";
import { CippApiResults } from "../CippComponents/CippApiResults";
+import { CIPPDeploymentUpdateTokens } from "./CIPPDeploymentUpdateTokens";
export const CippDeploymentStep = (props) => {
const queryClient = useQueryClient();
@@ -40,11 +41,6 @@ export const CippDeploymentStep = (props) => {
queryKey: `checkSetupStep${pollingStep}`,
waiting: !pollingStep,
});
- const appId = ApiGetCall({
- url: `/api/ExecListAppId`,
- queryKey: `ExecListAppId`,
- waiting: true,
- });
useEffect(() => {
if (
startSetupApi.data &&
@@ -237,66 +233,7 @@ export const CippDeploymentStep = (props) => {
)}
{values.selectedOption === "UpdateTokens" && (
-
- Update Tokens
-
- {appId.isLoading ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- CardButton={
- <>
- openPopup(appId?.data?.refreshUrl)}
- color="primary"
- startIcon={
-
- }
- >
- Refresh Graph Token
-
- appId.refetch()}
- variant="outlined"
- color="primary"
- startIcon={ }
- disabled={appId.isFetching}
- >
- Check Application ID
-
- {!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
- appId?.data?.applicationId
- ) && (
-
- The Application ID is not valid. Please return to the first page of the SAM
- wizard and use the Manual .
-
- )}
- >
- }
- >
-
- Click the button below to refresh your token.
-
- {formControl.setValue("noSubmitButton", true)}
-
-
+
)}
{values.selectedOption === "Manual" && (
diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
new file mode 100644
index 000000000000..099fcef8a39a
--- /dev/null
+++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
@@ -0,0 +1,57 @@
+import { useState } from "react";
+import { Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material";
+import { CheckCircle } from "@mui/icons-material";
+import CippButtonCard from "../CippCards/CippButtonCard";
+import { ApiGetCall } from "../../api/ApiCall";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
+
+export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
+ const values = formControl.getValues();
+ const [tokens, setTokens] = useState(null);
+
+ // Get application ID information for the card header
+ const appId = ApiGetCall({
+ url: `/api/ExecListAppId`,
+ queryKey: `ExecListAppId`,
+ waiting: true,
+ });
+
+ // Handle successful authentication
+ const handleAuthSuccess = (tokenData) => {
+ setTokens(tokenData);
+ console.log("Token data received:", tokenData);
+ };
+
+ return (
+
+ Update Tokens (MSAL Style)
+
+ {appId.isLoading ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ }
+ CardButton={
+
+ }
+ >
+
+ Click the button to refresh the Graph token for your tenants. We should write some text here
+ for replacing token for partner tenant vs client tenant.
+
+ {formControl.setValue("noSubmitButton", true)}
+
+
+ );
+};
+
+export default CIPPDeploymentUpdateTokens;
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
new file mode 100644
index 000000000000..4de309be07ae
--- /dev/null
+++ b/src/pages/onboardingv2.js
@@ -0,0 +1,107 @@
+import { Layout as DashboardLayout } from "../layouts/index.js";
+import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.js";
+import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.js";
+import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx";
+import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx";
+import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
+
+const Page = () => {
+ const steps = [
+ {
+ title: "Step 1",
+ description: "Onboarding",
+ component: CippWizardOptionsList,
+ componentProps: {
+ title: "Select your setup method",
+ subtext: `This wizard will guide you through setting up CIPPs access to your client tenants. If this is your first time setting up CIPP you will want to choose the option "Create application for me and connect to my tenants",`,
+ valuesKey: "SyncTool",
+ options: [
+ {
+ description:
+ "Choose this option if this is your first setup, or if you'd like to redo the previous setup.",
+ icon: ,
+ label: "First Setup",
+ value: "FirstSetup",
+ },
+ {
+ description:
+ "Choose this option if you would like to add a tenant to your environment.",
+ icon: ,
+ label: "Add a tenant",
+ value: "AddTenant",
+ },
+ {
+ description:
+ "Choose this option if you want to setup which application registration is used to connect to your tenants.",
+ icon: ,
+ label: "Create a new application registration for me and connect to my tenants",
+ value: "CreateApp",
+ },
+ {
+ description: "I would like to refresh my token or replace the account I've used.",
+ icon: ,
+ label: "Refresh Tokens for existing application registration",
+ value: "UpdateTokens",
+ },
+ {
+ description:
+ "I have an existing application and would like to manually enter my token, or update them. This is only recommended for advanced users.",
+ icon: ,
+ label: "Manually enter credentials",
+ value: "Manual",
+ },
+ ],
+ },
+ },
+ {
+ title: "Step 2",
+ description: "Application",
+ component: CippDeploymentStep,
+ },
+ {
+ title: "Step 3",
+ description: "Tenants",
+ component: CippDeploymentStep,
+ },
+ {
+ title: "Step 4",
+ description: "Baselines",
+ component: CippDeploymentStep,
+ },
+ {
+ title: "Step 5",
+ description: "Integrations",
+ component: CippDeploymentStep,
+ },
+ {
+ title: "Step 6",
+ description: "Notifications",
+ component: CippDeploymentStep,
+ },
+ {
+ title: "Step 7",
+ description: "Alerts",
+ component: CippDeploymentStep,
+ },
+ {
+ title: "Step 8",
+ description: "Confirmation",
+ component: CippWizardConfirmation,
+ },
+ ];
+
+ return (
+ <>
+
+ >
+ );
+};
+
+Page.getLayout = (page) => {page} ;
+
+export default Page;
From 1a50daf3164e59da7274f8ee5cdfa527c0a3c3b4 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Fri, 16 May 2025 02:42:03 +0200
Subject: [PATCH 002/865] Experimentation with new MSAL and creation
---
.../CippComponents/CIPPDeviceCodeButton.js | 253 ++++++++++++++
.../CippComponents/CIPPM365OAuthButton.js | 326 +++++++++++++-----
.../CippWizard/CIPPDeploymentUpdateTokens.js | 90 +++--
3 files changed, 566 insertions(+), 103 deletions(-)
create mode 100644 src/components/CippComponents/CIPPDeviceCodeButton.js
diff --git a/src/components/CippComponents/CIPPDeviceCodeButton.js b/src/components/CippComponents/CIPPDeviceCodeButton.js
new file mode 100644
index 000000000000..e262b69c7912
--- /dev/null
+++ b/src/components/CippComponents/CIPPDeviceCodeButton.js
@@ -0,0 +1,253 @@
+import { useState, useEffect } from "react";
+import {
+ Alert,
+ Button,
+ Stack,
+ Typography,
+ CircularProgress,
+ Box,
+} from "@mui/material";
+import { ApiGetCall, ApiPostCall } from "../../api/ApiCall";
+
+/**
+ * CIPPDeviceCodeButton - A button component for Microsoft 365 OAuth authentication using device code flow
+ *
+ * @param {Object} props - Component props
+ * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data
+ * @param {Function} props.onAuthError - Callback function called when authentication fails with error data
+ * @param {string} props.buttonText - Text to display on the button (default: "Login with Device Code")
+ * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true)
+ * @returns {JSX.Element} The CIPPDeviceCodeButton component
+ */
+export const CIPPDeviceCodeButton = ({
+ onAuthSuccess,
+ onAuthError,
+ buttonText = "Login with Device Code",
+ showResults = true,
+}) => {
+ const [authInProgress, setAuthInProgress] = useState(false);
+ const [authError, setAuthError] = useState(null);
+ const [deviceCodeInfo, setDeviceCodeInfo] = useState(null);
+ const [currentStep, setCurrentStep] = useState(0);
+ const [pollInterval, setPollInterval] = useState(null);
+ const [tokens, setTokens] = useState({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ // Get application ID information from API
+ const appIdInfo = ApiGetCall({
+ url: `/api/ExecListAppId`,
+ queryKey: `ExecListAppId`,
+ waiting: true,
+ });
+
+ // Handle closing the error
+ const handleCloseError = () => {
+ setAuthError(null);
+ };
+
+ // Clear polling interval when component unmounts
+ useEffect(() => {
+ return () => {
+ if (pollInterval) {
+ clearInterval(pollInterval);
+ }
+ };
+ }, [pollInterval]);
+
+ // Start device code authentication
+ const startDeviceCodeAuth = async () => {
+ try {
+ setAuthInProgress(true);
+ setAuthError(null);
+ setDeviceCodeInfo(null);
+ setCurrentStep(1);
+
+ // Call the API to start device code flow
+ const response = await fetch(`/api/ExecSAMSetup?CreateSAM=true`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.code) {
+ // Store device code info
+ setDeviceCodeInfo({
+ user_code: data.code,
+ verification_uri: data.url,
+ expires_in: 900, // Default to 15 minutes if not provided
+ });
+
+ // Start polling for token
+ const interval = setInterval(checkAuthStatus, 5000);
+ setPollInterval(interval);
+ } else {
+ // Error getting device code
+ setAuthError({
+ errorCode: "device_code_error",
+ errorMessage: data.message || "Failed to get device code",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ if (onAuthError) onAuthError(error);
+ }
+ } catch (error) {
+ console.error("Error starting device code authentication:", error);
+ setAuthError({
+ errorCode: "device_code_error",
+ errorMessage: error.message || "An error occurred during device code authentication",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ if (onAuthError) onAuthError(error);
+ }
+ };
+
+ // Check authentication status
+ const checkAuthStatus = async () => {
+ try {
+ // Call the API to check auth status
+ const response = await fetch(`/api/ExecSAMSetup?CheckSetupProcess=true&step=1`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ if (data.step === 2) {
+ // Authentication successful
+ clearInterval(pollInterval);
+ setPollInterval(null);
+
+ // Process token data
+ const tokenData = {
+ accessToken: "Successfully authenticated",
+ refreshToken: "Token stored on server",
+ accessTokenExpiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour from now
+ refreshTokenExpiresOn: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
+ username: "authenticated user",
+ tenantId: data.tenantId || "unknown",
+ onmicrosoftDomain: null,
+ };
+
+ // Store tokens in component state
+ setTokens(tokenData);
+ setDeviceCodeInfo(null);
+ setCurrentStep(2);
+
+ // Call the onAuthSuccess callback if provided
+ if (onAuthSuccess) onAuthSuccess(tokenData);
+
+ // Update UI state
+ setAuthInProgress(false);
+ }
+ } else {
+ // Error checking auth status
+ clearInterval(pollInterval);
+ setPollInterval(null);
+
+ setAuthError({
+ errorCode: "auth_status_error",
+ errorMessage: data.message || "Failed to check authentication status",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ if (onAuthError) onAuthError({
+ errorCode: "auth_status_error",
+ errorMessage: data.message || "Failed to check authentication status",
+ timestamp: new Date().toISOString(),
+ });
+ }
+ } catch (error) {
+ console.error("Error checking auth status:", error);
+ // Don't stop polling on transient errors
+ }
+ };
+
+ return (
+
+
+ {authInProgress ? (
+ <>
+
+ Authenticating...
+ >
+ ) : (
+ buttonText
+ )}
+
+
+ {!appIdInfo.isLoading &&
+ !appIdInfo?.data?.applicationId && (
+
+ The Application ID is not valid. Please check your configuration.
+
+ )
+ }
+
+ {showResults && (
+
+ {deviceCodeInfo && authInProgress ? (
+
+ Device Code Authentication
+
+ To sign in, use a web browser to open the page {deviceCodeInfo.verification_uri} and enter the code {deviceCodeInfo.user_code} to authenticate.
+
+
+ Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes
+
+
+ ) : tokens.accessToken ? (
+
+ Authentication Successful
+
+ You've successfully refreshed your token using device code flow.
+
+ {tokens.tenantId && (
+
+ Tenant ID: {tokens.tenantId}
+
+ )}
+
+ ) : authError ? (
+
+ Authentication Error: {authError.errorCode}
+ {authError.errorMessage}
+
+ Time: {authError.timestamp}
+
+
+
+ Dismiss
+
+
+
+ ) : null}
+
+ )}
+
+ );
+};
+
+export default CIPPDeviceCodeButton;
\ No newline at end of file
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.js
index 9d162aba8971..9c1e3ae58675 100644
--- a/src/components/CippComponents/CIPPM365OAuthButton.js
+++ b/src/components/CippComponents/CIPPM365OAuthButton.js
@@ -8,16 +8,19 @@ import {
Box,
} from "@mui/material";
import { ApiGetCall } from "../../api/ApiCall";
+import { CippCopyToClipBoard } from "./CippCopyToClipboard";
/**
* CIPPM365OAuthButton - A reusable button component for Microsoft 365 OAuth authentication
- *
+ *
* @param {Object} props - Component props
* @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data
* @param {Function} props.onAuthError - Callback function called when authentication fails with error data
* @param {string} props.buttonText - Text to display on the button (default: "Login with Microsoft")
* @param {boolean} props.showResults - Whether to show authentication results in the component (default: true)
* @param {string} props.scope - OAuth scope to request (default: "https://graph.microsoft.com/.default offline_access profile openid")
+ * @param {boolean} props.useDeviceCode - Whether to use device code flow instead of popup (default: false)
+ * @param {string} props.applicationId - Application ID to use for authentication (default: uses the one from API)
* @returns {JSX.Element} The CIPPM365OAuthButton component
*/
export const CIPPM365OAuthButton = ({
@@ -26,9 +29,12 @@ export const CIPPM365OAuthButton = ({
buttonText = "Login with Microsoft",
showResults = true,
scope = "https://graph.microsoft.com/.default offline_access profile openid",
+ useDeviceCode = false,
+ applicationId = null,
}) => {
const [authInProgress, setAuthInProgress] = useState(false);
const [authError, setAuthError] = useState(null);
+ const [deviceCodeInfo, setDeviceCodeInfo] = useState(null);
const [tokens, setTokens] = useState({
accessToken: null,
refreshToken: null,
@@ -39,8 +45,8 @@ export const CIPPM365OAuthButton = ({
onmicrosoftDomain: null,
});
- // Get application ID information
- const appId = ApiGetCall({
+ // Get application ID information from API if not provided
+ const appIdInfo = ApiGetCall({
url: `/api/ExecListAppId`,
queryKey: `ExecListAppId`,
waiting: true,
@@ -51,6 +57,216 @@ export const CIPPM365OAuthButton = ({
setAuthError(null);
};
+ // Device code authentication function
+ const handleDeviceCodeAuthentication = async () => {
+ setAuthInProgress(true);
+ setAuthError(null);
+ setDeviceCodeInfo(null);
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ try {
+ // Get the application ID to use
+ const appId = applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID
+
+ // Request device code from our API endpoint
+ const deviceCodeResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(scope)}`);
+ const deviceCodeData = await deviceCodeResponse.json();
+
+ if (deviceCodeResponse.ok && deviceCodeData.user_code) {
+ // Store device code info
+ setDeviceCodeInfo(deviceCodeData);
+
+ // Open popup to device login page
+ const width = 500;
+ const height = 600;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+
+ const popup = window.open(
+ "https://microsoft.com/devicelogin",
+ "deviceLoginPopup",
+ `width=${width},height=${height},left=${left},top=${top}`
+ );
+
+ // Start polling for token
+ const pollInterval = deviceCodeData.interval || 5;
+ const expiresIn = deviceCodeData.expires_in || 900;
+ const startTime = Date.now();
+
+ const pollForToken = async () => {
+ // Check if popup was closed
+ if (popup && popup.closed) {
+ clearInterval(checkPopupClosed);
+ setAuthError({
+ errorCode: "user_cancelled",
+ errorMessage: "Authentication was cancelled. Please try again.",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ return;
+ }
+
+ // Check if we've exceeded the expiration time
+ if (Date.now() - startTime >= expiresIn * 1000) {
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ setAuthError({
+ errorCode: "timeout",
+ errorMessage: "Device code authentication timed out",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ return;
+ }
+
+ try {
+ // Poll for token using our API endpoint
+ const tokenResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}`);
+ const tokenData = await tokenResponse.json();
+
+ if (tokenResponse.ok && tokenData.status === "success") {
+ // Successfully got token
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ handleTokenResponse(tokenData);
+ } else if (tokenData.error === 'authorization_pending' || tokenData.status === "pending") {
+ // User hasn't completed authentication yet, continue polling
+ setTimeout(pollForToken, pollInterval * 1000);
+ } else if (tokenData.error === 'slow_down') {
+ // Server asking us to slow down polling
+ setTimeout(pollForToken, (pollInterval + 5) * 1000);
+ } else {
+ // Other error
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ setAuthError({
+ errorCode: tokenData.error || "token_error",
+ errorMessage: tokenData.error_description || "Failed to get token",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ }
+ } catch (error) {
+ console.error("Error polling for token:", error);
+ setTimeout(pollForToken, pollInterval * 1000);
+ }
+ };
+
+ // Also monitor for popup closing as a fallback
+ const checkPopupClosed = setInterval(() => {
+ if (popup && popup.closed) {
+ clearInterval(checkPopupClosed);
+ setAuthInProgress(false);
+ setAuthError({
+ errorCode: "user_cancelled",
+ errorMessage: "Authentication was cancelled. Please try again.",
+ timestamp: new Date().toISOString(),
+ });
+ }
+ }, 1000);
+
+ // Start polling
+ setTimeout(pollForToken, pollInterval * 1000);
+ } else {
+ // Error getting device code
+ setAuthError({
+ errorCode: deviceCodeData.error || "device_code_error",
+ errorMessage: deviceCodeData.error_description || "Failed to get device code",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ }
+ } catch (error) {
+ console.error("Error in device code authentication:", error);
+ setAuthError({
+ errorCode: "device_code_error",
+ errorMessage: error.message || "An error occurred during device code authentication",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ }
+ };
+
+ // Process token response (common for both auth methods)
+ const handleTokenResponse = (tokenData) => {
+ // Extract token information
+ const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000);
+ // Refresh tokens typically last for 90 days, but this can vary
+ const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
+
+ // Extract information from ID token if available
+ let username = "unknown user";
+ let tenantId = "unknown tenant";
+ let onmicrosoftDomain = null;
+
+ if (tokenData.id_token) {
+ try {
+ const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1]));
+
+ // Extract username
+ username =
+ idTokenPayload.preferred_username ||
+ idTokenPayload.email ||
+ idTokenPayload.upn ||
+ idTokenPayload.name ||
+ "unknown user";
+
+ // Extract tenant ID if available in the token
+ if (idTokenPayload.tid) {
+ tenantId = idTokenPayload.tid;
+ }
+
+ // Try to extract onmicrosoft domain from the username or issuer
+ if (username && username.includes("@") && username.includes(".onmicrosoft.com")) {
+ onmicrosoftDomain = username.split("@")[1];
+ } else if (idTokenPayload.iss) {
+ const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//);
+ if (issuerMatch && issuerMatch[1]) {
+ // We have the tenant ID, but not the domain name
+ }
+ }
+ } catch (error) {
+ console.error("Error parsing ID token:", error);
+ }
+ }
+
+ // Create token result object
+ const tokenResult = {
+ accessToken: tokenData.access_token,
+ refreshToken: tokenData.refresh_token,
+ accessTokenExpiresOn: accessTokenExpiresOn,
+ refreshTokenExpiresOn: refreshTokenExpiresOn,
+ username: username,
+ tenantId: tenantId,
+ onmicrosoftDomain: onmicrosoftDomain,
+ };
+
+ // Store tokens in component state
+ setTokens(tokenResult);
+ setDeviceCodeInfo(null);
+
+ // Log only the necessary token information to console
+ console.log("Access Token:", tokenData.access_token);
+ console.log("Refresh Token:", tokenData.refresh_token);
+
+ // Call the onAuthSuccess callback if provided
+ if (onAuthSuccess) onAuthSuccess(tokenResult);
+
+ // Update UI state
+ setAuthInProgress(false);
+ };
+
// MSAL-like authentication function
const handleMsalAuthentication = () => {
// Clear previous authentication state when starting a new authentication
@@ -66,10 +282,13 @@ export const CIPPM365OAuthButton = ({
onmicrosoftDomain: null,
});
+ // Get the application ID to use
+ const appId = applicationId || appIdInfo?.data?.applicationId;
+
// Generate MSAL-like authentication parameters
const msalConfig = {
auth: {
- clientId: appId?.data?.applicationId,
+ clientId: appId,
authority: `https://login.microsoftonline.com/common`,
redirectUri: window.location.origin,
},
@@ -109,7 +328,7 @@ export const CIPPM365OAuthButton = ({
// Create the auth URL with PKCE parameters
const authUrl =
`https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
- `client_id=${appId?.data?.applicationId}` +
+ `client_id=${appId}` +
`&response_type=code` +
`&redirect_uri=${encodeURIComponent(window.location.origin)}` +
`&scope=${encodeURIComponent(scope)}` +
@@ -158,7 +377,7 @@ export const CIPPM365OAuthButton = ({
// Prepare the token request
const tokenRequest = {
grant_type: "authorization_code",
- client_id: appId?.data?.applicationId,
+ client_id: appId,
code: code,
redirect_uri: window.location.origin,
code_verifier: codeVerifier,
@@ -180,69 +399,7 @@ export const CIPPM365OAuthButton = ({
const tokenData = await tokenResponse.json();
if (tokenResponse.ok) {
- // Extract token information
- const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000);
- // Refresh tokens typically last for 90 days, but this can vary
- // For demonstration, we'll set it to 90 days from now
- const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
-
- // Extract information from ID token if available
- let username = "unknown user";
- let tenantId = appId?.data?.tenantId || "unknown tenant";
- let onmicrosoftDomain = null;
-
- if (tokenData.id_token) {
- try {
- const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1]));
-
- // Extract username
- username =
- idTokenPayload.preferred_username ||
- idTokenPayload.email ||
- idTokenPayload.upn ||
- idTokenPayload.name ||
- "unknown user";
-
- // Extract tenant ID if available in the token
- if (idTokenPayload.tid) {
- tenantId = idTokenPayload.tid;
- }
-
- // Try to extract onmicrosoft domain from the username or issuer
- if (username && username.includes("@") && username.includes(".onmicrosoft.com")) {
- onmicrosoftDomain = username.split("@")[1];
- } else if (idTokenPayload.iss) {
- const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//);
- if (issuerMatch && issuerMatch[1]) {
- // We have the tenant ID, but not the domain name
- // We could potentially make an API call to get the domain, but for now we'll leave it null
- }
- }
- } catch (error) {
- console.error("Error parsing ID token:", error);
- }
- }
-
- // Create token result object
- const tokenResult = {
- accessToken: tokenData.access_token,
- refreshToken: tokenData.refresh_token,
- accessTokenExpiresOn: accessTokenExpiresOn,
- refreshTokenExpiresOn: refreshTokenExpiresOn,
- username: username,
- tenantId: tenantId,
- onmicrosoftDomain: onmicrosoftDomain,
- };
-
- // Store tokens in component state
- setTokens(tokenResult);
-
- // Log only the necessary token information to console
- console.log("Access Token:", tokenData.access_token);
- console.log("Refresh Token:", tokenData.refresh_token);
-
- // Call the onAuthSuccess callback if provided
- if (onAuthSuccess) onAuthSuccess(tokenResult);
+ handleTokenResponse(tokenData);
} else {
// Handle token error - display in error box instead of throwing
const error = {
@@ -398,13 +555,13 @@ export const CIPPM365OAuthButton = ({
{authInProgress ? (
@@ -417,9 +574,9 @@ export const CIPPM365OAuthButton = ({
)}
- {!appId.isLoading &&
+ {!applicationId && !appIdInfo.isLoading &&
!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
- appId?.data?.applicationId
+ appIdInfo?.data?.applicationId
) && (
The Application ID is not valid. Please check your configuration.
@@ -429,7 +586,22 @@ export const CIPPM365OAuthButton = ({
{showResults && (
- {tokens.accessToken ? (
+ {deviceCodeInfo && authInProgress ? (
+
+ Device Code Authentication
+
+ A popup window has been opened to microsoft.com/devicelogin .
+ Enter this code to authenticate:
+
+
+ If the popup was blocked or you closed it, you can also go to microsoft.com/devicelogin manually
+ and enter the code shown above.
+
+
+ Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes
+
+
+ ) : tokens.accessToken ? (
Authentication Successful
diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
index 099fcef8a39a..14f57cf99202 100644
--- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
+++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
@@ -24,33 +24,71 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
};
return (
-
- Update Tokens (MSAL Style)
-
- {appId.isLoading ? (
-
- ) : (
-
-
-
- )}
+
+
+ Update Tokens (MSAL Style)
+
+ {appId.isLoading ? (
+
+ ) : (
+
+
+
+ )}
+
-
- }
- CardButton={
-
- }
- >
-
- Click the button to refresh the Graph token for your tenants. We should write some text here
- for replacing token for partner tenant vs client tenant.
-
- {formControl.setValue("noSubmitButton", true)}
-
-
+ }
+ CardButton={
+
+ }
+ >
+
+ Click the button to refresh the Graph token for your tenants using popup authentication.
+ This method opens a popup window where you can sign in to your Microsoft account.
+
+ {formControl.setValue("noSubmitButton", true)}
+
+
+
+
+ Update Tokens (Device Code Flow)
+
+ {appId.isLoading ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ }
+ CardButton={
+
+ }
+ >
+
+ Click the button to refresh the Graph token using Device Code Flow. This will open a popup
+ to microsoft.com/devicelogin where you can enter the provided code to authenticate. This
+ method is useful when regular popup authentication fails or when you need to authenticate
+ from a different device than the one running CIPP.
+
+
+
);
};
From 6b8f38c432f9087e4089708e99b04b0b007a96db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 16 May 2025 19:11:39 +0200
Subject: [PATCH 003/865] Set 'creatable' to false for some autoCompletes
---
.../tenant/administration/alert-configuration/alert.jsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx
index 41f09ddd481b..6cff52e95ff7 100644
--- a/src/pages/tenant/administration/alert-configuration/alert.jsx
+++ b/src/pages/tenant/administration/alert-configuration/alert.jsx
@@ -399,6 +399,7 @@ const AlertWizard = () => {
type="autoComplete"
name="logbook"
multiple={false}
+ creatable={false}
formControl={formControl}
validators={{
required: { value: true, message: "This field is required" },
@@ -503,7 +504,8 @@ const AlertWizard = () => {
required: { value: true, message: "This field is required" },
}}
formControl={formControl}
- multiple
+ multiple={true}
+ creatable={false}
options={actionstoTake}
/>
@@ -574,6 +576,7 @@ const AlertWizard = () => {
type="autoComplete"
validators={{ required: true }}
multiple={false}
+ creatable={false}
name="command"
formControl={formControl}
label="What alerting script should run"
@@ -588,6 +591,7 @@ const AlertWizard = () => {
{
required: { value: true, message: "This field is required" },
}}
formControl={formControl}
- multiple
+ multiple={true}
+ creatable={false}
options={postExecutionOptions}
/>
From dac4c8766b1b871db657c27640d19a4570f9ba55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 16 May 2025 19:12:34 +0200
Subject: [PATCH 004/865] Feat: Add EntraConnectSyncStatus alert
---
src/data/alerts.json | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/data/alerts.json b/src/data/alerts.json
index 4779d23e58e1..e71be4e1547f 100644
--- a/src/data/alerts.json
+++ b/src/data/alerts.json
@@ -28,6 +28,15 @@
"inputName": "InactiveLicensedUsersExcludeDisabled",
"recommendedRunInterval": "1d"
},
+ {
+ "name": "EntraConnectSyncStatus",
+ "label": "Alert if Entra Connect sync is enabled and has not run in the last X hours",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Hours(Default:72)",
+ "inputName": "EntraConnectSyncStatusHours",
+ "recommendedRunInterval": "1d"
+ },
{
"name": "QuotaUsed",
"label": "Alert on % mailbox quota used",
From 790dd320625724837855a4434c2d01dad91be0da Mon Sep 17 00:00:00 2001
From: ngms-psh
Date: Fri, 16 May 2025 22:24:11 +0200
Subject: [PATCH 005/865] Added standard for Custom Quarantine Policies
---
src/data/standards.json | 112 ++++++++++++++++++++++++++++++++++------
1 file changed, 97 insertions(+), 15 deletions(-)
diff --git a/src/data/standards.json b/src/data/standards.json
index 3a75fdc58266..8e97645da4d2 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -1871,6 +1871,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for Spoof",
"name": "standards.AntiPhishPolicy.SpoofQuarantineTag",
"options": [
@@ -1911,6 +1912,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for user impersonation",
"name": "standards.AntiPhishPolicy.TargetedUserQuarantineTag",
"options": [
@@ -1951,6 +1953,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for domain impersonation",
"name": "standards.AntiPhishPolicy.TargetedDomainQuarantineTag",
"options": [
@@ -1991,6 +1994,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Apply quarantine policy",
"name": "standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag",
"options": [
@@ -2045,6 +2049,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "QuarantineTag",
"name": "standards.SafeAttachmentPolicy.QuarantineTag",
"options": [
@@ -2171,6 +2176,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "QuarantineTag",
"name": "standards.MalwareFilterPolicy.QuarantineTag",
"options": [
@@ -2276,7 +2282,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Spam Quarantine Tag",
"name": "standards.SpamFilterPolicy.SpamQuarantineTag",
"options": [
@@ -2316,7 +2322,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "High Confidence Spam Quarantine Tag",
"name": "standards.SpamFilterPolicy.HighConfidenceSpamQuarantineTag",
"options": [
@@ -2356,7 +2362,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Bulk Quarantine Tag",
"name": "standards.SpamFilterPolicy.BulkQuarantineTag",
"options": [
@@ -2396,7 +2402,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Phish Quarantine Tag",
"name": "standards.SpamFilterPolicy.PhishQuarantineTag",
"options": [
@@ -2418,7 +2424,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "High Confidence Phish Quarantine Tag",
"name": "standards.SpamFilterPolicy.HighConfidencePhishQuarantineTag",
"options": [
@@ -2527,6 +2533,92 @@
"addedDate": "2024-07-15",
"powershellEquivalent": "New-HostedContentFilterPolicy or Set-HostedContentFilterPolicy",
"recommendedBy": []
+ },
+ {
+ "name": "standards.QuarantineTemplate",
+ "cat": "Defender Standards",
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "tag": [],
+ "helpText": "This standard creates a Custom Quarantine Policies that can be used in Anti-Spam and all MDO365 policies. Quarantine Policies can be used to specify recipients permissions, enable end-user spam notifications, and specify the release action preference",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": true,
+ "name": "displayName",
+ "label": "Quarantine Display Name",
+ "required": true
+ },
+ {
+ "type": "switch",
+ "label": "Enable end-user spam notifications",
+ "name": "ESNEnabled",
+ "defaultValue": true,
+ "required": false
+ },
+ {
+ "type": "select",
+ "multiple": false,
+ "label": "Select release action preference",
+ "name": "ReleaseAction",
+ "options": [
+ {
+ "label": "Allow recipients to request a message to be released from quarantine",
+ "value": "PermissionToRequestRelease"
+ },
+ {
+ "label": "Allow recipients to release a message from quarantine",
+ "value": "PermissionToRelease"
+ }
+ ]
+ },
+ {
+ "type": "switch",
+ "label": "Include Messages From Blocked Sender Address",
+ "name": "IncludeMessagesFromBlockedSenderAddress",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to delete message",
+ "name": "PermissionToDelete",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to preview message",
+ "name": "PermissionToPreview",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to block Sender Address",
+ "name": "PermissionToBlockSender",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to whitelist Sender Address",
+ "name": "PermissionToAllowSender",
+ "defaultValue": false,
+ "required": false
+ }
+ ],
+ "label": "Custom Quarantine Policy",
+ "multiple": true,
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-05-16",
+ "powershellEquivalent": "Set-QuarantinePolicy or New-QuarantinePolicy",
+ "recommendedBy": []
},
{
"name": "standards.intuneDeviceRetirementDays",
@@ -3377,11 +3469,6 @@
"name": "standards.TeamsExternalAccessPolicy.EnableFederationAccess",
"label": "Allow communication from trusted organizations"
},
- {
- "type": "switch",
- "name": "standards.TeamsExternalAccessPolicy.EnablePublicCloudAccess",
- "label": "Allow user to communicate with Skype users"
- },
{
"type": "switch",
"name": "standards.TeamsExternalAccessPolicy.EnableTeamsConsumerAccess",
@@ -3407,11 +3494,6 @@
"name": "standards.TeamsFederationConfiguration.AllowTeamsConsumer",
"label": "Allow users to communicate with other organizations"
},
- {
- "type": "switch",
- "name": "standards.TeamsFederationConfiguration.AllowPublicUsers",
- "label": "Allow users to communicate with Skype Users"
- },
{
"type": "autoComplete",
"required": true,
From 5a261ae31b3d0be4c9e2d0b571f281cfd96a33c7 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Sat, 17 May 2025 12:58:24 +0200
Subject: [PATCH 006/865] first new wizard step
---
.../CippComponents/CIPPM365OAuthButton.js | 144 +++++++++---------
.../CippComponents/CippApiResults.jsx | 6 +-
.../CippComponents/CippCopyToClipboard.jsx | 92 ++++++-----
.../CippWizard/CIPPDeploymentUpdateTokens.js | 5 +-
src/components/CippWizard/CippSAMDeploy.js | 130 ++++++++++++++++
src/pages/onboardingv2.js | 16 +-
6 files changed, 272 insertions(+), 121 deletions(-)
create mode 100644 src/components/CippWizard/CippSAMDeploy.js
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.js
index 9c1e3ae58675..f562c275ea50 100644
--- a/src/components/CippComponents/CIPPM365OAuthButton.js
+++ b/src/components/CippComponents/CIPPM365OAuthButton.js
@@ -1,33 +1,14 @@
import { useState } from "react";
-import {
- Alert,
- Button,
- Stack,
- Typography,
- CircularProgress,
- Box,
-} from "@mui/material";
+import { Alert, Button, Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material";
import { ApiGetCall } from "../../api/ApiCall";
import { CippCopyToClipBoard } from "./CippCopyToClipboard";
-/**
- * CIPPM365OAuthButton - A reusable button component for Microsoft 365 OAuth authentication
- *
- * @param {Object} props - Component props
- * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data
- * @param {Function} props.onAuthError - Callback function called when authentication fails with error data
- * @param {string} props.buttonText - Text to display on the button (default: "Login with Microsoft")
- * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true)
- * @param {string} props.scope - OAuth scope to request (default: "https://graph.microsoft.com/.default offline_access profile openid")
- * @param {boolean} props.useDeviceCode - Whether to use device code flow instead of popup (default: false)
- * @param {string} props.applicationId - Application ID to use for authentication (default: uses the one from API)
- * @returns {JSX.Element} The CIPPM365OAuthButton component
- */
export const CIPPM365OAuthButton = ({
onAuthSuccess,
onAuthError,
buttonText = "Login with Microsoft",
showResults = true,
+ showSuccessAlert = true,
scope = "https://graph.microsoft.com/.default offline_access profile openid",
useDeviceCode = false,
applicationId = null,
@@ -74,33 +55,38 @@ export const CIPPM365OAuthButton = ({
try {
// Get the application ID to use
- const appId = applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID
-
+ const appId =
+ applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID
+
// Request device code from our API endpoint
- const deviceCodeResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(scope)}`);
+ const deviceCodeResponse = await fetch(
+ `/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(
+ scope
+ )}`
+ );
const deviceCodeData = await deviceCodeResponse.json();
-
+
if (deviceCodeResponse.ok && deviceCodeData.user_code) {
// Store device code info
setDeviceCodeInfo(deviceCodeData);
-
+
// Open popup to device login page
const width = 500;
const height = 600;
const left = window.screen.width / 2 - width / 2;
const top = window.screen.height / 2 - height / 2;
-
+
const popup = window.open(
"https://microsoft.com/devicelogin",
"deviceLoginPopup",
`width=${width},height=${height},left=${left},top=${top}`
);
-
+
// Start polling for token
const pollInterval = deviceCodeData.interval || 5;
const expiresIn = deviceCodeData.expires_in || 900;
const startTime = Date.now();
-
+
const pollForToken = async () => {
// Check if popup was closed
if (popup && popup.closed) {
@@ -113,7 +99,7 @@ export const CIPPM365OAuthButton = ({
setAuthInProgress(false);
return;
}
-
+
// Check if we've exceeded the expiration time
if (Date.now() - startTime >= expiresIn * 1000) {
if (popup && !popup.closed) {
@@ -127,22 +113,27 @@ export const CIPPM365OAuthButton = ({
setAuthInProgress(false);
return;
}
-
+
try {
// Poll for token using our API endpoint
- const tokenResponse = await fetch(`/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}`);
+ const tokenResponse = await fetch(
+ `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}`
+ );
const tokenData = await tokenResponse.json();
-
+
if (tokenResponse.ok && tokenData.status === "success") {
// Successfully got token
if (popup && !popup.closed) {
popup.close();
}
handleTokenResponse(tokenData);
- } else if (tokenData.error === 'authorization_pending' || tokenData.status === "pending") {
+ } else if (
+ tokenData.error === "authorization_pending" ||
+ tokenData.status === "pending"
+ ) {
// User hasn't completed authentication yet, continue polling
setTimeout(pollForToken, pollInterval * 1000);
- } else if (tokenData.error === 'slow_down') {
+ } else if (tokenData.error === "slow_down") {
// Server asking us to slow down polling
setTimeout(pollForToken, (pollInterval + 5) * 1000);
} else {
@@ -162,7 +153,7 @@ export const CIPPM365OAuthButton = ({
setTimeout(pollForToken, pollInterval * 1000);
}
};
-
+
// Also monitor for popup closing as a fallback
const checkPopupClosed = setInterval(() => {
if (popup && popup.closed) {
@@ -175,7 +166,7 @@ export const CIPPM365OAuthButton = ({
});
}
}, 1000);
-
+
// Start polling
setTimeout(pollForToken, pollInterval * 1000);
} else {
@@ -209,11 +200,11 @@ export const CIPPM365OAuthButton = ({
let username = "unknown user";
let tenantId = "unknown tenant";
let onmicrosoftDomain = null;
-
+
if (tokenData.id_token) {
try {
const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1]));
-
+
// Extract username
username =
idTokenPayload.preferred_username ||
@@ -221,12 +212,12 @@ export const CIPPM365OAuthButton = ({
idTokenPayload.upn ||
idTokenPayload.name ||
"unknown user";
-
+
// Extract tenant ID if available in the token
if (idTokenPayload.tid) {
tenantId = idTokenPayload.tid;
}
-
+
// Try to extract onmicrosoft domain from the username or issuer
if (username && username.includes("@") && username.includes(".onmicrosoft.com")) {
onmicrosoftDomain = username.split("@")[1];
@@ -262,7 +253,7 @@ export const CIPPM365OAuthButton = ({
// Call the onAuthSuccess callback if provided
if (onAuthSuccess) onAuthSuccess(tokenResult);
-
+
// Update UI state
setAuthInProgress(false);
};
@@ -448,7 +439,7 @@ export const CIPPM365OAuthButton = ({
};
setAuthError(error);
if (onAuthError) onAuthError(error);
-
+
// Ensure we're not showing any previous success state
setTokens({
accessToken: null,
@@ -532,7 +523,7 @@ export const CIPPM365OAuthButton = ({
};
setAuthError(error);
if (onAuthError) onAuthError(error);
-
+
// Ensure we're not showing any previous success state
setTokens({
accessToken: null,
@@ -557,9 +548,10 @@ export const CIPPM365OAuthButton = ({
disabled={
appIdInfo.isLoading ||
authInProgress ||
- (!applicationId && !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
- appIdInfo?.data?.applicationId
- ))
+ (!applicationId &&
+ !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
+ appIdInfo?.data?.applicationId
+ ))
}
onClick={useDeviceCode ? handleDeviceCodeAuthentication : handleMsalAuthentication}
color="primary"
@@ -574,15 +566,15 @@ export const CIPPM365OAuthButton = ({
)}
- {!applicationId && !appIdInfo.isLoading &&
+ {!applicationId &&
+ !appIdInfo.isLoading &&
!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
appIdInfo?.data?.applicationId
) && (
The Application ID is not valid. Please check your configuration.
- )
- }
+ )}
{showResults && (
@@ -590,37 +582,45 @@ export const CIPPM365OAuthButton = ({
Device Code Authentication
- A popup window has been opened to microsoft.com/devicelogin .
- Enter this code to authenticate:
+ A popup window has been opened to microsoft.com/devicelogin . Enter
+ this code to authenticate:{" "}
+
- If the popup was blocked or you closed it, you can also go to microsoft.com/devicelogin manually
- and enter the code shown above.
+ If the popup was blocked or you closed it, you can also go to{" "}
+ microsoft.com/devicelogin manually and enter the code shown above.
Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes
) : tokens.accessToken ? (
-
- Authentication Successful
-
- You've successfully refreshed your token. The account you're using for authentication
- is: {tokens.username}
-
-
- Tenant ID: {tokens.tenantId}
- {tokens.onmicrosoftDomain && (
- <> | Domain: {tokens.onmicrosoftDomain} >
- )}
-
-
- Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()}
-
-
+ showSuccessAlert ? (
+
+ Authentication Successful
+
+ You've successfully refreshed your token. The account you're using for
+ authentication is: {tokens.username}
+
+
+ Tenant ID: {tokens.tenantId}
+ {tokens.onmicrosoftDomain && (
+ <>
+ {" "}
+ | Domain: {tokens.onmicrosoftDomain}
+ >
+ )}
+
+
+ Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()}
+
+
+ ) : null
) : authError ? (
- Authentication Error: {authError.errorCode}
+
+ Authentication Error: {authError.errorCode}
+
{authError.errorMessage}
Time: {authError.timestamp}
@@ -637,5 +637,3 @@ export const CIPPM365OAuthButton = ({
);
};
-
-export default CIPPM365OAuthButton;
\ No newline at end of file
diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx
index 21917507c396..b351ec4dd80b 100644
--- a/src/components/CippComponents/CippApiResults.jsx
+++ b/src/components/CippComponents/CippApiResults.jsx
@@ -73,7 +73,7 @@ const extractAllResults = (data) => {
results.push(processed);
}
} else {
- const ignoreKeys = ["metadata", "Metadata"];
+ const ignoreKeys = ["metadata", "Metadata", "severity"];
if (typeof obj === "object") {
Object.keys(obj).forEach((key) => {
@@ -296,7 +296,9 @@ export const CippApiResults = (props) => {
))}
>
)}
- {(apiObject.isSuccess || apiObject.isError) && finalResults?.length > 0 ? (
+ {(apiObject.isSuccess || apiObject.isError) &&
+ finalResults?.length > 0 &&
+ hasVisibleResults ? (
tableDialog.handleOpen()}>
diff --git a/src/components/CippComponents/CippCopyToClipboard.jsx b/src/components/CippComponents/CippCopyToClipboard.jsx
index 8f2cefd686a4..6d3603790df7 100644
--- a/src/components/CippComponents/CippCopyToClipboard.jsx
+++ b/src/components/CippComponents/CippCopyToClipboard.jsx
@@ -4,58 +4,68 @@ import { useState } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
export const CippCopyToClipBoard = (props) => {
- const { text, type = "button", ...other } = props;
+ const { text, type = "button", visible = true, ...other } = props;
const [showPassword, setShowPassword] = useState(false);
+
const handleTogglePassword = () => {
setShowPassword((prev) => !prev);
};
- return (
- <>
- {type === "button" && (
-
-
-
-
-
-
-
-
-
- )}
- {type === "chip" && (
-
+
+ if (!visible) return null;
+
+ if (type === "button") {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (type === "chip") {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (type === "password") {
+ return (
+ <>
+
+
+ {showPassword ? : }
+
+
+
- )}
- {type === "password" && (
- <>
-
-
- {showPassword ? : }
-
-
-
-
-
-
-
- >
- )}
- >
- );
+ >
+ );
+ }
+
+ return null;
};
diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
index 14f57cf99202..fd70341ab548 100644
--- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
+++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
@@ -82,10 +82,7 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
}
>
- Click the button to refresh the Graph token using Device Code Flow. This will open a popup
- to microsoft.com/devicelogin where you can enter the provided code to authenticate. This
- method is useful when regular popup authentication fails or when you need to authenticate
- from a different device than the one running CIPP.
+ Device code flow test
diff --git a/src/components/CippWizard/CippSAMDeploy.js b/src/components/CippWizard/CippSAMDeploy.js
new file mode 100644
index 000000000000..e45fa2a9f87b
--- /dev/null
+++ b/src/components/CippWizard/CippSAMDeploy.js
@@ -0,0 +1,130 @@
+import { useState } from "react";
+import { Alert, Stack, Box, CircularProgress } from "@mui/material";
+import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import { ApiPostCall } from "../../api/ApiCall";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+
+export const CippSAMDeploy = (props) => {
+ const { formControl, currentStep, onPreviousStep, onNextStep } = props;
+ const [authStatus, setAuthStatus] = useState({
+ success: false,
+ error: null,
+ loading: false,
+ });
+
+ //TODO: Make sure to block next button until the app is created.
+
+ // API call to create/update SAM app
+ const createSamApp = ApiPostCall({ urlfromdata: true });
+ // Handle successful authentication
+ const handleAuthSuccess = (tokenData) => {
+ setAuthStatus({
+ success: false,
+ error: null,
+ loading: true,
+ });
+
+ // Send the access token to the API to create/update SAM app
+ createSamApp.mutate({
+ url: "/api/ExecCreateSamApp",
+ data: { access_token: tokenData.accessToken },
+ });
+ };
+
+ // Handle authentication error
+ const handleAuthError = (error) => {
+ setAuthStatus({
+ success: false,
+ error: error.errorMessage || "Authentication failed",
+ loading: false,
+ });
+ };
+
+ // Update status when API call completes
+ if (createSamApp.isSuccess && authStatus.loading) {
+ const data = createSamApp.data;
+ if (data.severity === "error") {
+ setAuthStatus({
+ success: false,
+ error: data.message || "Failed to create SAM application",
+ loading: false,
+ });
+ } else if (data.severity === "success") {
+ setAuthStatus({
+ success: true,
+ error: null,
+ loading: false,
+ });
+ // Allow user to proceed to next step
+ formControl.setValue("samAppCreated", true);
+ }
+ }
+
+ // Handle API error
+ if (createSamApp.isError && authStatus.loading) {
+ setAuthStatus({
+ success: false,
+ error: "An error occurred while creating the SAM application",
+ loading: false,
+ });
+ }
+
+ return (
+
+
+ This step will create or update the CIPP Application Registration in your tenant. Make sure
+ the account you use is one of the following roles:
+
+ Global Administrator or Privileged Role Administrator
+ Application Administrator
+ Cloud Application Administrator
+
+
+
+ {/* Show API results */}
+
+
+ {/* Show error message if any */}
+ {authStatus.error && (
+
+ {authStatus.error}
+
+ )}
+
+ {/* Show success message when authentication is successful */}
+ {authStatus.success && !authStatus.loading && (
+
+ SAM application has been successfully created/updated. You can now proceed to the next
+ step.
+
+ )}
+
+ {/* Show authenticate button only if not successful yet */}
+ {(!authStatus.success || authStatus.loading) && (
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default CippSAMDeploy;
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index 4de309be07ae..f2cb811cedc7 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -3,6 +3,7 @@ import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfi
import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.js";
import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx";
import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx";
+import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.js";
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -56,37 +57,50 @@ const Page = () => {
{
title: "Step 2",
description: "Application",
- component: CippDeploymentStep,
+ component: CippSAMDeploy,
+ componentProps: {
+ title: "SAM Application Setup",
+ subtext: "This step will create or update the SAM application in your tenant.",
+ },
},
{
title: "Step 3",
description: "Tenants",
component: CippDeploymentStep,
+ //set the tenant mode to "GDAP", "perTenant" or "mixed".
+ //if the tenant mode is set to GDAP, show MSAL button to update token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "GDAP" }
+ //if the tenant mode is set to perTenant, show MSAL button to get token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "perTenant" }. List each tenant that has authenticated and been added.
+ //if the tenant mode is set to mixed, show first MSAL button to update GDAP access, then show second MSAL button to update perTenant access. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "mixed" }
},
{
title: "Step 4",
description: "Baselines",
component: CippDeploymentStep,
+ //give choice to download baselines from repos.
},
{
title: "Step 5",
description: "Integrations",
component: CippDeploymentStep,
+ //give the choice to configure integrations.
},
{
title: "Step 6",
description: "Notifications",
component: CippDeploymentStep,
+ //explain notifications, test if email is setup,etc.
},
{
title: "Step 7",
description: "Alerts",
component: CippDeploymentStep,
+ //show template alerts, allow user to configure them.
},
{
title: "Step 8",
description: "Confirmation",
component: CippWizardConfirmation,
+ //confirm and finish button, perform tasks, launch checks etc.
},
];
From 4204310110067c357f9002c45fd771053a759aa6 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Sun, 18 May 2025 01:11:45 +0200
Subject: [PATCH 007/865] updates to new sam wizard
---
...OAuthButton.js => CIPPM365OAuthButton.jsx} | 547 +++++++++++-------
...ploymentStep.js => CIPPDeploymentStep.jsx} | 0
...kens.js => CIPPDeploymentUpdateTokens.jsx} | 1 +
...ialsStep.js => CippPSACredentialsStep.jsx} | 0
...ASyncOptions.js => CippPSASyncOptions.jsx} | 0
.../{CippSAMDeploy.js => CippSAMDeploy.jsx} | 27 +-
.../CippWizard/CippTenantModeDeploy.jsx | 326 +++++++++++
...irmation.js => CippWizardConfirmation.jsx} | 0
src/pages/authredirect.js | 46 ++
src/pages/onboardingv2.js | 17 +-
.../tenant/administration/tenants/add.js | 2 +-
11 files changed, 721 insertions(+), 245 deletions(-)
rename src/components/CippComponents/{CIPPM365OAuthButton.js => CIPPM365OAuthButton.jsx} (58%)
rename src/components/CippWizard/{CIPPDeploymentStep.js => CIPPDeploymentStep.jsx} (100%)
rename src/components/CippWizard/{CIPPDeploymentUpdateTokens.js => CIPPDeploymentUpdateTokens.jsx} (98%)
rename src/components/CippWizard/{CippPSACredentialsStep.js => CippPSACredentialsStep.jsx} (100%)
rename src/components/CippWizard/{CippPSASyncOptions.js => CippPSASyncOptions.jsx} (100%)
rename src/components/CippWizard/{CippSAMDeploy.js => CippSAMDeploy.jsx} (82%)
create mode 100644 src/components/CippWizard/CippTenantModeDeploy.jsx
rename src/components/CippWizard/{CippWizardConfirmation.js => CippWizardConfirmation.jsx} (100%)
create mode 100644 src/pages/authredirect.js
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.js b/src/components/CippComponents/CIPPM365OAuthButton.jsx
similarity index 58%
rename from src/components/CippComponents/CIPPM365OAuthButton.js
rename to src/components/CippComponents/CIPPM365OAuthButton.jsx
index f562c275ea50..6241823034c7 100644
--- a/src/components/CippComponents/CIPPM365OAuthButton.js
+++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx
@@ -1,5 +1,5 @@
-import { useState } from "react";
-import { Alert, Button, Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material";
+import { useState, useEffect } from "react";
+import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material";
import { ApiGetCall } from "../../api/ApiCall";
import { CippCopyToClipBoard } from "./CippCopyToClipboard";
@@ -12,10 +12,14 @@ export const CIPPM365OAuthButton = ({
scope = "https://graph.microsoft.com/.default offline_access profile openid",
useDeviceCode = false,
applicationId = null,
+ autoStartDeviceLogon = false,
+ validateServiceAccount = true, // Add prop to control service account validation
}) => {
const [authInProgress, setAuthInProgress] = useState(false);
const [authError, setAuthError] = useState(null);
const [deviceCodeInfo, setDeviceCodeInfo] = useState(null);
+ const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false);
+ const [isServiceAccount, setIsServiceAccount] = useState(true); // Default to true to avoid showing warning initially
const [tokens, setTokens] = useState({
accessToken: null,
refreshToken: null,
@@ -29,29 +33,35 @@ export const CIPPM365OAuthButton = ({
// Get application ID information from API if not provided
const appIdInfo = ApiGetCall({
url: `/api/ExecListAppId`,
- queryKey: `ExecListAppId`,
waiting: true,
});
+ // Ensure appId is refetched every time the component is mounted
+ useEffect(() => {
+ // Refetch appId when component mounts
+ appIdInfo.refetch();
+ }, []); // Empty dependency array ensures this runs only on mount
+
// Handle closing the error
const handleCloseError = () => {
setAuthError(null);
};
- // Device code authentication function
- const handleDeviceCodeAuthentication = async () => {
- setAuthInProgress(true);
+ // Check if username is a service account (contains "service" or "cipp")
+ const checkIsServiceAccount = (username) => {
+ if (!username || !validateServiceAccount) return true; // If no username or validation disabled, don't show warning
+
+ const lowerUsername = username.toLowerCase();
+ return lowerUsername.includes("service") || lowerUsername.includes("cipp");
+ };
+
+ // Function to retrieve device code
+ const retrieveDeviceCode = async () => {
+ setCodeRetrievalInProgress(true);
setAuthError(null);
- setDeviceCodeInfo(null);
- setTokens({
- accessToken: null,
- refreshToken: null,
- accessTokenExpiresOn: null,
- refreshTokenExpiresOn: null,
- username: null,
- tenantId: null,
- onmicrosoftDomain: null,
- });
+
+ // Refetch appId to ensure we have the latest
+ await appIdInfo.refetch();
try {
// Get the application ID to use
@@ -69,117 +79,151 @@ export const CIPPM365OAuthButton = ({
if (deviceCodeResponse.ok && deviceCodeData.user_code) {
// Store device code info
setDeviceCodeInfo(deviceCodeData);
+ } else {
+ // Error getting device code
+ setAuthError({
+ errorCode: deviceCodeData.error || "device_code_error",
+ errorMessage: deviceCodeData.error_description || "Failed to get device code",
+ timestamp: new Date().toISOString(),
+ });
+ }
+ } catch (error) {
+ setAuthError({
+ errorCode: "device_code_error",
+ errorMessage: error.message || "An error occurred retrieving device code",
+ timestamp: new Date().toISOString(),
+ });
+ } finally {
+ setCodeRetrievalInProgress(false);
+ }
+ };
- // Open popup to device login page
- const width = 500;
- const height = 600;
- const left = window.screen.width / 2 - width / 2;
- const top = window.screen.height / 2 - height / 2;
-
- const popup = window.open(
- "https://microsoft.com/devicelogin",
- "deviceLoginPopup",
- `width=${width},height=${height},left=${left},top=${top}`
- );
-
- // Start polling for token
- const pollInterval = deviceCodeData.interval || 5;
- const expiresIn = deviceCodeData.expires_in || 900;
- const startTime = Date.now();
-
- const pollForToken = async () => {
- // Check if popup was closed
- if (popup && popup.closed) {
- clearInterval(checkPopupClosed);
- setAuthError({
- errorCode: "user_cancelled",
- errorMessage: "Authentication was cancelled. Please try again.",
- timestamp: new Date().toISOString(),
- });
- setAuthInProgress(false);
- return;
+ // Device code authentication function - opens popup and starts polling
+ const handleDeviceCodeAuthentication = async () => {
+ // Refetch appId to ensure we have the latest
+ await appIdInfo.refetch();
+
+ if (!deviceCodeInfo) {
+ // If we don't have a device code yet, retrieve it first
+ await retrieveDeviceCode();
+ return;
+ }
+
+ setAuthInProgress(true);
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ try {
+ // Get the application ID to use - refetch already happened at the start of this function
+ const appId =
+ applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID
+
+ // Open popup to device login page
+ const width = 500;
+ const height = 600;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+
+ const popup = window.open(
+ "https://microsoft.com/devicelogin",
+ "deviceLoginPopup",
+ `width=${width},height=${height},left=${left},top=${top}`
+ );
+
+ // Start polling for token
+ const pollInterval = deviceCodeInfo.interval || 5;
+ const expiresIn = deviceCodeInfo.expires_in || 900;
+ const startTime = Date.now();
+
+ const pollForToken = async () => {
+ // Check if popup was closed
+ if (popup && popup.closed) {
+ clearInterval(checkPopupClosed);
+ setAuthError({
+ errorCode: "user_cancelled",
+ errorMessage: "Authentication was cancelled. Please try again.",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ return;
+ }
+
+ // Check if we've exceeded the expiration time
+ if (Date.now() - startTime >= expiresIn * 1000) {
+ if (popup && !popup.closed) {
+ popup.close();
}
+ setAuthError({
+ errorCode: "timeout",
+ errorMessage: "Device code authentication timed out",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ return;
+ }
+
+ try {
+ // Poll for token using our API endpoint
+ const tokenResponse = await fetch(
+ `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}`
+ );
+ const tokenData = await tokenResponse.json();
- // Check if we've exceeded the expiration time
- if (Date.now() - startTime >= expiresIn * 1000) {
+ if (tokenResponse.ok && tokenData.status === "success") {
+ // Successfully got token
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ handleTokenResponse(tokenData);
+ } else if (
+ tokenData.error === "authorization_pending" ||
+ tokenData.status === "pending"
+ ) {
+ // User hasn't completed authentication yet, continue polling
+ setTimeout(pollForToken, pollInterval * 1000);
+ } else if (tokenData.error === "slow_down") {
+ // Server asking us to slow down polling
+ setTimeout(pollForToken, (pollInterval + 5) * 1000);
+ } else {
+ // Other error
if (popup && !popup.closed) {
popup.close();
}
setAuthError({
- errorCode: "timeout",
- errorMessage: "Device code authentication timed out",
+ errorCode: tokenData.error || "token_error",
+ errorMessage: tokenData.error_description || "Failed to get token",
timestamp: new Date().toISOString(),
});
setAuthInProgress(false);
- return;
- }
-
- try {
- // Poll for token using our API endpoint
- const tokenResponse = await fetch(
- `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeData.device_code}`
- );
- const tokenData = await tokenResponse.json();
-
- if (tokenResponse.ok && tokenData.status === "success") {
- // Successfully got token
- if (popup && !popup.closed) {
- popup.close();
- }
- handleTokenResponse(tokenData);
- } else if (
- tokenData.error === "authorization_pending" ||
- tokenData.status === "pending"
- ) {
- // User hasn't completed authentication yet, continue polling
- setTimeout(pollForToken, pollInterval * 1000);
- } else if (tokenData.error === "slow_down") {
- // Server asking us to slow down polling
- setTimeout(pollForToken, (pollInterval + 5) * 1000);
- } else {
- // Other error
- if (popup && !popup.closed) {
- popup.close();
- }
- setAuthError({
- errorCode: tokenData.error || "token_error",
- errorMessage: tokenData.error_description || "Failed to get token",
- timestamp: new Date().toISOString(),
- });
- setAuthInProgress(false);
- }
- } catch (error) {
- console.error("Error polling for token:", error);
- setTimeout(pollForToken, pollInterval * 1000);
}
- };
+ } catch (error) {
+ setTimeout(pollForToken, pollInterval * 1000);
+ }
+ };
- // Also monitor for popup closing as a fallback
- const checkPopupClosed = setInterval(() => {
- if (popup && popup.closed) {
- clearInterval(checkPopupClosed);
- setAuthInProgress(false);
- setAuthError({
- errorCode: "user_cancelled",
- errorMessage: "Authentication was cancelled. Please try again.",
- timestamp: new Date().toISOString(),
- });
- }
- }, 1000);
+ // Also monitor for popup closing as a fallback
+ const checkPopupClosed = setInterval(() => {
+ if (popup && popup.closed) {
+ clearInterval(checkPopupClosed);
+ setAuthInProgress(false);
+ setAuthError({
+ errorCode: "user_cancelled",
+ errorMessage: "Authentication was cancelled. Please try again.",
+ timestamp: new Date().toISOString(),
+ });
+ }
+ }, 1000);
- // Start polling
- setTimeout(pollForToken, pollInterval * 1000);
- } else {
- // Error getting device code
- setAuthError({
- errorCode: deviceCodeData.error || "device_code_error",
- errorMessage: deviceCodeData.error_description || "Failed to get device code",
- timestamp: new Date().toISOString(),
- });
- setAuthInProgress(false);
- }
+ // Start polling
+ setTimeout(pollForToken, pollInterval * 1000);
} catch (error) {
- console.error("Error in device code authentication:", error);
setAuthError({
errorCode: "device_code_error",
errorMessage: error.message || "An error occurred during device code authentication",
@@ -227,9 +271,10 @@ export const CIPPM365OAuthButton = ({
// We have the tenant ID, but not the domain name
}
}
- } catch (error) {
- console.error("Error parsing ID token:", error);
- }
+
+ // Check if username is a service account
+ setIsServiceAccount(checkIsServiceAccount(username));
+ } catch (error) {}
}
// Create token result object
@@ -243,15 +288,9 @@ export const CIPPM365OAuthButton = ({
onmicrosoftDomain: onmicrosoftDomain,
};
- // Store tokens in component state
setTokens(tokenResult);
setDeviceCodeInfo(null);
- // Log only the necessary token information to console
- console.log("Access Token:", tokenData.access_token);
- console.log("Refresh Token:", tokenData.refresh_token);
-
- // Call the onAuthSuccess callback if provided
if (onAuthSuccess) onAuthSuccess(tokenResult);
// Update UI state
@@ -259,7 +298,7 @@ export const CIPPM365OAuthButton = ({
};
// MSAL-like authentication function
- const handleMsalAuthentication = () => {
+ const handleMsalAuthentication = async () => {
// Clear previous authentication state when starting a new authentication
setAuthInProgress(true);
setAuthError(null);
@@ -273,7 +312,10 @@ export const CIPPM365OAuthButton = ({
onmicrosoftDomain: null,
});
- // Get the application ID to use
+ // Refetch app ID info to ensure we have the latest
+ await appIdInfo.refetch();
+
+ // Get the application ID to use - now we're sure to have the latest after the await
const appId = applicationId || appIdInfo?.data?.applicationId;
// Generate MSAL-like authentication parameters
@@ -281,7 +323,7 @@ export const CIPPM365OAuthButton = ({
auth: {
clientId: appId,
authority: `https://login.microsoftonline.com/common`,
- redirectUri: window.location.origin,
+ redirectUri: `${window.location.origin}/authredirect`,
},
};
@@ -290,9 +332,6 @@ export const CIPPM365OAuthButton = ({
scopes: [scope],
};
- console.log("MSAL Config:", msalConfig);
- console.log("Login Request:", loginRequest);
-
// Generate PKCE code verifier and challenge
const generateCodeVerifier = () => {
const array = new Uint8Array(32);
@@ -300,36 +339,20 @@ export const CIPPM365OAuthButton = ({
return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join("");
};
- const base64URLEncode = (str) => {
- return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
- };
-
- // Generate code verifier for PKCE
const codeVerifier = generateCodeVerifier();
- // In a real implementation, we would hash the code verifier to create the code challenge
- // For simplicity, we'll use the same value
const codeChallenge = codeVerifier;
-
- // Note: We're not storing the code verifier in session storage for security reasons
- // Instead, we'll use it directly in the token exchange
-
- // Create a random state value for security
const state = Math.random().toString(36).substring(2, 15);
-
- // Create the auth URL with PKCE parameters
const authUrl =
`https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
`client_id=${appId}` +
`&response_type=code` +
- `&redirect_uri=${encodeURIComponent(window.location.origin)}` +
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}/authredirect` +
`&scope=${encodeURIComponent(scope)}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=plain` +
`&state=${state}` +
`&prompt=select_account`;
- console.log("MSAL Auth URL:", authUrl);
-
// Open popup for authentication
const width = 500;
const height = 600;
@@ -347,7 +370,6 @@ export const CIPPM365OAuthButton = ({
// Verify the state parameter matches what we sent (security check)
if (receivedState !== state) {
const errorMessage = "State mismatch in auth response - possible CSRF attack";
- console.error(errorMessage);
const error = {
errorCode: "state_mismatch",
errorMessage: errorMessage,
@@ -358,38 +380,70 @@ export const CIPPM365OAuthButton = ({
setAuthInProgress(false);
return;
}
-
- console.log("Authorization code received:", code);
-
try {
- // Actually exchange the code for tokens using the token endpoint
- console.log("Exchanging authorization code for tokens...");
-
// Prepare the token request
const tokenRequest = {
grant_type: "authorization_code",
client_id: appId,
code: code,
- redirect_uri: window.location.origin,
+ redirect_uri: `${window.location.origin}/authredirect`,
code_verifier: codeVerifier,
};
- // Make the token request
- const tokenResponse = await fetch(
- `https://login.microsoftonline.com/common/oauth2/v2.0/token`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: new URLSearchParams(tokenRequest).toString(),
- }
- );
+ // Make the token request through our API proxy to avoid origin header issues
+ const tokenResponse = await fetch(`/api/ExecTokenExchange`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ tokenRequest,
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ tenantId: appId, // Pass the tenant ID to retrieve the correct client secret
+ }),
+ });
// Parse the token response
const tokenData = await tokenResponse.json();
+ // Check if the response contains an error
+ if (tokenData.error) {
+ const error = {
+ errorCode: tokenData.error || "token_error",
+ errorMessage:
+ tokenData.error_description || "Failed to exchange authorization code for tokens",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+ setAuthInProgress(false);
+ return;
+ }
+
if (tokenResponse.ok) {
+ // If we have a refresh token, store it
+ if (tokenData.refresh_token) {
+ try {
+ // Store the refresh token
+ const refreshResponse = await fetch(`/api/ExecUpdateRefreshToken`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ tenantId: appId,
+ refreshtoken: tokenData.refresh_token,
+ }),
+ });
+
+ if (!refreshResponse.ok) {
+ console.warn("Failed to store refresh token, but continuing with authentication");
+ }
+ } catch (error) {
+ console.error("Failed to store refresh token:", error);
+ }
+ }
+
handleTokenResponse(tokenData);
} else {
// Handle token error - display in error box instead of throwing
@@ -403,7 +457,6 @@ export const CIPPM365OAuthButton = ({
if (onAuthError) onAuthError(error);
}
} catch (error) {
- console.error("Error exchanging code for tokens:", error);
const errorObj = {
errorCode: "token_exchange_error",
errorMessage: error.message || "Failed to exchange authorization code for tokens",
@@ -431,7 +484,6 @@ export const CIPPM365OAuthButton = ({
// If authentication is still in progress when popup closes, it's an error
if (authInProgress) {
const errorMessage = "Authentication was cancelled. Please try again.";
- console.error(errorMessage);
const error = {
errorCode: "user_cancelled",
errorMessage: errorMessage,
@@ -463,9 +515,6 @@ export const CIPPM365OAuthButton = ({
// Check if the URL contains a code parameter (authorization code)
if (currentUrl.includes("code=") && currentUrl.includes("state=")) {
clearInterval(checkPopupLocation);
-
- console.log("Detected authorization code in URL:", currentUrl);
-
// Parse the URL to extract the code and state
const urlParams = new URLSearchParams(popup.location.search);
const code = urlParams.get("code");
@@ -478,9 +527,6 @@ export const CIPPM365OAuthButton = ({
// Check for error in the URL
if (currentUrl.includes("error=")) {
clearInterval(checkPopupLocation);
-
- console.error("Detected error in authentication response:", currentUrl);
-
// Parse the URL to extract the error details
const urlParams = new URLSearchParams(popup.location.search);
const errorCode = urlParams.get("error");
@@ -515,7 +561,6 @@ export const CIPPM365OAuthButton = ({
// If authentication is still in progress when popup closes, it's an error
if (authInProgress) {
const errorMessage = "Authentication was cancelled. Please try again.";
- console.error(errorMessage);
const error = {
errorCode: "user_cancelled",
errorMessage: errorMessage,
@@ -541,33 +586,32 @@ export const CIPPM365OAuthButton = ({
}, 1000);
};
+ // Auto-start device code retrieval if requested
+ useEffect(() => {
+ if (
+ useDeviceCode &&
+ autoStartDeviceLogon &&
+ !codeRetrievalInProgress &&
+ !deviceCodeInfo &&
+ !tokens.accessToken &&
+ appIdInfo?.data
+ ) {
+ retrieveDeviceCode();
+ }
+ }, [
+ useDeviceCode,
+ autoStartDeviceLogon,
+ codeRetrievalInProgress,
+ deviceCodeInfo,
+ tokens.accessToken,
+ appIdInfo?.data,
+ ]);
+
return (
-
- {authInProgress ? (
- <>
-
- Authenticating...
- >
- ) : (
- buttonText
- )}
-
-
{!applicationId &&
!appIdInfo.isLoading &&
+ appIdInfo?.data && // Only check if data is available
!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
appIdInfo?.data?.applicationId
) && (
@@ -577,45 +621,77 @@ export const CIPPM365OAuthButton = ({
)}
{showResults && (
-
- {deviceCodeInfo && authInProgress ? (
+
+ {deviceCodeInfo ? (
Device Code Authentication
- A popup window has been opened to microsoft.com/devicelogin . Enter
- this code to authenticate:{" "}
+ {authInProgress ? (
+ <>
+ A popup window has been opened to microsoft.com/devicelogin .
+ Enter this code to authenticate:{" "}
+ >
+ ) : (
+ <>Click the button below to authenticate. You will need to enter this code: >
+ )}
- If the popup was blocked or you closed it, you can also go to{" "}
- microsoft.com/devicelogin manually and enter the code shown above.
+ {authInProgress ? (
+ <>
+ If the popup was blocked or you closed it, you can also go to{" "}
+ microsoft.com/devicelogin manually and enter the code shown
+ above.
+ >
+ ) : (
+ <>
+ When you click the button below, a popup will open to{" "}
+ microsoft.com/devicelogin where you'll enter this code.
+ >
+ )}
Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes
) : tokens.accessToken ? (
- showSuccessAlert ? (
-
- Authentication Successful
-
- You've successfully refreshed your token. The account you're using for
- authentication is: {tokens.username}
-
-
- Tenant ID: {tokens.tenantId}
- {tokens.onmicrosoftDomain && (
- <>
- {" "}
- | Domain: {tokens.onmicrosoftDomain}
- >
- )}
-
-
- Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()}
-
-
- ) : null
+ <>
+ {showSuccessAlert ? (
+
+ Authentication Successful
+
+ You've successfully refreshed your token. The account you're using for
+ authentication is: {tokens.username}
+
+
+ Tenant ID: {tokens.tenantId}
+ {tokens.onmicrosoftDomain && (
+ <>
+ {" "}
+ | Domain: {tokens.onmicrosoftDomain}
+ >
+ )}
+
+
+ Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()}
+
+
+ ) : null}
+
+ {!isServiceAccount && (
+
+ Service Account Required
+
+ CIPP requires a service account for authentication. The account you're using (
+ {tokens.username} ) does not appear to be a service account.
+
+
+ Please redo authentication using an account with "service" or "cipp" in the
+ username.
+
+
+ )}
+ >
) : authError ? (
@@ -634,6 +710,31 @@ export const CIPPM365OAuthButton = ({
) : null}
)}
+
+ {authInProgress || codeRetrievalInProgress ? (
+ <>
+
+ Authenticating...
+ >
+ ) : deviceCodeInfo && useDeviceCode ? (
+ "Authenticate with Code"
+ ) : (
+ buttonText
+ )}
+
);
};
diff --git a/src/components/CippWizard/CIPPDeploymentStep.js b/src/components/CippWizard/CIPPDeploymentStep.jsx
similarity index 100%
rename from src/components/CippWizard/CIPPDeploymentStep.js
rename to src/components/CippWizard/CIPPDeploymentStep.jsx
diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
similarity index 98%
rename from src/components/CippWizard/CIPPDeploymentUpdateTokens.js
rename to src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
index fd70341ab548..6bfa70481719 100644
--- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.js
+++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
@@ -78,6 +78,7 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
buttonText="Refresh Graph Token (Device Code)"
useDeviceCode={true}
applicationId="1950a258-227b-4e31-a9cf-717495945fc2"
+ autoStartDeviceLogon={true}
/>
}
>
diff --git a/src/components/CippWizard/CippPSACredentialsStep.js b/src/components/CippWizard/CippPSACredentialsStep.jsx
similarity index 100%
rename from src/components/CippWizard/CippPSACredentialsStep.js
rename to src/components/CippWizard/CippPSACredentialsStep.jsx
diff --git a/src/components/CippWizard/CippPSASyncOptions.js b/src/components/CippWizard/CippPSASyncOptions.jsx
similarity index 100%
rename from src/components/CippWizard/CippPSASyncOptions.js
rename to src/components/CippWizard/CippPSASyncOptions.jsx
diff --git a/src/components/CippWizard/CippSAMDeploy.js b/src/components/CippWizard/CippSAMDeploy.jsx
similarity index 82%
rename from src/components/CippWizard/CippSAMDeploy.js
rename to src/components/CippWizard/CippSAMDeploy.jsx
index e45fa2a9f87b..3aef23bdb69d 100644
--- a/src/components/CippWizard/CippSAMDeploy.js
+++ b/src/components/CippWizard/CippSAMDeploy.jsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { Alert, Stack, Box, CircularProgress } from "@mui/material";
+import { Alert, Stack, Box, CircularProgress, Link } from "@mui/material";
import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
import { CippApiResults } from "../CippComponents/CippApiResults";
import { ApiPostCall } from "../../api/ApiCall";
@@ -73,15 +73,23 @@ export const CippSAMDeploy = (props) => {
return (
- This step will create or update the CIPP Application Registration in your tenant. Make sure
- the account you use is one of the following roles:
-
- Global Administrator or Privileged Role Administrator
- Application Administrator
- Cloud Application Administrator
-
+ To run this setup you will need the following prerequisites:
+
+ A CIPP Service Account. For more information on how to create a service account, click{" "}
+
+ here
+
+
+ (Temporary) Global Administrator permissions for the CIPP Service Account
+
+ Multi-factor authentication enabled for the CIPP Service Account, with no trusted
+ locations or other exclusions.
+
-
{/* Show API results */}
@@ -111,6 +119,7 @@ export const CippSAMDeploy = (props) => {
useDeviceCode={true}
applicationId="1950a258-227b-4e31-a9cf-717495945fc2"
showSuccessAlert={false}
+ autoStartDeviceLogon={true}
/>
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
new file mode 100644
index 000000000000..a478e8ea3b25
--- /dev/null
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -0,0 +1,326 @@
+import { useState, useEffect } from "react";
+import {
+ Alert,
+ Stack,
+ Box,
+ Typography,
+ CircularProgress,
+ Divider,
+ List,
+ ListItem,
+ ListItemText,
+ Paper,
+ Switch,
+ FormControlLabel,
+} from "@mui/material";
+import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import { ApiPostCall, ApiGetCall } from "../../api/ApiCall";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippAutoComplete } from "../CippComponents/CippAutocomplete";
+
+export const CippTenantModeDeploy = (props) => {
+ const { formControl, currentStep, onPreviousStep, onNextStep } = props;
+
+ const [tenantMode, setTenantMode] = useState("GDAP");
+ const [allowPartnerTenantManagement, setAllowPartnerTenantManagement] = useState(false);
+ const [gdapAuthStatus, setGdapAuthStatus] = useState({
+ success: false,
+ loading: false,
+ });
+ const [perTenantAuthStatus, setPerTenantAuthStatus] = useState({
+ success: false,
+ loading: false,
+ });
+ const [authenticatedTenants, setAuthenticatedTenants] = useState([]);
+
+ // API call to update refresh token
+ const updateRefreshToken = ApiPostCall({ urlfromdata: true });
+
+ // API call to get list of authenticated tenants (for perTenant mode)
+ const tenantList = ApiGetCall({
+ url: "/api/ListTenants",
+ queryKey: "ListTenants",
+ });
+
+ // Update authenticated tenants list when tenantList changes
+ useEffect(() => {
+ if (tenantList.data && tenantMode === "perTenant") {
+ setAuthenticatedTenants(tenantList.data);
+ }
+ }, [tenantList.data, tenantMode]);
+
+ // Handle tenant mode change
+ const handleTenantModeChange = (selectedOption) => {
+ if (selectedOption) {
+ setTenantMode(selectedOption.value);
+ // Reset auth status when changing modes
+ setGdapAuthStatus({
+ success: false,
+ loading: false,
+ });
+ setPerTenantAuthStatus({
+ success: false,
+ loading: false,
+ });
+ // Reset partner tenant management option
+ setAllowPartnerTenantManagement(false);
+ }
+ };
+
+ // Tenant mode options
+ const tenantModeOptions = [
+ {
+ label: "GDAP - Uses your partner center to connect to tenants",
+ value: "GDAP",
+ },
+ {
+ label: "Per Tenant - Add each tenant individually",
+ value: "perTenant",
+ },
+ {
+ label: "Mixed - Use Partner Center and add tenants individually",
+ value: "mixed",
+ },
+ ];
+
+ // Handle GDAP authentication success
+ const handleGdapAuthSuccess = (tokenData) => {
+ setGdapAuthStatus({
+ success: false,
+ loading: true,
+ });
+
+ // Send the refresh token to the API
+ updateRefreshToken.mutate({
+ url: "/api/ExecUpdateRefreshToken",
+ data: {
+ tenantId: tokenData.tenantId,
+ refreshToken: tokenData.refreshToken,
+ tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode,
+ allowPartnerTenantManagement: tenantMode === "GDAP" ? allowPartnerTenantManagement : false,
+ },
+ });
+ };
+
+ // Handle perTenant authentication success
+ const handlePerTenantAuthSuccess = (tokenData) => {
+ setPerTenantAuthStatus({
+ success: false,
+ loading: true,
+ });
+
+ // Send the refresh token to the API
+ updateRefreshToken.mutate({
+ url: "/api/ExecUpdateRefreshToken",
+ data: {
+ tenantId: tokenData.tenantId,
+ refreshToken: tokenData.refreshToken,
+ tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode,
+ },
+ });
+ };
+
+ // Update status when API call completes
+ useEffect(() => {
+ if (updateRefreshToken.isSuccess) {
+ const data = updateRefreshToken.data;
+
+ if (data.state === "error") {
+ if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) {
+ setGdapAuthStatus({
+ success: false,
+ loading: false,
+ });
+ } else {
+ setPerTenantAuthStatus({
+ success: false,
+ loading: false,
+ });
+ }
+ } else if (data.state === "success") {
+ if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) {
+ setGdapAuthStatus({
+ success: true,
+ loading: false,
+ });
+ // Allow user to proceed to next step if not in mixed mode
+ if (tenantMode !== "mixed") {
+ formControl.setValue("tenantModeSet", true);
+ }
+ } else {
+ setPerTenantAuthStatus({
+ success: true,
+ loading: false,
+ });
+ // Allow user to proceed to next step
+ formControl.setValue("tenantModeSet", true);
+
+ // Refresh tenant list for perTenant mode
+ if (tenantMode === "perTenant") {
+ tenantList.refetch();
+ }
+ }
+ }
+ }
+ }, [updateRefreshToken.isSuccess, updateRefreshToken.data]);
+
+ // Handle API error
+ useEffect(() => {
+ if (updateRefreshToken.isError) {
+ if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) {
+ setGdapAuthStatus({
+ success: false,
+ loading: false,
+ });
+ } else {
+ setPerTenantAuthStatus({
+ success: false,
+ loading: false,
+ });
+ }
+ }
+ }, [updateRefreshToken.isError]);
+
+ return (
+
+
+ Select how you want to connect to your tenants. You have three options:
+
+
+ GDAP: Use delegated administration (recommended)
+
+
+ Per Tenant: Authenticate to each tenant individually
+
+
+ Mixed: Use both GDAP and per-tenant authentication
+
+
+
+
+ {/* Tenant mode selection */}
+
+
+ Tenant Connection Mode
+
+ option.value === tenantMode)}
+ onChange={handleTenantModeChange}
+ multiple={false}
+ required={true}
+ />
+
+
+
+
+ {/* Show API results */}
+
+
+ {/* GDAP Authentication Section */}
+ {(tenantMode === "GDAP" || tenantMode === "mixed") && (
+
+
+ GDAP Authentication
+
+
+ {/* Show success message when authentication is successful */}
+ {gdapAuthStatus.success && (
+
+ GDAP authentication successful. You can now proceed to the next step.
+
+ )}
+
+ {/* GDAP Partner Tenant Management Switch */}
+ setAllowPartnerTenantManagement(e.target.checked)}
+ color="primary"
+ />
+ }
+ label="Allow management of the partner tenant"
+ />
+
+ {/* Show authenticate button only if not successful yet */}
+ {(!gdapAuthStatus.success || gdapAuthStatus.loading) && (
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Per Tenant Authentication Section */}
+ {(tenantMode === "perTenant" || (tenantMode === "mixed" && gdapAuthStatus.success)) && (
+
+
+ Per-Tenant Authentication
+
+
+ {/* Show success message when authentication is successful */}
+ {perTenantAuthStatus.success && (
+
+ Per-tenant authentication successful. You can add another tenant or proceed to the
+ next step.
+
+ )}
+
+ {/* Show authenticate button */}
+
+
+
+ {(perTenantAuthStatus.loading || updateRefreshToken.isLoading) && (
+
+ )}
+
+
+
+ {/* List authenticated tenants for perTenant mode */}
+ {tenantMode === "perTenant" && authenticatedTenants.length > 0 && (
+
+
+ Authenticated Tenants
+
+
+
+ {authenticatedTenants.map((tenant, index) => (
+
+
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default CippTenantModeDeploy;
diff --git a/src/components/CippWizard/CippWizardConfirmation.js b/src/components/CippWizard/CippWizardConfirmation.jsx
similarity index 100%
rename from src/components/CippWizard/CippWizardConfirmation.js
rename to src/components/CippWizard/CippWizardConfirmation.jsx
diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js
new file mode 100644
index 000000000000..892b712bdc47
--- /dev/null
+++ b/src/pages/authredirect.js
@@ -0,0 +1,46 @@
+import { Box, Container, Grid, Stack } from "@mui/material";
+import Head from "next/head";
+import { CippImageCard } from "../components/CippCards/CippImageCard.jsx";
+import { Layout as DashboardLayout } from "../layouts/index.js";
+
+const Page = () => (
+ <>
+
+
+ Authentication complete
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export default Page;
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index f2cb811cedc7..ba5890e30d46 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -1,9 +1,10 @@
import { Layout as DashboardLayout } from "../layouts/index.js";
-import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.js";
-import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.js";
+import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.jsx";
+import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.jsx";
import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx";
import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx";
-import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.js";
+import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx";
+import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx";
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -58,19 +59,11 @@ const Page = () => {
title: "Step 2",
description: "Application",
component: CippSAMDeploy,
- componentProps: {
- title: "SAM Application Setup",
- subtext: "This step will create or update the SAM application in your tenant.",
- },
},
{
title: "Step 3",
description: "Tenants",
- component: CippDeploymentStep,
- //set the tenant mode to "GDAP", "perTenant" or "mixed".
- //if the tenant mode is set to GDAP, show MSAL button to update token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "GDAP" }
- //if the tenant mode is set to perTenant, show MSAL button to get token. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "perTenant" }. List each tenant that has authenticated and been added.
- //if the tenant mode is set to mixed, show first MSAL button to update GDAP access, then show second MSAL button to update perTenant access. Send to /api/ExecUpdateRefreshToken with body { "tenantId": tenantId, "refreshToken": refreshToken, "tenantMode": "mixed" }
+ component: CippTenantModeDeploy,
},
{
title: "Step 4",
diff --git a/src/pages/tenant/administration/tenants/add.js b/src/pages/tenant/administration/tenants/add.js
index 68235a1135a8..cdfa1b05143d 100644
--- a/src/pages/tenant/administration/tenants/add.js
+++ b/src/pages/tenant/administration/tenants/add.js
@@ -3,7 +3,7 @@ import CippWizardPage from "../../../../components/CippWizard/CippWizardPage.jsx
import { CippWizardOptionsList } from "../../../../components/CippWizard/CippWizardOptionsList.jsx";
import { CippAddTenantForm } from "../../../../components/CippWizard/CippAddTenantForm.jsx";
import { BuildingOfficeIcon, CloudIcon } from "@heroicons/react/24/outline";
-import CippWizardConfirmation from "../../../../components/CippWizard/CippWizardConfirmation.js";
+import CippWizardConfirmation from "../../../../components/CippWizard/CippWizardConfirmation.jsx";
const Page = () => {
const steps = [
From 4592916e5175e3e31f72f727221cdff57c247f9b Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Sun, 18 May 2025 23:22:40 +0200
Subject: [PATCH 008/865] Updates for single tenant mode
---
.../CippComponents/CIPPM365OAuthButton.jsx | 2 +
.../CippWizard/CippTenantModeDeploy.jsx | 231 +++++++++---------
2 files changed, 119 insertions(+), 114 deletions(-)
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx
index 6241823034c7..83eae0dc7ebe 100644
--- a/src/components/CippComponents/CIPPM365OAuthButton.jsx
+++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx
@@ -433,6 +433,8 @@ export const CIPPM365OAuthButton = ({
body: JSON.stringify({
tenantId: appId,
refreshtoken: tokenData.refresh_token,
+ tenantMode: tokenData.tenantMode,
+ allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement,
}),
});
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index a478e8ea3b25..93b49e5bb2c5 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -34,8 +34,9 @@ export const CippTenantModeDeploy = (props) => {
});
const [authenticatedTenants, setAuthenticatedTenants] = useState([]);
- // API call to update refresh token
+ // API calls
const updateRefreshToken = ApiPostCall({ urlfromdata: true });
+ const addTenant = ApiPostCall({ urlfromdata: true });
// API call to get list of authenticated tenants (for perTenant mode)
const tenantList = ApiGetCall({
@@ -45,7 +46,7 @@ export const CippTenantModeDeploy = (props) => {
// Update authenticated tenants list when tenantList changes
useEffect(() => {
- if (tenantList.data && tenantMode === "perTenant") {
+ if (tenantList.data && (tenantMode === "perTenant" || tenantMode === "mixed")) {
setAuthenticatedTenants(tenantList.data);
}
}, [tenantList.data, tenantMode]);
@@ -87,100 +88,65 @@ export const CippTenantModeDeploy = (props) => {
// Handle GDAP authentication success
const handleGdapAuthSuccess = (tokenData) => {
setGdapAuthStatus({
- success: false,
- loading: true,
+ success: true,
+ loading: false,
});
- // Send the refresh token to the API
- updateRefreshToken.mutate({
- url: "/api/ExecUpdateRefreshToken",
- data: {
- tenantId: tokenData.tenantId,
- refreshToken: tokenData.refreshToken,
- tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode,
- allowPartnerTenantManagement: tenantMode === "GDAP" ? allowPartnerTenantManagement : false,
- },
- });
+ // Allow user to proceed to next step for GDAP mode
+ if (tenantMode === "GDAP") {
+ formControl.setValue("tenantModeSet", true);
+ } else if (tenantMode === "mixed") {
+ // For mixed mode, allow proceeding if either authentication is successful
+ formControl.setValue("tenantModeSet", true);
+ }
};
// Handle perTenant authentication success
const handlePerTenantAuthSuccess = (tokenData) => {
setPerTenantAuthStatus({
- success: false,
- loading: true,
- });
-
- // Send the refresh token to the API
- updateRefreshToken.mutate({
- url: "/api/ExecUpdateRefreshToken",
- data: {
- tenantId: tokenData.tenantId,
- refreshToken: tokenData.refreshToken,
- tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode,
- },
+ success: true,
+ loading: false,
});
- };
- // Update status when API call completes
- useEffect(() => {
- if (updateRefreshToken.isSuccess) {
- const data = updateRefreshToken.data;
+ // For perTenant mode or mixed mode with perTenant auth, add the tenant to the cache
+ if (tenantMode === "perTenant" || tenantMode === "mixed") {
+ // Call the AddTenant API to add the tenant to the cache with directTenant status
+ addTenant.mutate({
+ url: "/api/ExecAddTenant",
+ data: {
+ tenantId: tokenData.tenantId,
+ },
+ });
+ }
- if (data.state === "error") {
- if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) {
- setGdapAuthStatus({
- success: false,
- loading: false,
- });
- } else {
- setPerTenantAuthStatus({
- success: false,
- loading: false,
- });
- }
- } else if (data.state === "success") {
- if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) {
- setGdapAuthStatus({
- success: true,
- loading: false,
- });
- // Allow user to proceed to next step if not in mixed mode
- if (tenantMode !== "mixed") {
- formControl.setValue("tenantModeSet", true);
- }
- } else {
- setPerTenantAuthStatus({
- success: true,
- loading: false,
- });
- // Allow user to proceed to next step
- formControl.setValue("tenantModeSet", true);
+ // Allow user to proceed to next step
+ formControl.setValue("tenantModeSet", true);
- // Refresh tenant list for perTenant mode
- if (tenantMode === "perTenant") {
- tenantList.refetch();
- }
- }
- }
+ // Refresh tenant list for perTenant and mixed modes
+ if (tenantMode === "perTenant" || tenantMode === "mixed") {
+ tenantList.refetch();
}
- }, [updateRefreshToken.isSuccess, updateRefreshToken.data]);
+ };
// Handle API error
useEffect(() => {
- if (updateRefreshToken.isError) {
- if (tenantMode === "GDAP" || (tenantMode === "mixed" && gdapAuthStatus.loading)) {
- setGdapAuthStatus({
- success: false,
- loading: false,
- });
- } else {
- setPerTenantAuthStatus({
- success: false,
- loading: false,
- });
- }
+ if (addTenant.isError) {
+ setPerTenantAuthStatus({
+ success: false,
+ loading: false,
+ });
+ }
+ }, [addTenant.isError]);
+
+ // Handle AddTenant API response
+ useEffect(() => {
+ if (addTenant.isSuccess) {
+ console.log("Tenant added to cache successfully:", addTenant.data);
+ } else if (addTenant.isError) {
+ console.error("Failed to add tenant to cache:", addTenant.error);
}
- }, [updateRefreshToken.isError]);
+ }, [addTenant.isSuccess, addTenant.isError]);
+
return (
@@ -217,19 +183,31 @@ export const CippTenantModeDeploy = (props) => {
{/* Show API results */}
-
+
+ {addTenant.isSuccess && (
+
+ Tenant successfully added to the cache.
+
+ )}
+ {addTenant.isError && (
+
+ Failed to add tenant to the cache: {addTenant.error?.message || "Unknown error"}
+
+ )}
{/* GDAP Authentication Section */}
{(tenantMode === "GDAP" || tenantMode === "mixed") && (
- GDAP Authentication
+ Partner Tenant
{/* Show success message when authentication is successful */}
{gdapAuthStatus.success && (
- GDAP authentication successful. You can now proceed to the next step.
+ {tenantMode === "mixed"
+ ? "GDAP authentication successful. You can now proceed to the next step or connect to separate tenants below."
+ : "GDAP authentication successful. You can now proceed to the next step."}
)}
@@ -242,7 +220,7 @@ export const CippTenantModeDeploy = (props) => {
color="primary"
/>
}
- label="Allow management of the partner tenant"
+ label="Allow management of the partner tenant."
/>
{/* Show authenticate button only if not successful yet */}
@@ -250,8 +228,18 @@ export const CippTenantModeDeploy = (props) => {
{
+ // Add the tenantMode and allowPartnerTenantManagement parameters to the tokenData
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode,
+ allowPartnerTenantManagement: allowPartnerTenantManagement,
+ };
+ handleGdapAuthSuccess(updatedTokenData);
+ }}
+ buttonText={
+ tenantMode === "mixed" ? "Connect to GDAP" : "Authenticate with Microsoft GDAP"
+ }
showSuccessAlert={false}
/>
@@ -261,7 +249,7 @@ export const CippTenantModeDeploy = (props) => {
)}
{/* Per Tenant Authentication Section */}
- {(tenantMode === "perTenant" || (tenantMode === "mixed" && gdapAuthStatus.success)) && (
+ {(tenantMode === "perTenant" || tenantMode === "mixed") && (
Per-Tenant Authentication
@@ -270,45 +258,60 @@ export const CippTenantModeDeploy = (props) => {
{/* Show success message when authentication is successful */}
{perTenantAuthStatus.success && (
- Per-tenant authentication successful. You can add another tenant or proceed to the
- next step.
+ {tenantMode === "mixed"
+ ? "Tenant authentication successful. You can add another tenant or proceed to the next step."
+ : "Per-tenant authentication successful. You can add another tenant or proceed to the next step."}
)}
+
+ {tenantMode === "mixed"
+ ? "Click the button below to connect to individual tenants. You can authenticate to multiple tenants one by one."
+ : "You can click the button below to authenticate to a tenant. Perform this authentication for every tenant you wish to manage using CIPP."}
+
{/* Show authenticate button */}
{
+ // Add the tenantMode parameter to the tokenData
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode,
+ };
+ handlePerTenantAuthSuccess(updatedTokenData);
+ }}
+ buttonText={
+ tenantMode === "mixed"
+ ? "Connect to Separate Tenants"
+ : "Authenticate with Microsoft"
+ }
showSuccessAlert={false}
/>
- {(perTenantAuthStatus.loading || updateRefreshToken.isLoading) && (
-
- )}
- {/* List authenticated tenants for perTenant mode */}
- {tenantMode === "perTenant" && authenticatedTenants.length > 0 && (
-
-
- Authenticated Tenants
-
-
-
- {authenticatedTenants.map((tenant, index) => (
-
-
-
- ))}
-
-
-
- )}
+ {/* List authenticated tenants for perTenant and mixed modes */}
+ {(tenantMode === "perTenant" || tenantMode === "mixed") &&
+ authenticatedTenants.length > 0 && (
+
+
+ Authenticated Tenants
+
+
+
+ {authenticatedTenants.map((tenant, index) => (
+
+
+
+ ))}
+
+
+
+ )}
)}
From 573fc22c5f5a6aedc6c0693a57d217681d7a4595 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 19 May 2025 10:59:51 +0200
Subject: [PATCH 009/865] changes
---
src/components/CippWizard/CippTenantModeDeploy.jsx | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index 93b49e5bb2c5..d0eee14a96e6 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -147,7 +147,6 @@ export const CippTenantModeDeploy = (props) => {
}
}, [addTenant.isSuccess, addTenant.isError]);
-
return (
@@ -202,14 +201,7 @@ export const CippTenantModeDeploy = (props) => {
Partner Tenant
- {/* Show success message when authentication is successful */}
- {gdapAuthStatus.success && (
-
- {tenantMode === "mixed"
- ? "GDAP authentication successful. You can now proceed to the next step or connect to separate tenants below."
- : "GDAP authentication successful. You can now proceed to the next step."}
-
- )}
+
{/* GDAP Partner Tenant Management Switch */}
Date: Mon, 19 May 2025 17:06:43 +0800
Subject: [PATCH 010/865] Passthrough default sorting to CippTablePage to use
in Tenant Groups List
---
src/components/CippComponents/CippTablePage.jsx | 2 ++
src/pages/tenant/administration/tenants/groups/index.js | 1 +
2 files changed, 3 insertions(+)
diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx
index eb75f6cc1ad1..60a50c974dc3 100644
--- a/src/components/CippComponents/CippTablePage.jsx
+++ b/src/components/CippComponents/CippTablePage.jsx
@@ -25,6 +25,7 @@ export const CippTablePage = (props) => {
tableFilter,
tenantInTitle = true,
filters,
+ defaultSorting = [],
sx = { flexGrow: 1, py: 4 },
...other
} = props;
@@ -65,6 +66,7 @@ export const CippTablePage = (props) => {
columnsFromApi={columnsFromApi}
offCanvas={offCanvas}
filters={tableFilters}
+ defaultSorting={defaultSorting}
initialState={{
columnFilters: filters ? filters.map(filter => ({
id: filter.id || filter.columnId,
diff --git a/src/pages/tenant/administration/tenants/groups/index.js b/src/pages/tenant/administration/tenants/groups/index.js
index 8d3e4c328ceb..d8d201634969 100644
--- a/src/pages/tenant/administration/tenants/groups/index.js
+++ b/src/pages/tenant/administration/tenants/groups/index.js
@@ -38,6 +38,7 @@ const Page = () => {
queryKey="TenantGroupListPage"
apiDataKey="Results"
actions={actions}
+ defaultSorting={[{ id: "Name", desc: false }]}
cardButton={
Date: Mon, 19 May 2025 17:07:18 +0800
Subject: [PATCH 011/865] Revert "Passthrough default sorting to CippTablePage
to use in Tenant Groups List"
This reverts commit ff59725da54107a6aa6c05aacd331a0d09b9cda2.
---
src/components/CippComponents/CippTablePage.jsx | 2 --
src/pages/tenant/administration/tenants/groups/index.js | 1 -
2 files changed, 3 deletions(-)
diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx
index 60a50c974dc3..eb75f6cc1ad1 100644
--- a/src/components/CippComponents/CippTablePage.jsx
+++ b/src/components/CippComponents/CippTablePage.jsx
@@ -25,7 +25,6 @@ export const CippTablePage = (props) => {
tableFilter,
tenantInTitle = true,
filters,
- defaultSorting = [],
sx = { flexGrow: 1, py: 4 },
...other
} = props;
@@ -66,7 +65,6 @@ export const CippTablePage = (props) => {
columnsFromApi={columnsFromApi}
offCanvas={offCanvas}
filters={tableFilters}
- defaultSorting={defaultSorting}
initialState={{
columnFilters: filters ? filters.map(filter => ({
id: filter.id || filter.columnId,
diff --git a/src/pages/tenant/administration/tenants/groups/index.js b/src/pages/tenant/administration/tenants/groups/index.js
index d8d201634969..8d3e4c328ceb 100644
--- a/src/pages/tenant/administration/tenants/groups/index.js
+++ b/src/pages/tenant/administration/tenants/groups/index.js
@@ -38,7 +38,6 @@ const Page = () => {
queryKey="TenantGroupListPage"
apiDataKey="Results"
actions={actions}
- defaultSorting={[{ id: "Name", desc: false }]}
cardButton={
Date: Mon, 19 May 2025 17:44:57 +0200
Subject: [PATCH 012/865] Configuration
---
.../CippWizard/CippTenantModeDeploy.jsx | 211 ++++++++++++------
src/layouts/config.js | 4 +-
src/pages/onboardingv2.js | 2 +-
3 files changed, 140 insertions(+), 77 deletions(-)
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index d0eee14a96e6..1e7ad8999a44 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -1,10 +1,8 @@
import { useState, useEffect } from "react";
import {
- Alert,
Stack,
Box,
Typography,
- CircularProgress,
Divider,
List,
ListItem,
@@ -18,6 +16,7 @@ import { CippApiResults } from "../CippComponents/CippApiResults";
import { ApiPostCall, ApiGetCall } from "../../api/ApiCall";
import { CippWizardStepButtons } from "./CippWizardStepButtons";
import { CippAutoComplete } from "../CippComponents/CippAutocomplete";
+import { getCippError } from "../../utils/get-cipp-error";
export const CippTenantModeDeploy = (props) => {
const { formControl, currentStep, onPreviousStep, onNextStep } = props;
@@ -87,11 +86,49 @@ export const CippTenantModeDeploy = (props) => {
// Handle GDAP authentication success
const handleGdapAuthSuccess = (tokenData) => {
+ // Set loading state
setGdapAuthStatus({
success: true,
- loading: false,
+ loading: true,
});
+ // Log the token data for debugging
+ console.log("GDAP Auth Success - Token Data:", {
+ tenantId: tokenData.tenantId,
+ refreshToken: tokenData.refreshToken ? "present" : "missing",
+ tenantMode: tokenData.tenantMode,
+ allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement,
+ });
+
+ // Explicitly call the updateRefreshToken API
+ updateRefreshToken.mutate(
+ {
+ url: "/api/ExecUpdateRefreshToken",
+ data: {
+ tenantId: tokenData.tenantId,
+ refreshtoken: tokenData.refreshToken,
+ tenantMode: tokenData.tenantMode,
+ allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement,
+ },
+ },
+ {
+ onSuccess: (data) => {
+ console.log("Update Refresh Token Success:", data);
+ setGdapAuthStatus({
+ success: true,
+ loading: false,
+ });
+ },
+ onError: (error) => {
+ console.error("Update Refresh Token Error:", error);
+ setGdapAuthStatus({
+ success: false,
+ loading: false,
+ });
+ },
+ }
+ );
+
// Allow user to proceed to next step for GDAP mode
if (tenantMode === "GDAP") {
formControl.setValue("tenantModeSet", true);
@@ -103,19 +140,50 @@ export const CippTenantModeDeploy = (props) => {
// Handle perTenant authentication success
const handlePerTenantAuthSuccess = (tokenData) => {
+ // Set loading state
setPerTenantAuthStatus({
success: true,
- loading: false,
+ loading: true,
+ });
+
+ // Log the token data for debugging
+ console.log("Per-Tenant Auth Success - Token Data:", {
+ tenantId: tokenData.tenantId,
+ tenantMode: tokenData.tenantMode,
});
// For perTenant mode or mixed mode with perTenant auth, add the tenant to the cache
if (tenantMode === "perTenant" || tenantMode === "mixed") {
// Call the AddTenant API to add the tenant to the cache with directTenant status
- addTenant.mutate({
- url: "/api/ExecAddTenant",
- data: {
- tenantId: tokenData.tenantId,
+ addTenant.mutate(
+ {
+ url: "/api/ExecAddTenant",
+ data: {
+ tenantId: tokenData.tenantId,
+ },
},
+ {
+ onSuccess: (data) => {
+ console.log("Add Tenant Success:", data);
+ setPerTenantAuthStatus({
+ success: true,
+ loading: false,
+ });
+ },
+ onError: (error) => {
+ console.error("Add Tenant Error:", error);
+ setPerTenantAuthStatus({
+ success: false,
+ loading: false,
+ });
+ },
+ }
+ );
+ } else {
+ // If not adding tenant, still update state
+ setPerTenantAuthStatus({
+ success: true,
+ loading: false,
});
}
@@ -138,31 +206,47 @@ export const CippTenantModeDeploy = (props) => {
}
}, [addTenant.isError]);
- // Handle AddTenant API response
+ // Debug logging for API states
useEffect(() => {
- if (addTenant.isSuccess) {
- console.log("Tenant added to cache successfully:", addTenant.data);
- } else if (addTenant.isError) {
- console.error("Failed to add tenant to cache:", addTenant.error);
+ if (
+ updateRefreshToken.isLoading ||
+ updateRefreshToken.isSuccess ||
+ updateRefreshToken.isError
+ ) {
+ console.log("updateRefreshToken state:", {
+ isLoading: updateRefreshToken.isLoading,
+ isSuccess: updateRefreshToken.isSuccess,
+ isError: updateRefreshToken.isError,
+ data: updateRefreshToken.data,
+ error: updateRefreshToken.error ? getCippError(updateRefreshToken.error) : null,
+ });
}
- }, [addTenant.isSuccess, addTenant.isError]);
+ }, [
+ updateRefreshToken.isLoading,
+ updateRefreshToken.isSuccess,
+ updateRefreshToken.isError,
+ updateRefreshToken.data,
+ updateRefreshToken.error,
+ ]);
return (
-
- Select how you want to connect to your tenants. You have three options:
-
-
- GDAP: Use delegated administration (recommended)
-
-
- Per Tenant: Authenticate to each tenant individually
-
-
- Mixed: Use both GDAP and per-tenant authentication
-
-
-
+
+
+ Select how you want to connect to your tenants. You have three options:
+
+
+ GDAP: Use delegated administration (recommended)
+
+
+ Per Tenant: Authenticate to each tenant individually
+
+
+ Mixed: Use both GDAP and per-tenant authentication
+
+
+
+
{/* Tenant mode selection */}
@@ -181,18 +265,9 @@ export const CippTenantModeDeploy = (props) => {
- {/* Show API results */}
+ {/* Show API results at top level for visibility across all modes */}
+
- {addTenant.isSuccess && (
-
- Tenant successfully added to the cache.
-
- )}
- {addTenant.isError && (
-
- Failed to add tenant to the cache: {addTenant.error?.message || "Unknown error"}
-
- )}
{/* GDAP Authentication Section */}
{(tenantMode === "GDAP" || tenantMode === "mixed") && (
@@ -201,8 +276,6 @@ export const CippTenantModeDeploy = (props) => {
Partner Tenant
-
-
{/* GDAP Partner Tenant Management Switch */}
{
label="Allow management of the partner tenant."
/>
- {/* Show authenticate button only if not successful yet */}
- {(!gdapAuthStatus.success || gdapAuthStatus.loading) && (
-
-
- {
- // Add the tenantMode and allowPartnerTenantManagement parameters to the tokenData
- const updatedTokenData = {
- ...tokenData,
- tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode,
- allowPartnerTenantManagement: allowPartnerTenantManagement,
- };
- handleGdapAuthSuccess(updatedTokenData);
- }}
- buttonText={
- tenantMode === "mixed" ? "Connect to GDAP" : "Authenticate with Microsoft GDAP"
- }
- showSuccessAlert={false}
- />
-
-
- )}
+ {/* Always show authenticate button */}
+
+
+ {
+ // Add the tenantMode and allowPartnerTenantManagement parameters to the tokenData
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode,
+ allowPartnerTenantManagement: allowPartnerTenantManagement,
+ };
+ handleGdapAuthSuccess(updatedTokenData);
+ }}
+ buttonText={
+ tenantMode === "mixed" ? "Connect to GDAP" : "Authenticate with Microsoft GDAP"
+ }
+ showSuccessAlert={false}
+ />
+
+
)}
@@ -247,20 +318,12 @@ export const CippTenantModeDeploy = (props) => {
Per-Tenant Authentication
- {/* Show success message when authentication is successful */}
- {perTenantAuthStatus.success && (
-
- {tenantMode === "mixed"
- ? "Tenant authentication successful. You can add another tenant or proceed to the next step."
- : "Per-tenant authentication successful. You can add another tenant or proceed to the next step."}
-
- )}
-
-
+
{tenantMode === "mixed"
? "Click the button below to connect to individual tenants. You can authenticate to multiple tenants one by one."
: "You can click the button below to authenticate to a tenant. Perform this authentication for every tenant you wish to manage using CIPP."}
-
+
+
{/* Show authenticate button */}
diff --git a/src/layouts/config.js b/src/layouts/config.js
index ccf67d86e9ba..de5a12c35b5f 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -464,12 +464,12 @@ export const nativeMenuItems = [
items: [
{ title: "Application Settings", path: "/cipp/settings", roles: ["admin", "superadmin"] },
{ title: "Logbook", path: "/cipp/logs", roles: ["editor", "admin", "superadmin"] },
- { title: "SAM Setup Wizard", path: "/onboarding", roles: ["admin", "superadmin"] },
+ { title: "Setup Wizard", path: "/onboarding", roles: ["admin", "superadmin"] },
{ title: "Integrations", path: "/cipp/integrations", roles: ["admin", "superadmin"] },
{
title: "Custom Data",
path: "/cipp/custom-data/directory-extensions",
- roles: ["admin", "superadmin"]
+ roles: ["admin", "superadmin"],
},
{
title: "Advanced",
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index ba5890e30d46..c5e0a4a89134 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -102,7 +102,7 @@ const Page = () => {
>
From f830836f1305446638a6a3766d7cce59704436fe Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 12:49:04 -0400
Subject: [PATCH 013/865] Entra Group Auth
---
.../CippComponents/CippFormComponent.jsx | 2 +-
.../CippComponents/CippSettingsSideBar.jsx | 4 +-
.../CippSettings/CippRoleAddEdit.jsx | 578 ++++++++++++++++++
src/components/CippSettings/CippRoles.jsx | 115 ++++
src/components/PrivateRoute.js | 25 +-
src/data/cipp-roles.json | 23 +
src/layouts/account-popover.js | 4 +-
src/layouts/index.js | 68 ++-
src/layouts/side-nav.js | 167 ++---
src/pages/cipp/preferences.js | 6 +-
src/pages/cipp/super-admin/cipp-roles/add.js | 23 +
src/pages/cipp/super-admin/cipp-roles/edit.js | 28 +
.../{custom-roles.js => cipp-roles/index.js} | 10 +-
src/pages/cipp/super-admin/tabOptions.json | 4 +-
src/pages/loading.js | 71 +++
src/pages/unauthenticated.js | 35 +-
staticwebapp.config.json | 54 +-
17 files changed, 1020 insertions(+), 197 deletions(-)
create mode 100644 src/components/CippSettings/CippRoleAddEdit.jsx
create mode 100644 src/components/CippSettings/CippRoles.jsx
create mode 100644 src/data/cipp-roles.json
create mode 100644 src/pages/cipp/super-admin/cipp-roles/add.js
create mode 100644 src/pages/cipp/super-admin/cipp-roles/edit.js
rename src/pages/cipp/super-admin/{custom-roles.js => cipp-roles/index.js} (79%)
create mode 100644 src/pages/loading.js
diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx
index 02f6d9a50cbc..ba6b3ba9d458 100644
--- a/src/components/CippComponents/CippFormComponent.jsx
+++ b/src/components/CippComponents/CippFormComponent.jsx
@@ -228,7 +228,7 @@ export const CippFormComponent = (props) => {
}
+ control={ }
label={option.label}
/>
))}
diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx
index cb08993a8ea1..3a3c998c7c38 100644
--- a/src/components/CippComponents/CippSettingsSideBar.jsx
+++ b/src/components/CippComponents/CippSettingsSideBar.jsx
@@ -20,10 +20,8 @@ export const CippSettingsSideBar = (props) => {
const { isDirty, isValid } = useFormState({ control: formcontrol.control });
const currentUser = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
const saveSettingsPost = ApiPostCall({
diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx
new file mode 100644
index 000000000000..08ea7fcd821c
--- /dev/null
+++ b/src/components/CippSettings/CippRoleAddEdit.jsx
@@ -0,0 +1,578 @@
+import React, { useEffect, useState } from "react";
+
+import {
+ Box,
+ Button,
+ Alert,
+ Typography,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ Stack,
+ SvgIcon,
+ Skeleton,
+} from "@mui/material";
+
+import Grid from "@mui/material/Grid2";
+import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "../../api/ApiCall";
+import { CippOffCanvas } from "/src/components/CippComponents/CippOffCanvas";
+import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector";
+import { Save, Warning, WarningOutlined } from "@mui/icons-material";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import CippFormComponent from "../CippComponents/CippFormComponent";
+import { useForm, useFormState, useWatch } from "react-hook-form";
+import { InformationCircleIcon } from "@heroicons/react/24/outline";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import cippRoles from "../../data/cipp-roles.json";
+
+export const CippRoleAddEdit = ({ selectedRole }) => {
+ const updatePermissions = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["customRoleList", "customRoleTable"],
+ });
+
+ const [allTenantSelected, setAllTenantSelected] = useState(false);
+ const [cippApiRoleSelected, setCippApiRoleSelected] = useState(false);
+ const [selectedRoleState, setSelectedRoleState] = useState(null);
+ const [updateDefaults, setUpdateDefaults] = useState(false);
+ const [baseRolePermissions, setBaseRolePermissions] = useState({});
+ const [isBaseRole, setIsBaseRole] = useState(false);
+
+ const formControl = useForm({
+ mode: "onChange",
+ });
+
+ const formState = useFormState({ control: formControl.control });
+
+ const validateRoleName = (value) => {
+ if (
+ customRoleList?.pages?.[0]?.some(
+ (role) => role?.RowKey?.toLowerCase() === value?.toLowerCase()
+ )
+ ) {
+ return `Role '${value}' already exists`;
+ }
+ return true;
+ };
+
+ const selectedTenant = useWatch({ control: formControl.control, name: "allowedTenants" });
+ const blockedTenants = useWatch({ control: formControl.control, name: "blockedTenants" });
+ const setDefaults = useWatch({ control: formControl.control, name: "Defaults" });
+ const selectedPermissions = useWatch({ control: formControl.control, name: "Permissions" });
+ const selectedEntraGroup = useWatch({ control: formControl.control, name: "EntraGroup" });
+
+ const {
+ data: apiPermissions = [],
+ isFetching: apiPermissionFetching,
+ isSuccess: apiPermissionSuccess,
+ } = ApiGetCall({
+ url: "/api/ExecAPIPermissionList",
+ queryKey: "apiPermissions",
+ });
+
+ const {
+ data: customRoleList = [],
+ isFetching: customRoleListFetching,
+ isSuccess: customRoleListSuccess,
+ } = ApiGetCallWithPagination({
+ url: "/api/ExecCustomRole",
+ queryKey: "customRoleList",
+ });
+
+ const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({
+ url: "/api/ListTenants?AllTenantSelector=true",
+ queryKey: "ListTenants-AllTenantSelector",
+ });
+ const tenants = pages[0] || [];
+
+ const matchPattern = (pattern, value) => {
+ const regex = new RegExp(`^${pattern.replace("*", ".*")}$`);
+ return regex.test(value);
+ };
+
+ const getBaseRolePermissions = (role) => {
+ const roleConfig = cippRoles[role];
+ if (!roleConfig) return {};
+
+ const permissions = {};
+ Object.keys(apiPermissions).forEach((cat) => {
+ Object.keys(apiPermissions[cat]).forEach((obj) => {
+ const includeRead = roleConfig.include.some((pattern) =>
+ matchPattern(pattern, `${cat}.${obj}.Read`)
+ );
+ const includeReadWrite = roleConfig.include.some((pattern) =>
+ matchPattern(pattern, `${cat}.${obj}.ReadWrite`)
+ );
+ const excludeRead = roleConfig.exclude.some((pattern) =>
+ matchPattern(pattern, `${cat}.${obj}.Read`)
+ );
+ const excludeReadWrite = roleConfig.exclude.some((pattern) =>
+ matchPattern(pattern, `${cat}.${obj}.ReadWrite`)
+ );
+
+ if ((includeRead || includeReadWrite) && !(excludeRead || excludeReadWrite)) {
+ if (!permissions[cat]) permissions[cat] = {};
+ permissions[cat][obj] = includeReadWrite ? `ReadWrite` : `Read`;
+ }
+ if (!permissions[cat] || !permissions[cat][obj]) {
+ if (!permissions[cat]) permissions[cat] = {};
+ permissions[cat][obj] = `None`;
+ }
+ });
+ });
+ return permissions;
+ };
+
+ useEffect(() => {
+ if (selectedRole && cippRoles[selectedRole]) {
+ setBaseRolePermissions(getBaseRolePermissions(selectedRole));
+ setIsBaseRole(true);
+ } else {
+ setBaseRolePermissions({});
+ setIsBaseRole(false);
+ }
+ }, [selectedRole, apiPermissions]);
+
+ useEffect(() => {
+ if (
+ (customRoleListSuccess &&
+ tenantsSuccess &&
+ selectedRole &&
+ selectedRoleState !== selectedRole) ||
+ baseRolePermissions
+ ) {
+ setSelectedRoleState(selectedRole);
+ const isApiRole = selectedRole === "api-role";
+ setCippApiRoleSelected(isApiRole);
+
+ const currentPermissions = customRoleList?.pages?.[0]?.find(
+ (role) => role.RowKey === selectedRole
+ );
+
+ var newAllowedTenants = [];
+ currentPermissions?.AllowedTenants.map((tenant) => {
+ var tenantInfo = tenants.find((t) => t.customerId === tenant);
+ var label = `${tenantInfo?.displayName} (${tenantInfo?.defaultDomainName})`;
+ if (tenantInfo?.displayName) {
+ newAllowedTenants.push({
+ label: label,
+ value: tenantInfo.defaultDomainName,
+ });
+ }
+ });
+
+ var newBlockedTenants = [];
+ currentPermissions?.BlockedTenants.map((tenant) => {
+ var tenantInfo = tenants.find((t) => t.customerId === tenant);
+ var label = `${tenantInfo?.displayName} (${tenantInfo?.defaultDomainName})`;
+ if (tenantInfo?.displayName) {
+ newBlockedTenants.push({
+ label: label,
+ value: tenantInfo.defaultDomainName,
+ });
+ }
+ });
+
+ const basePermissions = {};
+ Object.entries(getBaseRolePermissions(selectedRole)).forEach(([cat, objects]) => {
+ Object.entries(objects).forEach(([obj, permission]) => {
+ basePermissions[`${cat}${obj}`] = `${cat}.${obj}.${permission}`;
+ });
+ });
+ const processPermissions = (permissions) => {
+ const processed = {};
+ Object.keys(apiPermissions).forEach((cat) => {
+ Object.keys(apiPermissions[cat]).forEach((obj) => {
+ const key = `${cat}${obj}`;
+ const existingPerm = permissions?.[key];
+ processed[key] = existingPerm || `${cat}.${obj}.None`;
+ });
+ });
+ return processed;
+ };
+
+ formControl.reset({
+ Permissions:
+ basePermissions && Object.keys(basePermissions).length > 0
+ ? basePermissions
+ : processPermissions(currentPermissions?.Permissions),
+ RoleName: selectedRole ?? currentPermissions?.RowKey,
+ allowedTenants: newAllowedTenants,
+ blockedTenants: newBlockedTenants,
+ EntraGroup: currentPermissions?.EntraGroup,
+ });
+ }
+ }, [customRoleList, customRoleListSuccess, tenantsSuccess, baseRolePermissions]);
+
+ useEffect(() => {
+ if (updateDefaults !== setDefaults) {
+ setUpdateDefaults(setDefaults);
+ var newPermissions = {};
+ Object.keys(apiPermissions).forEach((cat) => {
+ Object.keys(apiPermissions[cat]).forEach((obj) => {
+ var newval = "";
+ if (cat == "CIPP" && obj == "Core" && setDefaults == "None") {
+ newval = "Read";
+ } else {
+ newval = setDefaults;
+ }
+ newPermissions[`${cat}${obj}`] = `${cat}.${obj}.${newval}`;
+ });
+ });
+ formControl.setValue("Permissions", newPermissions);
+ }
+ }, [setDefaults, updateDefaults]);
+
+ useEffect(() => {
+ var alltenant = false;
+ selectedTenant?.map((tenant) => {
+ if (tenant?.value === "AllTenants") {
+ alltenant = true;
+ }
+ });
+ if (alltenant) {
+ setAllTenantSelected(true);
+ } else {
+ setAllTenantSelected(false);
+ }
+ }, [selectedTenant, blockedTenants]);
+
+ useEffect(() => {
+ if (selectedRole) {
+ formControl.setValue("RoleName", selectedRole);
+ }
+ }, [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);
+ }
+ });
+
+ updatePermissions.mutate({
+ url: "/api/ExecCustomRole?Action=AddUpdate",
+ data: {
+ RoleName: values?.["RoleName"],
+ Permissions: selectedPermissions,
+ EntraGroup: selectedEntraGroup,
+ AllowedTenants: allowedTenantIds,
+ BlockedTenants: blockedTenantIds,
+ },
+ });
+ };
+
+ const ApiPermissionRow = ({ obj, cat, readOnly }) => {
+ const [offcanvasVisible, setOffcanvasVisible] = useState(false);
+
+ var items = [];
+ for (var key in apiPermissions[cat][obj])
+ for (var key2 in apiPermissions[cat][obj][key]) {
+ items.push({ heading: "", content: apiPermissions[cat][obj][key][key2] });
+ }
+ var group = [{ items: items }];
+
+ return (
+
+ {obj}
+
+ setOffcanvasVisible(true)} size="sm" color="info">
+
+
+
+
+
+
+ {
+ setOffcanvasVisible(false);
+ }}
+ >
+
+
+ {`${cat}.${obj}`}
+
+
+ Listed below are the available API endpoints based on permission level, ReadWrite
+ level includes endpoints under Read.
+
+ {[apiPermissions[cat][obj]].map((permissions, key) => {
+ var sections = Object.keys(permissions).map((type) => {
+ var items = [];
+ for (var api in permissions[type]) {
+ items.push({ heading: "", content: permissions[type][api] });
+ }
+ return (
+
+ {type}
+
+ {items.map((item, idx) => (
+
+ {item.content}
+
+ ))}
+
+
+ );
+ });
+ return sections;
+ })}
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+ {!selectedRole && (
+
+ )}
+ {selectedRole && isBaseRole && ["admin", "superadmin"].includes(selectedRole) && (
+ }>
+ This is a highly privileged role and overrides any custom role restrictions.
+
+ )}
+ {cippApiRoleSelected && (
+
+ This is the default role for all API clients in the CIPP-API integration. If you
+ would like different permissions for specific applications, create a role per
+ application and select it from the CIPP-API integrations page.
+
+ )}
+
+
+ {!isBaseRole && (
+ <>
+
+
+ {allTenantSelected && blockedTenants?.length == 0 && (
+
+ All tenants selected, no tenant restrictions will be applied unless blocked
+ tenants are specified.
+
+ )}
+
+ {allTenantSelected && (
+
+
+
+ )}
+ >
+ )}
+ {apiPermissionFetching && }
+ {apiPermissionSuccess && (
+ <>
+ API Permissions
+ {!isBaseRole && (
+
+ Set All Permissions
+
+
+
+
+
+ )}
+
+ <>
+ {Object.keys(apiPermissions)
+ .sort()
+ .map((cat, catIndex) => (
+
+ }>{cat}
+
+ {Object.keys(apiPermissions[cat])
+ .sort()
+ .map((obj, index) => {
+ const readOnly = baseRolePermissions?.[cat] ? true : false;
+ return (
+
+
+
+ );
+ })}
+
+
+ ))}
+ >
+
+ >
+ )}
+
+
+
+ {selectedEntraGroup && (
+
+ This role will be assigned to the Entra Group:{" "}
+ {selectedEntraGroup.label}
+
+ )}
+ {selectedTenant?.length > 0 && (
+ <>
+ Allowed Tenants
+
+ {selectedTenant.map((tenant, idx) => (
+ {tenant?.label}
+ ))}
+
+ >
+ )}
+ {blockedTenants?.length > 0 && (
+ <>
+ Blocked Tenants
+
+ {blockedTenants.map((tenant, idx) => (
+ {tenant?.label}
+ ))}
+
+ >
+ )}
+ {selectedPermissions && apiPermissionSuccess && (
+ <>
+ Selected Permissions
+
+ {selectedPermissions &&
+ Object.keys(selectedPermissions)
+ ?.sort()
+ .map((cat, idx) => (
+ <>
+ {selectedPermissions?.[cat] &&
+ typeof selectedPermissions[cat] === "string" &&
+ !selectedPermissions[cat]?.includes("None") && (
+ {selectedPermissions[cat]}
+ )}
+ >
+ ))}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ }
+ onClick={handleSubmit}
+ >
+ Save
+
+
+ >
+ );
+};
+
+export default CippRoleAddEdit;
diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx
new file mode 100644
index 000000000000..15766897d4f4
--- /dev/null
+++ b/src/components/CippSettings/CippRoles.jsx
@@ -0,0 +1,115 @@
+import React from "react";
+import { Box, Button, SvgIcon } from "@mui/material";
+import { CippDataTable } from "../CippTable/CippDataTable";
+import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
+import NextLink from "next/link";
+import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard";
+import { getCippTranslation } from "../../utils/get-cipp-translation";
+import { getCippFormatting } from "../../utils/get-cipp-formatting";
+import { Stack } from "@mui/system";
+import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard";
+
+const CippRoles = () => {
+ const actions = [
+ {
+ label: "Edit",
+ icon: (
+
+
+
+ ),
+ link: "/cipp/super-admin/cipp-roles/edit?role=[RoleName]",
+ },
+ {
+ label: "Delete",
+ icon: (
+
+
+
+ ),
+ confirmText: "Are you sure you want to delete this custom role?",
+ url: "/api/ExecCustomRole",
+ type: "POST",
+ data: {
+ Action: "Delete",
+ RoleName: "RoleName",
+ },
+ condition: (row) => row?.Type === "Custom",
+ relatedQueryKeys: ["customRoleList"],
+ },
+ ];
+
+ const offCanvas = {
+ children: (data) => {
+ const includeProps = ["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"];
+ const keys = includeProps.filter((key) => Object.keys(data).includes(key));
+ const properties = [];
+ keys.forEach((key) => {
+ if (data[key] && data[key].length > 0) {
+ properties.push({
+ label: getCippTranslation(key),
+ value: getCippFormatting(data[key], key),
+ });
+ }
+ });
+
+ if (data["Permissions"] && Object.keys(data["Permissions"]).length > 0) {
+ properties.push({
+ label: "Permissions",
+ value: (
+
+ {Object.keys(data["Permissions"])
+ .sort()
+ .map((permission, idx) => (
+
+
+
+ ))}
+
+ ),
+ });
+ }
+
+ return (
+
+ );
+ },
+ };
+
+ return (
+
+
+
+
+ }
+ component={NextLink}
+ href="/cipp/super-admin/cipp-roles/add"
+ >
+ Add Role
+
+ }
+ api={{
+ url: "/api/ListCustomRole",
+ }}
+ queryKey="customRoleTable"
+ simpleColumns={["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]}
+ offCanvas={offCanvas}
+ />
+
+ );
+};
+
+export default CippRoles;
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 011886bc4499..10e41a3dbb98 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -1,5 +1,6 @@
import { ApiGetCall } from "../api/ApiCall.jsx";
import UnauthenticatedPage from "../pages/unauthenticated.js";
+import LoadingPage from "../pages/loading.js";
export const PrivateRoute = ({ children, routeType }) => {
const {
@@ -7,27 +8,39 @@ export const PrivateRoute = ({ children, routeType }) => {
error,
isLoading,
} = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
refetchOnWindowFocus: true,
+ });
+
+ const session = ApiGetCall({
+ url: "/.auth/me",
+ queryKey: "authmeswa",
+ refetchOnWindowFocus: true,
staleTime: 120000, // 2 minutes
});
+ // if not logged into swa
+ if (null === session?.data?.clientPrincipal || session?.data === undefined) {
+ return ;
+ }
+
if (isLoading) {
- return "Loading...";
+ return ;
}
let roles = null;
- if (null !== profile?.clientPrincipal) {
- roles = profile?.clientPrincipal.userRoles;
- } else if (null === profile?.clientPrincipal) {
+
+ if (null !== profile?.clientPrincipal && undefined !== profile) {
+ roles = profile?.clientPrincipal?.userRoles;
+ } else if (null === profile?.clientPrincipal || undefined === profile) {
return ;
}
if (null === roles) {
return ;
} else {
const blockedRoles = ["anonymous", "authenticated"];
- const userRoles = roles.filter((role) => !blockedRoles.includes(role));
+ const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? [];
const isAuthenticated = userRoles.length > 0 && !error;
const isAdmin = roles.includes("admin");
if (routeType === "admin") {
diff --git a/src/data/cipp-roles.json b/src/data/cipp-roles.json
new file mode 100644
index 000000000000..f95e32fa18c6
--- /dev/null
+++ b/src/data/cipp-roles.json
@@ -0,0 +1,23 @@
+{
+ "readonly": {
+ "include": ["*.Read"],
+ "exclude": ["CIPP.SuperAdmin.*"]
+ },
+ "editor": {
+ "include": ["*.Read", "*.ReadWrite"],
+ "exclude": [
+ "CIPP.SuperAdmin.*",
+ "CIPP.Admin.*",
+ "CIPP.AppSettings.*",
+ "Tenant.Standards.ReadWrite"
+ ]
+ },
+ "admin": {
+ "include": ["*"],
+ "exclude": ["CIPP.SuperAdmin.*"]
+ },
+ "superadmin": {
+ "include": ["*"],
+ "exclude": []
+ }
+}
diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js
index ab6b9a11155b..6f16a30e8788 100644
--- a/src/layouts/account-popover.js
+++ b/src/layouts/account-popover.js
@@ -38,10 +38,8 @@ export const AccountPopover = (props) => {
const popover = usePopover();
const orgData = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
const handleLogout = useCallback(async () => {
diff --git a/src/layouts/index.js b/src/layouts/index.js
index ddd1b2460645..5a6c9d86b701 100644
--- a/src/layouts/index.js
+++ b/src/layouts/index.js
@@ -82,13 +82,20 @@ export const Layout = (props) => {
const [menuItems, setMenuItems] = useState(nativeMenuItems);
const currentTenant = settings?.currentTenant;
const currentRole = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
staleTime: 120000,
refetchOnWindowFocus: true,
});
const [hideSidebar, setHideSidebar] = useState(false);
+ const swaStatus = ApiGetCall({
+ url: "/.auth/me",
+ queryKey: "authmeswa",
+ staleTime: 120000,
+ refetchOnWindowFocus: true,
+ });
+
useEffect(() => {
if (currentRole.isSuccess && !currentRole.isFetching) {
const userRoles = currentRole.data?.clientPrincipal?.userRoles;
@@ -118,8 +125,15 @@ export const Layout = (props) => {
const filteredMenu = filterItemsByRole(nativeMenuItems);
setMenuItems(filteredMenu);
+ } else if (
+ swaStatus.isLoading ||
+ swaStatus.data?.clientPrincipal === null ||
+ swaStatus.data === undefined ||
+ currentRole.isLoading
+ ) {
+ setHideSidebar(true);
}
- }, [currentRole.isSuccess]);
+ }, [currentRole.isSuccess, swaStatus.data, swaStatus.isLoading]);
const handleNavPin = useCallback(() => {
settings.handleUpdate({
@@ -181,11 +195,11 @@ export const Layout = (props) => {
});
useEffect(() => {
- if (version.isFetched && !alertsAPI.isFetched) {
+ if (!hideSidebar && version.isFetched && !alertsAPI.isFetched) {
alertsAPI.waiting = true;
alertsAPI.refetch();
}
- }, [version, alertsAPI]);
+ }, [version, alertsAPI, hideSidebar]);
useEffect(() => {
if (alertsAPI.isSuccess && !alertsAPI.isFetching) {
@@ -238,6 +252,27 @@ export const Layout = (props) => {
}}
>
+
+ Setup Wizard
+
+
+
+
+ {!setupCompleted && (
+
+
+
+ Setup has not been completed.
+ Start Wizard
+
+
+
+ )}
{(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? (
@@ -255,30 +290,7 @@ export const Layout = (props) => {
) : (
- <>
-
- Setup Wizard
-
-
-
-
- {!setupCompleted && (
-
-
-
- Setup has not been completed.
- Start Wizard
-
-
-
- )}
- {children}
- >
+ <>{children}>
)}
diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js
index ca6d32b13ed8..7f58bb1d5e95 100644
--- a/src/layouts/side-nav.js
+++ b/src/layouts/side-nav.js
@@ -15,7 +15,6 @@ const markOpenItems = (items, pathname) => {
return items.map((item) => {
const checkPath = !!(item.path && pathname);
const exactMatch = checkPath ? pathname === item.path : false;
- // Use startsWith for partial matches so that subpages not in the menu still keep parent open
const partialMatch = checkPath ? pathname.startsWith(item.path) : false;
let openImmediately = exactMatch;
@@ -24,11 +23,9 @@ const markOpenItems = (items, pathname) => {
if (newItems.length > 0) {
newItems = markOpenItems(newItems, pathname);
const childOpen = newItems.some((child) => child.openImmediately);
- // Parent should open if exactMatch, childOpen, or partialMatch
- openImmediately = openImmediately || childOpen || partialMatch;
+ openImmediately = openImmediately || childOpen || exactMatch; // Ensure parent opens if child is open
} else {
- // For leaf items, consider them open if exact or partial match
- openImmediately = openImmediately || partialMatch;
+ openImmediately = openImmediately || partialMatch; // Leaf items open on partial match
}
return {
@@ -47,8 +44,6 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname }) => {
const exactMatch = checkPath && pathname === item.path;
const partialMatch = checkPath && pathname.startsWith(item.path);
- // Consider item active if exactMatch or partialMatch for leaf items
- // For parent items, being active is determined by their children or openImmediately
const hasChildren = item.items && item.items.length > 0;
const isActive = exactMatch || (partialMatch && !hasChildren);
@@ -107,7 +102,7 @@ export const SideNav = (props) => {
const pathname = usePathname();
const [hovered, setHovered] = useState(false);
const collapse = !(pinned || hovered);
- const { data: profile } = ApiGetCall({ url: "/.auth/me", queryKey: "authmecipp" });
+ const { data: profile } = ApiGetCall({ url: "/api/me", queryKey: "authmecipp" });
// Preprocess items to mark which should be open
const processedItems = markOpenItems(items, pathname);
@@ -159,90 +154,96 @@ export const SideNav = (props) => {
const randomimg = randomSponsorImage();
return (
- {
- setHovered(true);
- },
- onMouseLeave: () => {
- setHovered(false);
- },
- sx: {
- backgroundColor: "background.default",
- height: `calc(100% - ${TOP_NAV_HEIGHT}px)`,
- overflowX: "hidden",
- top: TOP_NAV_HEIGHT,
- transition: "width 250ms ease-in-out",
- width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH,
- zIndex: (theme) => theme.zIndex.appBar - 100,
- },
- }}
- >
-
-
+ {profile?.clientPrincipal && profile?.clientPrincipal?.userRoles?.length > 2 && (
+ {
+ setHovered(true);
+ },
+ onMouseLeave: () => {
+ setHovered(false);
+ },
+ sx: {
+ backgroundColor: "background.default",
+ height: `calc(100% - ${TOP_NAV_HEIGHT}px)`,
+ overflowX: "hidden",
+ top: TOP_NAV_HEIGHT,
+ transition: "width 250ms ease-in-out",
+ width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH,
+ zIndex: (theme) => theme.zIndex.appBar - 100,
+ },
}}
>
-
- {renderItems({
- collapse,
- depth: 0,
- items: processedItems,
- pathname,
- })}
-
- {profile?.clientPrincipal && (
- <>
-
-
- This application is sponsored by
-
+
- window.open(randomimg.link)}
- width={"100px"}
- />
-
- >
- )}
-
-
-
+ {renderItems({
+ collapse,
+ depth: 0,
+ items: processedItems,
+ pathname,
+ })}
+ {" "}
+ {/* Add this closing tag */}
+ {profile?.clientPrincipal && (
+ <>
+
+
+ This application is sponsored by
+
+
+ window.open(randomimg.link)}
+ width={"100px"}
+ />
+
+ >
+ )}
+ {" "}
+ {/* Closing tag for the parent Box */}
+
+
+ )}
+ >
);
};
diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js
index 40ccac234133..219f9f043ad7 100644
--- a/src/pages/cipp/preferences.js
+++ b/src/pages/cipp/preferences.js
@@ -17,10 +17,8 @@ const Page = () => {
const formcontrol = useForm({ mode: "onChange", defaultValues: settings });
const auth = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
const addedAttributes = [
@@ -92,7 +90,6 @@ const Page = () => {
value: (
{
{
+ return (
+
+
+
+
+ Create a new custom role with specific permissions and settings.
+
+
+
+
+
+ );
+};
+
+AddRolePage.getLayout = (page) => {page} ;
+
+export default AddRolePage;
diff --git a/src/pages/cipp/super-admin/cipp-roles/edit.js b/src/pages/cipp/super-admin/cipp-roles/edit.js
new file mode 100644
index 000000000000..85a4b2e0c431
--- /dev/null
+++ b/src/pages/cipp/super-admin/cipp-roles/edit.js
@@ -0,0 +1,28 @@
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { CippRoleAddEdit } from "/src/components/CippSettings/CippRoleAddEdit";
+import { CardContent, Stack, Alert } from "@mui/material";
+
+const EditRolePage = () => {
+ const router = useRouter();
+ const { role } = router.query;
+
+ return (
+
+
+
+
+ Editing an existing role will update the permissions for all users assigned to this
+ role.
+
+
+
+
+
+ );
+};
+
+EditRolePage.getLayout = (page) => {page} ;
+
+export default EditRolePage;
diff --git a/src/pages/cipp/super-admin/custom-roles.js b/src/pages/cipp/super-admin/cipp-roles/index.js
similarity index 79%
rename from src/pages/cipp/super-admin/custom-roles.js
rename to src/pages/cipp/super-admin/cipp-roles/index.js
index 76e95c16241b..9f2ea69fbfa5 100644
--- a/src/pages/cipp/super-admin/custom-roles.js
+++ b/src/pages/cipp/super-admin/cipp-roles/index.js
@@ -1,18 +1,18 @@
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import tabOptions from "./tabOptions";
+import tabOptions from "../tabOptions";
import CippPageCard from "/src/components/CippCards/CippPageCard";
-import { CippCustomRoles } from "/src/components/CippSettings/CippCustomRoles";
+import CippRoles from "/src/components/CippSettings/CippRoles";
import { Alert, CardContent, Stack, Typography } from "@mui/material";
import { WarningAmberOutlined } from "@mui/icons-material";
const Page = () => {
return (
-
+
- Custom roles can be used to restrict permissions for users with the 'editor' or
+ CIPP roles can be used to restrict permissions for users with the 'editor' or
'readonly' roles in CIPP. They can be limited to a subset of tenants and API
permissions. To restrict direct API access, create a role with the name 'CIPP-API'.
@@ -20,7 +20,7 @@ const Page = () => {
This functionality is in beta and should be treated as such. The custom role must be
added to the user in SWA in conjunction with the base role. (e.g. editor,mycustomrole)
-
+
diff --git a/src/pages/cipp/super-admin/tabOptions.json b/src/pages/cipp/super-admin/tabOptions.json
index 9c1e7af7eb16..8697d8c2c5e2 100644
--- a/src/pages/cipp/super-admin/tabOptions.json
+++ b/src/pages/cipp/super-admin/tabOptions.json
@@ -8,8 +8,8 @@
"path": "/cipp/super-admin/function-offloading"
},
{
- "label": "Custom Roles",
- "path": "/cipp/super-admin/custom-roles"
+ "label": "CIPP Roles",
+ "path": "/cipp/super-admin/cipp-roles"
},
{
"label": "SAM App Roles",
diff --git a/src/pages/loading.js b/src/pages/loading.js
new file mode 100644
index 000000000000..d5d457531db4
--- /dev/null
+++ b/src/pages/loading.js
@@ -0,0 +1,71 @@
+import { Box, Container, Grid, Stack } from "@mui/material";
+import Head from "next/head";
+import { CippImageCard } from "../components/CippCards/CippImageCard";
+import { Layout as DashboardLayout } from "../layouts/index.js";
+import { ApiGetCall } from "../api/ApiCall";
+import { useState, useEffect } from "react";
+
+const Page = () => {
+ const [loadingText, setLoadingText] = useState("Please wait while we log you in...");
+ const orgData = ApiGetCall({
+ url: "/api/me",
+ queryKey: "authmecipp",
+ });
+
+ const [loadingImage, setLoadingImage] = useState(
+ "/assets/illustrations/undraw_analysis_dq08.svg"
+ );
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (!orgData.isSuccess) {
+ setLoadingText(
+ "The function app may be experiencing a cold start currently, this can take a little longer than usual..."
+ );
+ setLoadingImage("/assets/illustrations/undraw-into-the-night-nd84.svg");
+ }
+ }, 20000); // 20 seconds
+
+ return () => clearTimeout(timer);
+ }, [orgData.isSuccess]);
+
+ return (
+ <>
+
+
+ Loading
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Page;
diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js
index c2cb0d5d4352..ff575a2c7da1 100644
--- a/src/pages/unauthenticated.js
+++ b/src/pages/unauthenticated.js
@@ -3,27 +3,32 @@ import Head from "next/head";
import { CippImageCard } from "../components/CippCards/CippImageCard";
import { Layout as DashboardLayout } from "../layouts/index.js";
import { ApiGetCall } from "../api/ApiCall";
-import { useState, useEffect, useMemo } from "react";
+import { useState, useEffect } from "react";
const Page = () => {
const orgData = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
+ });
+
+ const swaStatus = ApiGetCall({
+ url: "/.auth/me",
+ queryKey: "authmeswa",
staleTime: 120000,
refetchOnWindowFocus: true,
});
- const blockedRoles = useMemo(() => ["anonymous", "authenticated"], []);
+
+ const blockedRoles = ["anonymous", "authenticated"];
const [userRoles, setUserRoles] = useState([]);
- const userRolesData = orgData.data?.clientPrincipal?.userRoles;
useEffect(() => {
- if (orgData.isSuccess && userRolesData) {
- const roles = userRolesData.filter(
+ if (orgData.isSuccess) {
+ const roles = orgData.data?.clientPrincipal?.userRoles.filter(
(role) => !blockedRoles.includes(role)
);
setUserRoles(roles ?? []);
}
- }, [orgData.isSuccess, userRolesData, blockedRoles]);
+ }, [orgData, blockedRoles]);
return (
<>
@@ -47,14 +52,24 @@ const Page = () => {
sx={{ height: "100%" }} // Ensure the container takes full height
>
- {orgData.isSuccess && Array.isArray(userRoles) && (
+ {(orgData.isSuccess || swaStatus.isSuccess) && Array.isArray(userRoles) && (
0 ? "Return to Home" : "Login"}
- link={userRoles.length > 0 ? "/" : `/.auth/login/aad?post_login_redirect_uri=${encodeURIComponent(window.location.href)}`}
+ linkText={
+ swaStatus?.data?.clientPrincipal !== null && userRoles.length > 0
+ ? "Return to Home"
+ : "Login"
+ }
+ link={
+ swaStatus?.data?.clientPrincipal !== null && userRoles.length > 0
+ ? "/"
+ : `/.auth/login/aad?post_login_redirect_uri=${encodeURIComponent(
+ window.location.href
+ )}`
+ }
/>
)}
diff --git a/staticwebapp.config.json b/staticwebapp.config.json
index e82c3b401e9e..a19bc9f9580c 100644
--- a/staticwebapp.config.json
+++ b/staticwebapp.config.json
@@ -36,15 +36,7 @@
},
{
"route": "/api/ExecSAMSetup",
- "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"]
- },
- {
- "route": "/api/AddStandardTemplate",
- "allowedRoles": ["admin"]
- },
- {
- "route": "/api/RemoveStandardTemplate",
- "allowedRoles": ["admin"]
+ "allowedRoles": ["authenticated", "anonymous"]
},
{
"route": "/LogoutRedirect",
@@ -54,53 +46,13 @@
"route": "/404",
"allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"]
},
- {
- "route": "/api/RemoveStandard",
- "allowedRoles": ["admin"]
- },
- {
- "route": "/api/add*",
- "allowedRoles": ["admin", "editor"]
- },
- {
- "route": "/api/edit*",
- "allowedRoles": ["admin", "editor"]
- },
- {
- "route": "/api/ExecSendPush",
- "allowedRoles": ["admin", "editor", "readonly"]
- },
- {
- "route": "/api/ExecExcludeTenant",
- "allowedRoles": ["admin"]
- },
- {
- "route": "/api/Exec*",
- "allowedRoles": ["admin", "editor"]
- },
- {
- "route": "/api/Remove*",
- "allowedRoles": ["admin", "editor"]
- },
- {
- "route": "/cipp/*",
- "allowedRoles": ["admin"]
- },
- {
- "route": "/tenant/standards/*",
- "allowedRoles": ["admin"]
- },
- {
- "route": "/",
- "allowedRoles": ["admin", "editor", "readonly", "reader"]
- },
{
"route": "/api/Public*",
- "allowedRoles": ["admin", "editor", "readonly", "reader", "authenticated", "anonymous"]
+ "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"]
},
{
"route": "*",
- "allowedRoles": ["admin", "editor", "readonly", "reader"]
+ "allowedRoles": ["admin", "editor", "readonly", "authenticated"]
}
],
"navigationFallback": {
From 9de972cb7821212c7ec26c50f226c65163db2753 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 12:52:16 -0400
Subject: [PATCH 014/865] add refresh button
---
src/pages/tenant/gdap-management/onboarding/start.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js
index 0880f78a5192..3a1317117610 100644
--- a/src/pages/tenant/gdap-management/onboarding/start.js
+++ b/src/pages/tenant/gdap-management/onboarding/start.js
@@ -334,6 +334,7 @@ const Page = () => {
!relationship?.addedFields?.displayName?.startsWith("MLT_")
);
},
+ showRefresh: true,
}}
multiple={false}
creatable={true}
From f7a2cd43dc423cd38fb054e226f6ef6472f03cf5 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 13:04:39 -0400
Subject: [PATCH 015/865] Create undraw-into-the-night-nd84.svg
---
public/assets/illustrations/undraw-into-the-night-nd84.svg | 1 +
1 file changed, 1 insertion(+)
create mode 100644 public/assets/illustrations/undraw-into-the-night-nd84.svg
diff --git a/public/assets/illustrations/undraw-into-the-night-nd84.svg b/public/assets/illustrations/undraw-into-the-night-nd84.svg
new file mode 100644
index 000000000000..a7d05162299b
--- /dev/null
+++ b/public/assets/illustrations/undraw-into-the-night-nd84.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
From a120df868e0459b01b1d17b1bad813e547fda8d2 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 14:00:42 -0400
Subject: [PATCH 016/865] persist query client tweaks
---
src/components/PrivateRoute.js | 13 ++++++++++++-
src/pages/_app.js | 15 +++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 10e41a3dbb98..1dd5a18acfeb 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -7,6 +7,8 @@ export const PrivateRoute = ({ children, routeType }) => {
data: profile,
error,
isLoading,
+ isSuccess,
+ refetch,
} = ApiGetCall({
url: "/api/me",
queryKey: "authmecipp",
@@ -31,6 +33,15 @@ export const PrivateRoute = ({ children, routeType }) => {
let roles = null;
+ if (
+ session?.isSuccess &&
+ isSuccess &&
+ session?.data?.clientPrincipal?.userDetails !== profile?.userDetails
+ ) {
+ // refetch the profile if the user details are different
+ refetch();
+ }
+
if (null !== profile?.clientPrincipal && undefined !== profile) {
roles = profile?.clientPrincipal?.userRoles;
} else if (null === profile?.clientPrincipal || undefined === profile) {
@@ -42,7 +53,7 @@ export const PrivateRoute = ({ children, routeType }) => {
const blockedRoles = ["anonymous", "authenticated"];
const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? [];
const isAuthenticated = userRoles.length > 0 && !error;
- const isAdmin = roles.includes("admin");
+ const isAdmin = roles.includes("admin") || roles.includes("superadmin");
if (routeType === "admin") {
return !isAdmin ? : children;
} else {
diff --git a/src/pages/_app.js b/src/pages/_app.js
index 6d585563825b..f8a805f3180d 100644
--- a/src/pages/_app.js
+++ b/src/pages/_app.js
@@ -46,6 +46,8 @@ const App = (props) => {
const pathname = usePathname();
const route = useRouter();
+ const excludeQueryKeys = ["authmeswa"];
+
// 👇 Persist TanStack Query cache to localStorage
useEffect(() => {
if (typeof window !== "undefined") {
@@ -59,6 +61,19 @@ const App = (props) => {
maxAge: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // optional: 5 minutes
buster: "v1",
+ dehydrateOptions: {
+ shouldDehydrateQuery: (query) => {
+ const queryIsReadyForPersistance = query.state.status === "success";
+ if (queryIsReadyForPersistance) {
+ const { queryKey } = query;
+ const excludeFromPersisting = excludeQueryKeys.some((key) =>
+ queryKey[0].toString().includes(key)
+ );
+ return !excludeFromPersisting;
+ }
+ return queryIsReadyForPersistance;
+ },
+ },
});
}
}, []);
From 3084940bc1cc3e9df2e5bf65210f49d811988db1 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 14:08:17 -0400
Subject: [PATCH 017/865] Update account-popover.js
---
src/layouts/account-popover.js | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js
index 6f16a30e8788..f09a1a575b4f 100644
--- a/src/layouts/account-popover.js
+++ b/src/layouts/account-popover.js
@@ -15,6 +15,7 @@ import {
ListItemIcon,
ListItemText,
Popover,
+ Skeleton,
Stack,
SvgIcon,
Typography,
@@ -87,10 +88,12 @@ export const AccountPopover = (props) => {
<>
- {orgData.data?.Org?.Domain}
+ {orgData?.isFetching ? "Loading..." : orgData.data?.clientPrincipal?.userDetails?.split('@')[1]}
- {orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"}
+ {orgData?.isFetching
+ ?
+ : orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"}
{orgData.data?.clientPrincipal?.userDetails && (
From 4511530825f673f7e9bb9c274368422e9bf632f8 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 19 May 2025 21:38:10 +0200
Subject: [PATCH 018/865] tenant stuff
---
src/pages/cipp/settings/tenants.js | 9 +++++----
src/utils/get-cipp-formatting.js | 7 ++++---
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/pages/cipp/settings/tenants.js b/src/pages/cipp/settings/tenants.js
index 320e22fa5add..b1e15787e154 100644
--- a/src/pages/cipp/settings/tenants.js
+++ b/src/pages/cipp/settings/tenants.js
@@ -21,7 +21,7 @@ const Page = () => {
data: { value: "customerId" },
confirmText: "Are you sure you want to exclude these tenants?",
multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
+ condition: (row) => row.displayName !== "*Partner Tenant",
},
{
label: "Include Tenants",
@@ -31,7 +31,7 @@ const Page = () => {
data: { value: "customerId" },
confirmText: "Are you sure you want to include these tenants?",
multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
+ condition: (row) => row.displayName !== "*Partner Tenant",
},
{
label: "Refresh CPV Permissions",
@@ -51,7 +51,7 @@ const Page = () => {
confirmText:
"Are you sure you want to reset the CPV permissions for these tenants? (This will delete the Service Principal and re-add it.)",
multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
+ condition: (row) => row.displayName !== "*Partner Tenant",
},
{
label: "Remove Tenant",
@@ -61,7 +61,7 @@ const Page = () => {
data: { TenantID: "customerId" },
confirmText: "Are you sure you want to remove this tenant?",
multiPost: false,
- condition: (row) => row.displayName !== '*Partner Tenant',
+ condition: (row) => row.displayName !== "*Partner Tenant",
},
];
@@ -70,6 +70,7 @@ const Page = () => {
extendedInfoFields: [
"displayName",
"defaultDomainName",
+ "delegatedPrivilegeStatus",
"Excluded",
"ExcludeDate",
"ExcludeUser",
diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js
index 1cfdb1decd38..fac1ea1aae28 100644
--- a/src/utils/get-cipp-formatting.js
+++ b/src/utils/get-cipp-formatting.js
@@ -6,9 +6,6 @@ import {
MailOutline,
Shield,
Description,
- Group,
- MeetingRoom,
- GroupWork,
GroupOutlined,
} from "@mui/icons-material";
import { Chip, Link, SvgIcon } from "@mui/material";
@@ -239,6 +236,10 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
return isText ? data : ;
}
+ if (cellName === "delegatedPrivilegeStatus") {
+ return data === "directTenant" ? "Direct Tenant" : "GDAP Tenant";
+ }
+
//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") {
//check if data is an array.
From 09f229f5bef0a2b31b57c50339ff7ab189051cd4 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 19 May 2025 22:23:04 +0200
Subject: [PATCH 019/865] added tenant state
---
src/pages/cipp/settings/tenants.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/pages/cipp/settings/tenants.js b/src/pages/cipp/settings/tenants.js
index b1e15787e154..000f1d7b5ec5 100644
--- a/src/pages/cipp/settings/tenants.js
+++ b/src/pages/cipp/settings/tenants.js
@@ -41,6 +41,7 @@ const Page = () => {
data: { tenantFilter: "customerId" },
confirmText: "Are you sure you want to refresh the CPV permissions for these tenants?",
multiPost: false,
+ condition: (row) => row.delegatedPrivilegeStatus !== "directTenant",
},
{
label: "Reset CPV Permissions",
@@ -51,7 +52,8 @@ const Page = () => {
confirmText:
"Are you sure you want to reset the CPV permissions for these tenants? (This will delete the Service Principal and re-add it.)",
multiPost: false,
- condition: (row) => row.displayName !== "*Partner Tenant",
+ condition: (row) =>
+ row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant",
},
{
label: "Remove Tenant",
From 3742c4cf52c13dbf04a5b20321efbb63e081709a Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 16:35:37 -0400
Subject: [PATCH 020/865] tweak auth refresh
---
src/components/PrivateRoute.js | 1 -
src/layouts/account-popover.js | 19 ++++++++++++-------
src/layouts/index.js | 2 --
3 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 1dd5a18acfeb..069d33822c09 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -12,7 +12,6 @@ export const PrivateRoute = ({ children, routeType }) => {
} = ApiGetCall({
url: "/api/me",
queryKey: "authmecipp",
- refetchOnWindowFocus: true,
});
const session = ApiGetCall({
diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js
index f09a1a575b4f..88830a672145 100644
--- a/src/layouts/account-popover.js
+++ b/src/layouts/account-popover.js
@@ -9,6 +9,7 @@ import SunIcon from "@heroicons/react/24/outline/SunIcon";
import {
Avatar,
Box,
+ CircularProgress,
List,
ListItem,
ListItemButton,
@@ -88,18 +89,22 @@ export const AccountPopover = (props) => {
<>
- {orgData?.isFetching ? "Loading..." : orgData.data?.clientPrincipal?.userDetails?.split('@')[1]}
+ {orgData.data?.clientPrincipal?.userDetails?.split("@")?.[1]}
- {orgData?.isFetching
- ?
- : orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"}
+ {orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"}
{orgData.data?.clientPrincipal?.userDetails && (
-
-
-
+ <>
+ {orgData?.isFetching ? (
+
+ ) : (
+
+
+
+ )}
+ >
)}
>
)}
diff --git a/src/layouts/index.js b/src/layouts/index.js
index 5a6c9d86b701..6603b874f2e5 100644
--- a/src/layouts/index.js
+++ b/src/layouts/index.js
@@ -84,8 +84,6 @@ export const Layout = (props) => {
const currentRole = ApiGetCall({
url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
const [hideSidebar, setHideSidebar] = useState(false);
From 8bb150b59bcab40578d3ec2c56c3cb6b646d7f6c Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 16:50:24 -0400
Subject: [PATCH 021/865] auth refresh tweak
---
src/components/PrivateRoute.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 069d33822c09..adbfe2c0b4d5 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -35,6 +35,9 @@ export const PrivateRoute = ({ children, routeType }) => {
if (
session?.isSuccess &&
isSuccess &&
+ undefined !== profile &&
+ session?.data?.clientPrincipal?.userDetails &&
+ profile?.userDetails &&
session?.data?.clientPrincipal?.userDetails !== profile?.userDetails
) {
// refetch the profile if the user details are different
From 6e33095da668d87b59d732b6ecc62fd847bb91f5 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 16:57:15 -0400
Subject: [PATCH 022/865] tweak auth checks
---
src/components/PrivateRoute.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index adbfe2c0b4d5..3541eeda2d96 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -21,15 +21,16 @@ export const PrivateRoute = ({ children, routeType }) => {
staleTime: 120000, // 2 minutes
});
+ // Check if the session is still loading before determining authentication status
+ if (session.isLoading || isLoading) {
+ return ;
+ }
+
// if not logged into swa
if (null === session?.data?.clientPrincipal || session?.data === undefined) {
return ;
}
- if (isLoading) {
- return ;
- }
-
let roles = null;
if (
From 356ba423b5ef1e56c7eda8f0c6c980c8cda6b8d2 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 17:08:55 -0400
Subject: [PATCH 023/865] application pages
---
src/layouts/config.js | 6 +-
.../applications/app-registrations.js | 84 +++++++++++++++++++
.../enterprise-apps.js} | 8 +-
.../applications/tabOptions.json | 10 +++
4 files changed, 104 insertions(+), 4 deletions(-)
create mode 100644 src/pages/tenant/administration/applications/app-registrations.js
rename src/pages/tenant/administration/{enterprise-apps/index.js => applications/enterprise-apps.js} (89%)
create mode 100644 src/pages/tenant/administration/applications/tabOptions.json
diff --git a/src/layouts/config.js b/src/layouts/config.js
index b1d9e72b9623..3933af9fbaaa 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -87,8 +87,8 @@ export const nativeMenuItems = [
},
{ title: "Audit Logs", path: "/tenant/administration/audit-logs" },
{
- title: "Enterprise Applications",
- path: "/tenant/administration/enterprise-apps",
+ title: "Applications",
+ path: "/tenant/administration/applications/enterprise-apps",
},
{ title: "Secure Score", path: "/tenant/administration/securescore" },
{
@@ -470,7 +470,7 @@ export const nativeMenuItems = [
{
title: "Custom Data",
path: "/cipp/custom-data/directory-extensions",
- roles: ["admin", "superadmin"]
+ roles: ["admin", "superadmin"],
},
{
title: "Advanced",
diff --git a/src/pages/tenant/administration/applications/app-registrations.js b/src/pages/tenant/administration/applications/app-registrations.js
new file mode 100644
index 000000000000..489b64ce7b5b
--- /dev/null
+++ b/src/pages/tenant/administration/applications/app-registrations.js
@@ -0,0 +1,84 @@
+// this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83
+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 tabOptions from "./tabOptions";
+
+const Page = () => {
+ const pageTitle = "App Registrations";
+ const apiUrl = "/api/ListGraphRequest";
+
+ const actions = [
+ {
+ icon: ,
+ label: "View App Registration",
+ link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/[appId]`,
+ color: "info",
+ target: "_blank",
+ multiPost: false,
+ external: true,
+ },
+ {
+ icon: ,
+ label: "View API Permissions",
+ link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/ApiPermissions/appId/[appId]`,
+ color: "info",
+ target: "_blank",
+ multiPost: false,
+ external: true,
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "displayName",
+ "id",
+ "appId",
+ "createdDateTime",
+ "signInAudience",
+ "replyUrls",
+ "requiredResourceAccess",
+ "web",
+ "api",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "displayName",
+ "appId",
+ "createdDateTime",
+ "signInAudience",
+ "web.redirectUris",
+ "publisherDomain",
+ ];
+
+ const apiParams = {
+ Endpoint: "applications",
+ $select:
+ "id,appId,displayName,createdDateTime,signInAudience,web,api,requiredResourceAccess,publisherDomain,replyUrls",
+ $count: true,
+ $top: 999,
+ };
+
+ return (
+
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/tenant/administration/enterprise-apps/index.js b/src/pages/tenant/administration/applications/enterprise-apps.js
similarity index 89%
rename from src/pages/tenant/administration/enterprise-apps/index.js
rename to src/pages/tenant/administration/applications/enterprise-apps.js
index 258e09ce23d8..14861d14f4ea 100644
--- a/src/pages/tenant/administration/enterprise-apps/index.js
+++ b/src/pages/tenant/administration/applications/enterprise-apps.js
@@ -1,7 +1,9 @@
// this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83
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 tabOptions from "./tabOptions";
const Page = () => {
const pageTitle = "Enterprise Applications";
@@ -71,6 +73,10 @@ const Page = () => {
);
};
-Page.getLayout = (page) => {page} ;
+Page.getLayout = (page) => (
+
+ {page}
+
+);
export default Page;
diff --git a/src/pages/tenant/administration/applications/tabOptions.json b/src/pages/tenant/administration/applications/tabOptions.json
new file mode 100644
index 000000000000..77f3bc9074a5
--- /dev/null
+++ b/src/pages/tenant/administration/applications/tabOptions.json
@@ -0,0 +1,10 @@
+[
+ {
+ "label": "Enterprise Apps",
+ "path": "/tenant/administration/applications/enterprise-apps"
+ },
+ {
+ "label": "App Registrations",
+ "path": "/tenant/administration/applications/app-registrations"
+ }
+]
\ No newline at end of file
From 878c104e5312acebff3546172f91f39b2ec178b0 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 20:07:38 -0400
Subject: [PATCH 024/865] application permission sets
---
.../CippAppPermissionBuilder.jsx | 5 +-
.../applications/app-registrations.js | 8 +-
.../applications/enterprise-apps.js | 14 +-
.../applications/permission-sets/add.js | 224 ++++++++++++++++++
.../applications/permission-sets/edit.js | 153 ++++++++++++
.../applications/permission-sets/index.js | 75 ++++++
.../applications/tabOptions.json | 4 +
7 files changed, 466 insertions(+), 17 deletions(-)
create mode 100644 src/pages/tenant/administration/applications/permission-sets/add.js
create mode 100644 src/pages/tenant/administration/applications/permission-sets/edit.js
create mode 100644 src/pages/tenant/administration/applications/permission-sets/index.js
diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx
index 7fa7099fb4c3..92736b31da2d 100644
--- a/src/components/CippComponents/CippAppPermissionBuilder.jsx
+++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx
@@ -1073,10 +1073,7 @@ const CippAppPermissionBuilder = ({
}
type="submit"
- disabled={
- updatePermissions.isPending ||
- _.isEqual(currentPermissions.Permissions, newPermissions.Permissions)
- }
+ disabled={updatePermissions.isPending}
onClick={handleSubmit}
>
Save
diff --git a/src/pages/tenant/administration/applications/app-registrations.js b/src/pages/tenant/administration/applications/app-registrations.js
index 489b64ce7b5b..ff9a643f1e92 100644
--- a/src/pages/tenant/administration/applications/app-registrations.js
+++ b/src/pages/tenant/administration/applications/app-registrations.js
@@ -22,7 +22,7 @@ const Page = () => {
{
icon: ,
label: "View API Permissions",
- link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/ApiPermissions/appId/[appId]`,
+ link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/[appId]`,
color: "info",
target: "_blank",
multiPost: false,
@@ -41,6 +41,8 @@ const Page = () => {
"requiredResourceAccess",
"web",
"api",
+ "passwordCredentials",
+ "keyCredentials",
],
actions: actions,
};
@@ -52,12 +54,14 @@ const Page = () => {
"signInAudience",
"web.redirectUris",
"publisherDomain",
+ "passwordCredentials",
+ "keyCredentials",
];
const apiParams = {
Endpoint: "applications",
$select:
- "id,appId,displayName,createdDateTime,signInAudience,web,api,requiredResourceAccess,publisherDomain,replyUrls",
+ "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 14861d14f4ea..b7c80840c4e4 100644
--- a/src/pages/tenant/administration/applications/enterprise-apps.js
+++ b/src/pages/tenant/administration/applications/enterprise-apps.js
@@ -19,16 +19,6 @@ const Page = () => {
multiPost: false,
external: true,
},
- {
- icon: ,
- label: "View App Registration",
- link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/[appId]`,
- color: "info",
- target: "_blank",
- multiPost: false,
- external: true,
- condition: (row) => row.tags.includes("WindowsAzureActiveDirectoryIntegratedApp"),
- },
];
const offCanvas = {
@@ -50,12 +40,14 @@ const Page = () => {
"createdDateTime",
"publisherName",
"homepage",
+ "passwordCredentials",
+ "keyCredentials",
];
const apiParams = {
Endpoint: "servicePrincipals",
$select:
- "id,appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags",
+ "id,appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags,passwordCredentials,keyCredentials",
$count: true,
$top: 999,
};
diff --git a/src/pages/tenant/administration/applications/permission-sets/add.js b/src/pages/tenant/administration/applications/permission-sets/add.js
new file mode 100644
index 000000000000..e81955249ed8
--- /dev/null
+++ b/src/pages/tenant/administration/applications/permission-sets/add.js
@@ -0,0 +1,224 @@
+import { useRouter } from "next/router";
+import { TabbedLayout } from "/src/layouts/TabbedLayout";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import tabOptions from "../tabOptions";
+import { useForm } from "react-hook-form";
+import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
+import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { Alert, CardContent, Skeleton, Stack, Typography, Button, Box } from "@mui/material";
+import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent";
+import { useEffect, useState } from "react";
+import { CopyAll } from "@mui/icons-material";
+
+const Page = () => {
+ const router = useRouter();
+ const { template, copy, name } = router.query;
+ const pageTitle = copy ? "Copy Permission Set" : "Add Permission Set";
+
+ const [initialPermissions, setInitialPermissions] = useState(null);
+ const [availableTemplates, setAvailableTemplates] = useState([]);
+ // Add refetch key for refreshing data after save
+ const [refetchKey, setRefetchKey] = useState(0);
+
+ const formControl = useForm({
+ mode: "onBlur",
+ });
+
+ // Get the specified template if template ID is provided
+ const { data: templateData, isLoading: templateLoading } = ApiGetCall({
+ url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null,
+ queryKey: template ? ["execAppPermissionTemplate", template, refetchKey] : null,
+ enabled: !!template,
+ });
+
+ // Get all available templates for importing
+ const { data: allTemplates, isLoading: templatesLoading } = ApiGetCall({
+ url: "/api/ExecAppPermissionTemplate",
+ queryKey: "execAppPermissionTemplate",
+ });
+
+ useEffect(() => {
+ if (allTemplates && allTemplates.length > 0) {
+ setAvailableTemplates(allTemplates);
+ }
+ }, [allTemplates]);
+
+ useEffect(() => {
+ // Initialize with empty structure for new templates
+ if (!template && !copy && !initialPermissions) {
+ setInitialPermissions({
+ Permissions: {},
+ TemplateName: "New Permission Set",
+ });
+ formControl.setValue("templateName", "New Permission Set");
+ } else if (templateData && copy && !initialPermissions) {
+ // When copying, we want to load the permissions but assign a new ID
+ if (templateData[0]) {
+ const copyName = `Copy of ${templateData[0].TemplateName}`;
+ setInitialPermissions({
+ Permissions: templateData[0].Permissions,
+ TemplateName: copyName,
+ });
+ formControl.setValue("templateName", copyName);
+ }
+ } else if (templateData && !initialPermissions) {
+ // For editing, keep the same ID
+ if (templateData[0]) {
+ setInitialPermissions({
+ TemplateId: templateData[0].TemplateId,
+ Permissions: templateData[0].Permissions,
+ TemplateName: templateData[0].TemplateName,
+ });
+ formControl.setValue("templateName", templateData[0].TemplateName);
+ }
+ }
+ }, [templateData, copy, template]);
+
+ const updatePermissions = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["ExecAppPermissionTemplate", "execAppPermissionTemplate"],
+ });
+
+ const handleUpdatePermissions = (data) => {
+ let payload = {
+ ...data,
+ };
+
+ if (copy) {
+ // For copy, ensure we're not sending the original ID
+ delete payload.TemplateId;
+ } else if (template && !copy) {
+ // For editing, include the template ID
+ payload.TemplateId = template;
+ }
+
+ // Use the current value from the text field
+ payload.TemplateName = formControl.getValues("templateName");
+
+ updatePermissions.mutate(
+ {
+ url: "/api/ExecAppPermissionTemplate?Action=Save",
+ data: payload,
+ queryKey: "execAppPermissionTemplate",
+ },
+ {
+ onSuccess: (data) => {
+ // Instead of navigating away, stay on the page and refresh
+ if (copy || !template) {
+ // If we're copying or creating new, update the URL to edit mode with the new template ID
+ const newTemplateId = data[0].TemplateId;
+ router.push(
+ {
+ pathname: "/tenant/administration/applications/permission-sets/edit",
+ query: {
+ template: newTemplateId,
+ name: payload.TemplateName,
+ },
+ },
+ undefined,
+ { shallow: true }
+ );
+ } else {
+ // Otherwise just refresh the current data
+ setRefetchKey((prev) => prev + 1);
+ }
+ },
+ }
+ );
+ };
+
+ const handleImportTemplate = () => {
+ const importTemplate = formControl.getValues("importTemplate");
+ if (!importTemplate) return;
+
+ const selectedTemplate = availableTemplates.find((t) => t.TemplateId === importTemplate.value);
+ if (selectedTemplate) {
+ setInitialPermissions({
+ Permissions: selectedTemplate.Permissions,
+ TemplateName: `Import of ${selectedTemplate.TemplateName}`,
+ });
+ formControl.setValue("templateName", `Import of ${selectedTemplate.TemplateName}`);
+ }
+ };
+
+ return (
+
+
+
+ {templateLoading && }
+ {(!templateLoading || !template) && (
+ <>
+
+ {copy
+ ? "Create a copy of an existing permission set with your own modifications."
+ : template
+ ? "Edit the permissions in this permission set."
+ : "Create a new permission set to define a collection of application permissions."}
+
+
+ Permission sets allow you to define collections of permissions that can be applied
+ to applications consistently.
+
+
+
+
+ {!template && !copy && (
+
+
+ ({
+ label: template.TemplateName,
+ value: template.TemplateId,
+ }))}
+ isFetching={templatesLoading}
+ multiple={false}
+ />
+
+ }
+ disabled={!formControl.watch("importTemplate")}
+ >
+ Import
+
+
+ )}
+
+ {initialPermissions && (
+
+ )}
+ >
+ )}
+
+
+
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/tenant/administration/applications/permission-sets/edit.js b/src/pages/tenant/administration/applications/permission-sets/edit.js
new file mode 100644
index 000000000000..7fdfc15f2696
--- /dev/null
+++ b/src/pages/tenant/administration/applications/permission-sets/edit.js
@@ -0,0 +1,153 @@
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { useForm } from "react-hook-form";
+import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
+import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { Alert, CardContent, Skeleton, Stack, Typography } from "@mui/material";
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import { Button } from "@mui/material";
+import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent";
+
+const Page = () => {
+ const router = useRouter();
+ const { template, name } = router.query;
+
+ const [initialPermissions, setInitialPermissions] = useState(null);
+
+ const formControl = useForm({
+ mode: "onBlur",
+ });
+
+ // Add a key to force refetch when we save
+ const [refetchKey, setRefetchKey] = useState(0);
+
+ const {
+ data: templateData,
+ isLoading,
+ refetch,
+ } = ApiGetCall({
+ url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null,
+ queryKey: template ? ["execAppPermissionTemplate", template, refetchKey] : null,
+ enabled: !!template,
+ });
+
+ const updatePermissions = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["ExecAppPermissionTemplate", "execAppPermissionTemplate"],
+ });
+
+ useEffect(() => {
+ if (templateData && templateData[0]) {
+ setInitialPermissions({
+ TemplateId: templateData[0].TemplateId,
+ Permissions: templateData[0].Permissions,
+ TemplateName: templateData[0].TemplateName,
+ });
+ formControl.setValue("templateName", templateData[0].TemplateName, {
+ shouldValidate: true,
+ shouldDirty: false,
+ });
+ }
+ }, [templateData]);
+
+ const handleUpdatePermissions = (data) => {
+ let payload = {
+ ...data,
+ TemplateId: template,
+ };
+
+ // Use the current value from the text field
+ payload.TemplateName = formControl.getValues("templateName");
+
+ updatePermissions.mutate(
+ {
+ url: "/api/ExecAppPermissionTemplate?Action=Save",
+ data: payload,
+ queryKey: "execAppPermissionTemplate",
+ },
+ {
+ onSuccess: () => {
+ // Refresh the data by incrementing the refetch key
+ setRefetchKey((prev) => prev + 1);
+
+ // Explicitly refetch the data
+ refetch();
+ },
+ }
+ );
+ };
+
+ // Instead of redirecting, we'll display an error message
+ if (!template) {
+ return (
+
+
+
+ The requested permission set does not exist or was not specified. Please select a valid
+ permission set from the list.
+
+
+ Back to Permission Sets
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {isLoading && }
+ {!isLoading && initialPermissions && (
+ <>
+
+ Modify the permissions in this permission set. Any changes will affect all
+ applications using this permission set.
+
+
+ Permission sets allow you to define collections of permissions that can be applied
+ to applications consistently.
+
+
+
+
+
+ >
+ )}
+
+
+
+ );
+};
+
+Page.getLayout = (page) => {page} ;
+
+export default Page;
diff --git a/src/pages/tenant/administration/applications/permission-sets/index.js b/src/pages/tenant/administration/applications/permission-sets/index.js
new file mode 100644
index 000000000000..41b02f844fc4
--- /dev/null
+++ b/src/pages/tenant/administration/applications/permission-sets/index.js
@@ -0,0 +1,75 @@
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { TabbedLayout } from "/src/layouts/TabbedLayout";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import { Edit, Delete, ContentCopy, Add } from "@mui/icons-material";
+import tabOptions from "../tabOptions";
+import { Button } from "@mui/material";
+import Link from "next/link";
+
+const Page = () => {
+ const pageTitle = "Permission Sets";
+ const apiUrl = "/api/ExecAppPermissionTemplate";
+
+ const actions = [
+ {
+ icon: ,
+ label: "Edit Permission Set",
+ color: "warning",
+ link: "/tenant/administration/applications/permission-sets/edit?template=[TemplateId]&name=[TemplateName]",
+ },
+ {
+ icon: ,
+ label: "Copy Permission Set",
+ color: "info",
+ link: "/tenant/administration/applications/permission-sets/add?template=[TemplateId]©=true&name=[TemplateName]",
+ },
+ {
+ icon: ,
+ label: "Delete Permission Set",
+ color: "danger",
+ url: apiUrl,
+ data: {
+ Action: "Delete",
+ TemplateId: "TemplateId",
+ },
+ type: "POST",
+ confirmText: "Are you sure you want to delete [TemplateName]?",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: ["TemplateName", "Permissions", "UpdatedBy", "Timestamp"],
+ actions: actions,
+ };
+
+ const simpleColumns = ["TemplateName", "Permissions", "UpdatedBy", "Timestamp"];
+
+ return (
+ }
+ >
+ Add Permission Set
+
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/tenant/administration/applications/tabOptions.json b/src/pages/tenant/administration/applications/tabOptions.json
index 77f3bc9074a5..bedc483e2e5f 100644
--- a/src/pages/tenant/administration/applications/tabOptions.json
+++ b/src/pages/tenant/administration/applications/tabOptions.json
@@ -6,5 +6,9 @@
{
"label": "App Registrations",
"path": "/tenant/administration/applications/app-registrations"
+ },
+ {
+ "label": "Permission Sets",
+ "path": "/tenant/administration/applications/permission-sets"
}
]
\ No newline at end of file
From 17c3ac0fcc81605a904a67e74c1124e5963d8f99 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 21:48:20 -0400
Subject: [PATCH 025/865] app deployment templates
---
.../AppDeploymentTemplateForm.jsx | 204 ++++++++++++++++++
.../applications/tabOptions.json | 4 +
.../applications/templates/add.js | 96 +++++++++
.../applications/templates/edit.js | 104 +++++++++
.../applications/templates/index.js | 89 ++++++++
5 files changed, 497 insertions(+)
create mode 100644 src/components/CippComponents/AppDeploymentTemplateForm.jsx
create mode 100644 src/pages/tenant/administration/applications/templates/add.js
create mode 100644 src/pages/tenant/administration/applications/templates/edit.js
create mode 100644 src/pages/tenant/administration/applications/templates/index.js
diff --git a/src/components/CippComponents/AppDeploymentTemplateForm.jsx b/src/components/CippComponents/AppDeploymentTemplateForm.jsx
new file mode 100644
index 000000000000..236c275ed08c
--- /dev/null
+++ b/src/components/CippComponents/AppDeploymentTemplateForm.jsx
@@ -0,0 +1,204 @@
+import React, { useState, useEffect } from "react";
+import { Alert, Skeleton, Stack, Typography, Button, Box } from "@mui/material";
+import { ApiGetCall } from "/src/api/ApiCall";
+import { CippFormComponent } from "./CippFormComponent";
+import { CippApiResults } from "./CippApiResults";
+
+const AppDeploymentTemplateForm = ({
+ formControl,
+ templateData,
+ templateLoading,
+ isEditing,
+ isCopy,
+ updatePermissions,
+ onSubmit,
+ refetchKey,
+}) => {
+ const [selectedPermissionSet, setSelectedPermissionSet] = useState(null);
+
+ // When templateData changes, update the form
+ useEffect(() => {
+ if (!isEditing && !isCopy) {
+ formControl.setValue("templateName", "New App Deployment Template");
+ formControl.setValue("appType", "EnterpriseApp");
+ } else if (templateData && isCopy) {
+ // When copying, we want to load the template data but not the ID
+ 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,
+ },
+ });
+ formControl.setValue("permissionSetId", {
+ label: templateData[0].PermissionSetName || "Custom Permissions",
+ value: templateData[0].PermissionSetId,
+ });
+ }
+ } 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,
+ },
+ });
+ formControl.setValue("permissionSetId", {
+ label: templateData[0].PermissionSetName || "Custom Permissions",
+ value: templateData[0].PermissionSetId,
+ });
+ }
+ }
+ }, [templateData, isCopy, isEditing, formControl]);
+
+ // Watch for app selection changes to update template name
+ const selectedApp = formControl.watch("appId");
+
+ useEffect(() => {
+ // Update template name when app is selected if we're in add mode and name hasn't been manually changed
+ if (selectedApp && !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];
+ if (appName) {
+ formControl.setValue("templateName", `${appName} Template`);
+ }
+ }
+ }
+ }, [selectedApp, isEditing, isCopy, formControl]);
+
+ // Handle form submission
+ const handleSubmit = (data) => {
+ // Remove console.log in production
+ const appDisplayName =
+ data.appId?.addedFields?.displayName ||
+ (data.appId?.label ? data.appId.label.split(" (")[0] : undefined);
+
+ const payload = {
+ TemplateName: data.templateName,
+ AppId: data.appId?.value,
+ AppName: appDisplayName,
+ PermissionSetId: data.permissionSetId?.value,
+ PermissionSetName: data.permissionSetId?.label,
+ Permissions: data.permissionSetId?.addedFields?.Permissions,
+ };
+
+ if (isEditing && !isCopy && templateData?.[0]?.TemplateId) {
+ payload.TemplateId = templateData[0].TemplateId;
+ }
+
+ // Store values before submission to set them back afterward
+ const currentValues = {
+ templateName: data.templateName,
+ appId: data.appId,
+ permissionSetId: data.permissionSetId,
+ };
+
+ onSubmit(payload);
+
+ // After submission, set the values back to what they were but mark as clean
+ // This will only apply to add page, as edit will get refreshed data
+ if (!isEditing) {
+ setTimeout(() => {
+ formControl.setValue("templateName", currentValues.templateName, { shouldDirty: false });
+ formControl.setValue("appId", currentValues.appId, { shouldDirty: false });
+ formControl.setValue("permissionSetId", currentValues.permissionSetId, {
+ shouldDirty: false,
+ });
+ }, 100);
+ }
+ };
+
+ return (
+
+ {templateLoading && }
+ {(!templateLoading || !isEditing) && (
+ <>
+
+ {isCopy
+ ? "Create a copy of an existing app deployment template with your own modifications."
+ : isEditing
+ ? "Edit this app deployment template."
+ : "Create a new app deployment template to define application deployment settings."}
+
+
+ App deployment templates allow you to define an application with its permissions that
+ can be deployed to multiple tenants. Select an application and permission set to create
+ a template.
+
+
+
+
+ `${item.displayName} (${item.appId})`,
+ valueField: "appId",
+ addedField: {
+ displayName: "displayName",
+ },
+ showRefresh: true,
+ }}
+ multiple={false}
+ validators={{ required: "Application is required" }}
+ />
+
+ item.TemplateName,
+ valueField: "TemplateId",
+ addedField: {
+ Permissions: "Permissions",
+ },
+ showRefresh: true,
+ }}
+ multiple={false}
+ validators={{ required: "Permission Set is required" }}
+ />
+
+
+
+
+ {isEditing ? "Update Template" : "Create Template"}
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default AppDeploymentTemplateForm;
diff --git a/src/pages/tenant/administration/applications/tabOptions.json b/src/pages/tenant/administration/applications/tabOptions.json
index bedc483e2e5f..dc943c0fdbe0 100644
--- a/src/pages/tenant/administration/applications/tabOptions.json
+++ b/src/pages/tenant/administration/applications/tabOptions.json
@@ -10,5 +10,9 @@
{
"label": "Permission Sets",
"path": "/tenant/administration/applications/permission-sets"
+ },
+ {
+ "label": "Deployment Templates",
+ "path": "/tenant/administration/applications/templates"
}
]
\ No newline at end of file
diff --git a/src/pages/tenant/administration/applications/templates/add.js b/src/pages/tenant/administration/applications/templates/add.js
new file mode 100644
index 000000000000..eecb3d638ecf
--- /dev/null
+++ b/src/pages/tenant/administration/applications/templates/add.js
@@ -0,0 +1,96 @@
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { useForm } from "react-hook-form";
+import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { CardContent } from "@mui/material";
+import { useEffect, useState } from "react";
+import AppDeploymentTemplateForm from "/src/components/CippComponents/AppDeploymentTemplateForm";
+
+const Page = () => {
+ const router = useRouter();
+ const { template, copy, name } = router.query;
+ const pageTitle = copy ? "Copy App Deployment Template" : "Add App Deployment Template";
+
+ // Add refetch key for refreshing data after save
+ const [refetchKey, setRefetchKey] = useState(0);
+
+ const formControl = useForm({
+ mode: "onBlur",
+ });
+
+ // Get the specified template if template ID is provided
+ const { data: templateData, isLoading: templateLoading } = ApiGetCall({
+ url: template ? `/api/ExecAppDeploymentTemplate?Action=Get&TemplateId=${template}` : null,
+ queryKey: template ? ["ExecAppDeploymentTemplate", template, refetchKey] : null,
+ enabled: !!template,
+ });
+
+ const updatePermissions = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["ListAppDeploymentTemplates", "ExecAppDeploymentTemplate"],
+ });
+
+ const handleSubmit = (payload) => {
+ updatePermissions.mutate(
+ {
+ url: "/api/ExecAppDeploymentTemplate?Action=Save",
+ data: payload,
+ queryKey: "ExecAppDeploymentTemplate",
+ },
+ {
+ onSuccess: (data) => {
+ // If we're adding or copying, redirect to edit page with the new ID
+ if (!template || copy) {
+ // Check if data exists and has the expected structure
+ const newTemplateId = data?.[0]?.TemplateId;
+
+ if (newTemplateId) {
+ router.push(
+ {
+ pathname: "/tenant/administration/applications/templates/edit",
+ query: {
+ template: newTemplateId,
+ name: payload.TemplateName,
+ },
+ },
+ undefined,
+ { shallow: true }
+ );
+ } else {
+ // Handle the case where TemplateId is missing
+ console.error("Missing TemplateId in response:", data);
+ // Just refresh the data as fallback
+ setRefetchKey((prev) => prev + 1);
+ }
+ } else {
+ // Just refresh the data if we're editing
+ setRefetchKey((prev) => prev + 1);
+ }
+ },
+ }
+ );
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+// Changed from TabbedLayout to just DashboardLayout
+Page.getLayout = (page) => {page} ;
+
+export default Page;
diff --git a/src/pages/tenant/administration/applications/templates/edit.js b/src/pages/tenant/administration/applications/templates/edit.js
new file mode 100644
index 000000000000..d6f3b974b0fb
--- /dev/null
+++ b/src/pages/tenant/administration/applications/templates/edit.js
@@ -0,0 +1,104 @@
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { useForm } from "react-hook-form";
+import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
+import CippPageCard from "/src/components/CippCards/CippPageCard";
+import { Alert, Button, CardContent } from "@mui/material";
+import { useState } from "react";
+import Link from "next/link";
+import AppDeploymentTemplateForm from "/src/components/CippComponents/AppDeploymentTemplateForm";
+
+const Page = () => {
+ const router = useRouter();
+ const { template, name } = router.query;
+
+ // Add a key to force refetch when we save
+ const [refetchKey, setRefetchKey] = useState(0);
+
+ const formControl = useForm({
+ mode: "onBlur",
+ });
+
+ const { data: templateData, isLoading } = ApiGetCall({
+ url: template ? `/api/ExecAppDeploymentTemplate?Action=Get&TemplateId=${template}` : null,
+ queryKey: template ? ["ExecAppDeploymentTemplate", template, refetchKey] : null,
+ enabled: !!template,
+ });
+
+ const updatePermissions = ApiPostCall({
+ urlFromData: true,
+ relatedQueryKeys: ["ListAppDeploymentTemplates", "ExecAppDeploymentTemplate"],
+ });
+
+ const handleSubmit = (payload) => {
+ // Ensure we're passing the TemplateId in the payload for updates
+ const updatedPayload = {
+ ...payload,
+ TemplateId: template,
+ Action: "Save",
+ };
+
+ updatePermissions.mutate(
+ {
+ url: "/api/ExecAppDeploymentTemplate?Action=Save",
+ data: updatedPayload,
+ queryKey: "ExecAppDeploymentTemplate",
+ },
+ {
+ onSuccess: (data) => {
+ // Check if we received a valid response
+ const newTemplateId = data?.[0]?.TemplateId || template;
+
+ // Refresh the data
+ setRefetchKey((prev) => prev + 1);
+ },
+ }
+ );
+ };
+
+ // Show error if template doesn't exist
+ if (!template) {
+ return (
+
+
+
+ The requested app deployment template does not exist or was not specified. Please select
+ a valid template from the list.
+
+
+ Back to Templates
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+// Changed from TabbedLayout to just DashboardLayout
+Page.getLayout = (page) => {page} ;
+
+export default Page;
diff --git a/src/pages/tenant/administration/applications/templates/index.js b/src/pages/tenant/administration/applications/templates/index.js
new file mode 100644
index 000000000000..a86639dd3c65
--- /dev/null
+++ b/src/pages/tenant/administration/applications/templates/index.js
@@ -0,0 +1,89 @@
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { TabbedLayout } from "/src/layouts/TabbedLayout";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import { Edit, Delete, ContentCopy, Add } from "@mui/icons-material";
+import tabOptions from "../tabOptions";
+import { Button } from "@mui/material";
+import Link from "next/link";
+
+const Page = () => {
+ const pageTitle = "Deployment Templates";
+ const apiUrl = "/api/ListAppDeploymentTemplates";
+
+ const actions = [
+ {
+ icon: ,
+ label: "Edit Template",
+ color: "warning",
+ link: "/tenant/administration/applications/templates/edit?template=[TemplateId]&name=[TemplateName]",
+ },
+ {
+ icon: ,
+ label: "Copy Template",
+ color: "info",
+ link: "/tenant/administration/applications/templates/add?template=[TemplateId]©=true&name=[TemplateName]",
+ },
+ {
+ icon: ,
+ label: "Delete Template",
+ color: "danger",
+ url: "/api/ExecAppDeploymentTemplate",
+ data: {
+ Action: "Delete",
+ TemplateId: "TemplateId",
+ },
+ type: "POST",
+ confirmText: "Are you sure you want to delete [TemplateName]?",
+ },
+ ];
+
+ const offCanvas = {
+ extendedInfoFields: [
+ "TemplateName",
+ "AppId",
+ "AppName",
+ "PermissionSetName",
+ "UpdatedBy",
+ "Timestamp",
+ ],
+ actions: actions,
+ };
+
+ const simpleColumns = [
+ "TemplateName",
+ "AppId",
+ "AppName",
+ "PermissionSetName",
+ "UpdatedBy",
+ "Timestamp",
+ ];
+
+ return (
+ }
+ >
+ Add App Deployment Template
+
+ }
+ />
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
From e296686ad2348dd2d39cadcf86b562a5848407af Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 21:51:46 -0400
Subject: [PATCH 026/865] consistent wording
---
.../CippComponents/AppDeploymentTemplateForm.jsx | 12 ++++++------
.../administration/applications/tabOptions.json | 2 +-
.../administration/applications/templates/index.js | 2 +-
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/components/CippComponents/AppDeploymentTemplateForm.jsx b/src/components/CippComponents/AppDeploymentTemplateForm.jsx
index 236c275ed08c..1a933644c9cd 100644
--- a/src/components/CippComponents/AppDeploymentTemplateForm.jsx
+++ b/src/components/CippComponents/AppDeploymentTemplateForm.jsx
@@ -124,15 +124,15 @@ const AppDeploymentTemplateForm = ({
<>
{isCopy
- ? "Create a copy of an existing app deployment template with your own modifications."
+ ? "Create a copy of an existing app approval template with your own modifications."
: isEditing
- ? "Edit this app deployment template."
- : "Create a new app deployment template to define application deployment settings."}
+ ? "Edit this app approval template."
+ : "Create a new app approval template to define application permissions to consent."}
- App deployment templates allow you to define an application with its permissions that
- can be deployed to multiple tenants. Select an application and permission set to create
- a template.
+ App approval templates allow you to define an application with its permissions that can
+ be deployed to multiple tenants. Select an application and permission set to create a
+ template.
{
- const pageTitle = "Deployment Templates";
+ const pageTitle = "Templates";
const apiUrl = "/api/ListAppDeploymentTemplates";
const actions = [
From b2ba51828bf66e4af1cf33552c42828d8f5a7d2c Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Mon, 19 May 2025 21:52:24 -0400
Subject: [PATCH 027/865] Update index.js
---
src/pages/tenant/administration/applications/templates/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/tenant/administration/applications/templates/index.js b/src/pages/tenant/administration/applications/templates/index.js
index 0b4000afa2f5..4aa8db2c940a 100644
--- a/src/pages/tenant/administration/applications/templates/index.js
+++ b/src/pages/tenant/administration/applications/templates/index.js
@@ -73,7 +73,7 @@ const Page = () => {
href="/tenant/administration/applications/templates/add"
startIcon={ }
>
- Add App Deployment Template
+ Add App Approval Template
}
/>
From 1a7c50bbe22c141e5fe25c26945c7cac4dc80eee Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 09:37:55 -0400
Subject: [PATCH 028/865] add permission preview functionality
---
.../AppApprovalTemplateForm.jsx | 655 ++++++++++++++++++
.../AppDeploymentTemplateForm.jsx | 204 ------
.../applications/templates/add.js | 16 +-
.../applications/templates/edit.js | 20 +-
.../applications/templates/index.js | 6 +-
5 files changed, 676 insertions(+), 225 deletions(-)
create mode 100644 src/components/CippComponents/AppApprovalTemplateForm.jsx
delete mode 100644 src/components/CippComponents/AppDeploymentTemplateForm.jsx
diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx
new file mode 100644
index 000000000000..a816639c7739
--- /dev/null
+++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx
@@ -0,0 +1,655 @@
+import React, { useState, useEffect } from "react";
+import {
+ Alert,
+ Skeleton,
+ Stack,
+ Typography,
+ Button,
+ Box,
+ Paper,
+ List,
+ ListItem,
+ ListItemText,
+ Divider,
+ Tab,
+ Tabs,
+ Chip,
+ SvgIcon,
+} from "@mui/material";
+import { ApiGetCall, ApiGetCallWithPagination } from "/src/api/ApiCall";
+import { CippFormComponent } from "./CippFormComponent";
+import { CippApiResults } from "./CippApiResults";
+import { Grid } from "@mui/system";
+import { ShieldCheckIcon } from "@heroicons/react/24/outline";
+import { CippCardTabPanel } from "./CippCardTabPanel";
+
+const AppApprovalTemplateForm = ({
+ formControl,
+ templateData,
+ templateLoading,
+ isEditing,
+ isCopy,
+ updatePermissions,
+ onSubmit,
+ refetchKey,
+}) => {
+ const [selectedPermissionSet, setSelectedPermissionSet] = useState(null);
+ const [selectedPermissionTab, setSelectedPermissionTab] = useState(0);
+ const [permissionsLoaded, setPermissionsLoaded] = useState(false);
+
+ // When templateData changes, update the form
+ useEffect(() => {
+ if (!isEditing && !isCopy) {
+ formControl.setValue("templateName", "New App Deployment Template");
+ formControl.setValue("appType", "EnterpriseApp");
+ setPermissionsLoaded(false);
+ } else if (templateData && isCopy) {
+ // When copying, we want to load the template data but not the ID
+ 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);
+ }
+ } 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);
+ }
+ }
+ }, [templateData, isCopy, isEditing, formControl]);
+
+ // Watch for app selection changes to update template name
+ const selectedApp = formControl.watch("appId");
+
+ useEffect(() => {
+ // Update template name when app is selected if we're in add mode and name hasn't been manually changed
+ if (selectedApp && !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];
+ if (appName) {
+ formControl.setValue("templateName", `${appName} Template`);
+ }
+ }
+ }
+ }, [selectedApp, isEditing, isCopy, formControl]);
+
+ // Watch for permission set selection changes
+ const selectedPermissionSetValue = formControl.watch("permissionSetId");
+
+ useEffect(() => {
+ if (selectedPermissionSetValue?.value) {
+ setSelectedPermissionSet(selectedPermissionSetValue);
+ setPermissionsLoaded(true);
+ } else {
+ setSelectedPermissionSet(null);
+ setPermissionsLoaded(false);
+ }
+ }, [selectedPermissionSetValue]);
+
+ // Fetch all service principals to get display names
+ const {
+ data: servicePrincipals,
+ isLoading: spLoading,
+ isSuccess: spSuccess,
+ } = ApiGetCallWithPagination({
+ url: "/api/ExecServicePrincipals",
+ queryKey: "execServicePrincipals",
+ enabled: permissionsLoaded && !!selectedPermissionSet?.addedFields?.Permissions,
+ });
+
+ // Fetch additional details about the application if needed
+ const {
+ data: appDetails,
+ isLoading: appDetailsLoading,
+ isSuccess: appDetailsSuccess,
+ } = ApiGetCall({
+ url:
+ permissionsLoaded && selectedPermissionSet?.addedFields?.Permissions && selectedApp?.value
+ ? `/api/ExecServicePrincipals?Id=${selectedApp.value}`
+ : null,
+ queryKey:
+ permissionsLoaded && selectedPermissionSet ? `app-details-${selectedApp?.value}` : null,
+ enabled:
+ permissionsLoaded &&
+ !!selectedPermissionSet?.addedFields?.Permissions &&
+ !!selectedApp?.value,
+ });
+
+ const handlePermissionTabChange = (event, newValue) => {
+ setSelectedPermissionTab(newValue);
+ };
+
+ // Handle form submission
+ const handleSubmit = (data) => {
+ // Remove console.log in production
+ const appDisplayName =
+ data.appId?.addedFields?.displayName ||
+ (data.appId?.label ? data.appId.label.split(" (")[0] : undefined);
+
+ const payload = {
+ TemplateName: data.templateName,
+ AppId: data.appId?.value,
+ AppName: appDisplayName,
+ PermissionSetId: data.permissionSetId?.value,
+ PermissionSetName: data.permissionSetId?.label,
+ Permissions: data.permissionSetId?.addedFields?.Permissions,
+ };
+
+ if (isEditing && !isCopy && templateData?.[0]?.TemplateId) {
+ payload.TemplateId = templateData[0].TemplateId;
+ }
+
+ // Store values before submission to set them back afterward
+ const currentValues = {
+ templateName: data.templateName,
+ appId: data.appId,
+ permissionSetId: data.permissionSetId,
+ };
+
+ onSubmit(payload);
+
+ // After submission, set the values back to what they were but mark as clean
+ // This will only apply to add page, as edit will get refreshed data
+ if (!isEditing) {
+ setTimeout(() => {
+ formControl.setValue("templateName", currentValues.templateName, { shouldDirty: false });
+ formControl.setValue("appId", currentValues.appId, { shouldDirty: false });
+ formControl.setValue("permissionSetId", currentValues.permissionSetId, {
+ shouldDirty: false,
+ });
+ }, 100);
+ }
+ };
+
+ // Function to get permission counts for the selected app
+ const getPermissionCounts = (permissions) => {
+ if (!permissions) return { app: 0, delegated: 0 };
+
+ let appCount = 0;
+ let delegatedCount = 0;
+
+ Object.entries(permissions).forEach(([resourceName, perms]) => {
+ if (perms.applicationPermissions) {
+ appCount += perms.applicationPermissions.length;
+ }
+ if (perms.delegatedPermissions) {
+ delegatedCount += perms.delegatedPermissions.length;
+ }
+ });
+
+ return { app: appCount, delegated: delegatedCount };
+ };
+
+ // Component to display permissions in a more detailed format
+ const PermissionDetails = ({ permissions }) => {
+ if (!permissions) return null;
+
+ // Helper to get the display name for a resource ID
+ const getResourceDisplayName = (resourceId) => {
+ if (!spSuccess || !servicePrincipals?.pages?.[0]?.Results) return resourceId;
+
+ const foundSp = servicePrincipals?.pages?.[0]?.Results.find((sp) => sp.appId === resourceId);
+ return foundSp ? foundSp.displayName : resourceId;
+ };
+
+ // Helper to get the appropriate permission description from service principals data
+ const getPermissionDescription = (resourceId, permissionId, permissionType) => {
+ if (!spSuccess || !servicePrincipals?.pages?.[0]?.Results) return null;
+
+ const foundSp = servicePrincipals?.pages?.[0]?.Results.find((sp) => sp.appId === resourceId);
+ if (!foundSp) return null;
+
+ if (permissionType === "application") {
+ // For application permissions, use description
+ const foundRole = foundSp.appRoles?.find((role) => role.id === permissionId);
+ return foundRole?.description || null;
+ } else {
+ // For delegated permissions, use userConsentDescription
+ const foundScope = foundSp.publishedPermissionScopes?.find(
+ (scope) => scope.id === permissionId
+ );
+ return foundScope?.userConsentDescription || foundScope?.description || null;
+ }
+ };
+
+ return (
+
+ {Object.entries(permissions).map(([resourceId, resourcePerms]) => {
+ const hasAppPermissions =
+ resourcePerms.applicationPermissions && resourcePerms.applicationPermissions.length > 0;
+ const hasDelegatedPermissions =
+ resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0;
+
+ return (
+
+
+
+ {getResourceDisplayName(resourceId)}
+
+ {resourceId}
+
+
+
+ {hasAppPermissions && (
+
+
+ Application Permissions ({resourcePerms.applicationPermissions.length})
+
+
+ {resourcePerms.applicationPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "application") ||
+ perm.description ||
+ "No description available";
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {hasDelegatedPermissions && (
+
+
+ Delegated Permissions ({resourcePerms.delegatedPermissions.length})
+
+
+ {resourcePerms.delegatedPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "delegated") ||
+ perm.description ||
+ "No description available";
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
+
+ {templateLoading && }
+ {(!templateLoading || !isEditing) && (
+ <>
+
+ {isCopy
+ ? "Create a copy of an existing app approval template with your own modifications."
+ : isEditing
+ ? "Edit this app approval template."
+ : "Create a new app approval template to define application permissions to consent."}
+
+
+ App approval templates allow you to define an application with its permissions that
+ can be deployed to multiple tenants. Select an application and permission set to
+ create a template.
+
+
+
+
+ `${item.displayName} (${item.appId})`,
+ valueField: "appId",
+ addedField: {
+ displayName: "displayName",
+ },
+ showRefresh: true,
+ }}
+ multiple={false}
+ validators={{ required: "Application is required" }}
+ />
+
+ item.TemplateName,
+ valueField: "TemplateId",
+ addedField: {
+ Permissions: "Permissions",
+ },
+ showRefresh: true,
+ }}
+ multiple={false}
+ validators={{ required: "Permission Set is required" }}
+ />
+
+
+
+
+ {isEditing ? "Update Template" : "Create Template"}
+
+
+
+
+ >
+ )}
+
+
+
+
+
+ Permission Preview
+ {selectedPermissionSet && (
+
+
+
+ }
+ title="Application/Delegated Permissions"
+ />
+ )}
+
+
+ {templateLoading ? (
+
+ ) : !selectedPermissionSet || !permissionsLoaded ? (
+
+ {isEditing && templateLoading
+ ? "Loading permission details..."
+ : "Select a permission set to see what permissions will be consented."}
+
+ ) : (
+
+ {!selectedPermissionSet.addedFields?.Permissions ? (
+ No permissions data available
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Object.entries(selectedPermissionSet.addedFields.Permissions || {})
+ .filter(
+ ([_, perms]) =>
+ perms.applicationPermissions && perms.applicationPermissions.length > 0
+ )
+ .map(([resourceId, resourcePerms]) => {
+ const resourceName =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )?.displayName || resourceId
+ : resourceId;
+
+ return (
+
+
+ {resourceName}
+
+ {resourceId}
+
+
+
+
+ {resourcePerms.applicationPermissions.map((perm, idx) => {
+ // Get proper application permission description
+ const description =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )?.appRoles?.find((role) => role.id === perm.id)
+ ?.description ||
+ perm.description ||
+ "No description available"
+ : perm.description || "No description available";
+
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+
+
+
+
+ {Object.entries(selectedPermissionSet.addedFields.Permissions || {})
+ .filter(
+ ([_, perms]) =>
+ perms.delegatedPermissions && perms.delegatedPermissions.length > 0
+ )
+ .map(([resourceId, resourcePerms]) => {
+ const resourceName =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )?.displayName || resourceId
+ : resourceId;
+
+ return (
+
+
+ {resourceName}
+
+ {resourceId}
+
+
+
+
+ {resourcePerms.delegatedPermissions.map((perm, idx) => {
+ // Get proper delegated permission description - prefer userConsentDescription
+ const spData =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )
+ : null;
+
+ const permScope = spData
+ ? spData.publishedPermissionScopes?.find(
+ (scope) => scope.id === perm.id
+ )
+ : null;
+
+ const description = permScope
+ ? permScope.userConsentDescription || permScope.description
+ : perm.description || "No description available";
+
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+
+ >
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default AppApprovalTemplateForm;
diff --git a/src/components/CippComponents/AppDeploymentTemplateForm.jsx b/src/components/CippComponents/AppDeploymentTemplateForm.jsx
deleted file mode 100644
index 1a933644c9cd..000000000000
--- a/src/components/CippComponents/AppDeploymentTemplateForm.jsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { Alert, Skeleton, Stack, Typography, Button, Box } from "@mui/material";
-import { ApiGetCall } from "/src/api/ApiCall";
-import { CippFormComponent } from "./CippFormComponent";
-import { CippApiResults } from "./CippApiResults";
-
-const AppDeploymentTemplateForm = ({
- formControl,
- templateData,
- templateLoading,
- isEditing,
- isCopy,
- updatePermissions,
- onSubmit,
- refetchKey,
-}) => {
- const [selectedPermissionSet, setSelectedPermissionSet] = useState(null);
-
- // When templateData changes, update the form
- useEffect(() => {
- if (!isEditing && !isCopy) {
- formControl.setValue("templateName", "New App Deployment Template");
- formControl.setValue("appType", "EnterpriseApp");
- } else if (templateData && isCopy) {
- // When copying, we want to load the template data but not the ID
- 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,
- },
- });
- formControl.setValue("permissionSetId", {
- label: templateData[0].PermissionSetName || "Custom Permissions",
- value: templateData[0].PermissionSetId,
- });
- }
- } 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,
- },
- });
- formControl.setValue("permissionSetId", {
- label: templateData[0].PermissionSetName || "Custom Permissions",
- value: templateData[0].PermissionSetId,
- });
- }
- }
- }, [templateData, isCopy, isEditing, formControl]);
-
- // Watch for app selection changes to update template name
- const selectedApp = formControl.watch("appId");
-
- useEffect(() => {
- // Update template name when app is selected if we're in add mode and name hasn't been manually changed
- if (selectedApp && !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];
- if (appName) {
- formControl.setValue("templateName", `${appName} Template`);
- }
- }
- }
- }, [selectedApp, isEditing, isCopy, formControl]);
-
- // Handle form submission
- const handleSubmit = (data) => {
- // Remove console.log in production
- const appDisplayName =
- data.appId?.addedFields?.displayName ||
- (data.appId?.label ? data.appId.label.split(" (")[0] : undefined);
-
- const payload = {
- TemplateName: data.templateName,
- AppId: data.appId?.value,
- AppName: appDisplayName,
- PermissionSetId: data.permissionSetId?.value,
- PermissionSetName: data.permissionSetId?.label,
- Permissions: data.permissionSetId?.addedFields?.Permissions,
- };
-
- if (isEditing && !isCopy && templateData?.[0]?.TemplateId) {
- payload.TemplateId = templateData[0].TemplateId;
- }
-
- // Store values before submission to set them back afterward
- const currentValues = {
- templateName: data.templateName,
- appId: data.appId,
- permissionSetId: data.permissionSetId,
- };
-
- onSubmit(payload);
-
- // After submission, set the values back to what they were but mark as clean
- // This will only apply to add page, as edit will get refreshed data
- if (!isEditing) {
- setTimeout(() => {
- formControl.setValue("templateName", currentValues.templateName, { shouldDirty: false });
- formControl.setValue("appId", currentValues.appId, { shouldDirty: false });
- formControl.setValue("permissionSetId", currentValues.permissionSetId, {
- shouldDirty: false,
- });
- }, 100);
- }
- };
-
- return (
-
- {templateLoading && }
- {(!templateLoading || !isEditing) && (
- <>
-
- {isCopy
- ? "Create a copy of an existing app approval template with your own modifications."
- : isEditing
- ? "Edit this app approval template."
- : "Create a new app approval template to define application permissions to consent."}
-
-
- App approval templates allow you to define an application with its permissions that can
- be deployed to multiple tenants. Select an application and permission set to create a
- template.
-
-
-
-
- `${item.displayName} (${item.appId})`,
- valueField: "appId",
- addedField: {
- displayName: "displayName",
- },
- showRefresh: true,
- }}
- multiple={false}
- validators={{ required: "Application is required" }}
- />
-
- item.TemplateName,
- valueField: "TemplateId",
- addedField: {
- Permissions: "Permissions",
- },
- showRefresh: true,
- }}
- multiple={false}
- validators={{ required: "Permission Set is required" }}
- />
-
-
-
-
- {isEditing ? "Update Template" : "Create Template"}
-
-
-
-
- >
- )}
-
- );
-};
-
-export default AppDeploymentTemplateForm;
diff --git a/src/pages/tenant/administration/applications/templates/add.js b/src/pages/tenant/administration/applications/templates/add.js
index eecb3d638ecf..792e30b8cc24 100644
--- a/src/pages/tenant/administration/applications/templates/add.js
+++ b/src/pages/tenant/administration/applications/templates/add.js
@@ -5,12 +5,12 @@ import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
import CippPageCard from "/src/components/CippCards/CippPageCard";
import { CardContent } from "@mui/material";
import { useEffect, useState } from "react";
-import AppDeploymentTemplateForm from "/src/components/CippComponents/AppDeploymentTemplateForm";
+import AppApprovalTemplateForm from "/src/components/CippComponents/AppApprovalTemplateForm";
const Page = () => {
const router = useRouter();
const { template, copy, name } = router.query;
- const pageTitle = copy ? "Copy App Deployment Template" : "Add App Deployment Template";
+ const pageTitle = copy ? "Copy App Approval Template" : "Add App Approval Template";
// Add refetch key for refreshing data after save
const [refetchKey, setRefetchKey] = useState(0);
@@ -21,22 +21,22 @@ const Page = () => {
// Get the specified template if template ID is provided
const { data: templateData, isLoading: templateLoading } = ApiGetCall({
- url: template ? `/api/ExecAppDeploymentTemplate?Action=Get&TemplateId=${template}` : null,
- queryKey: template ? ["ExecAppDeploymentTemplate", template, refetchKey] : null,
+ url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null,
+ queryKey: template ? ["ExecAppApprovalTemplate", template, refetchKey] : null,
enabled: !!template,
});
const updatePermissions = ApiPostCall({
urlFromData: true,
- relatedQueryKeys: ["ListAppDeploymentTemplates", "ExecAppDeploymentTemplate"],
+ relatedQueryKeys: ["ListAppApprovalTemplates", "ExecAppApprovalTemplate"],
});
const handleSubmit = (payload) => {
updatePermissions.mutate(
{
- url: "/api/ExecAppDeploymentTemplate?Action=Save",
+ url: "/api/ExecAppApprovalTemplate?Action=Save",
data: payload,
- queryKey: "ExecAppDeploymentTemplate",
+ queryKey: "ExecAppApprovalTemplate",
},
{
onSuccess: (data) => {
@@ -75,7 +75,7 @@ const Page = () => {
return (
- {
const router = useRouter();
@@ -20,14 +20,14 @@ const Page = () => {
});
const { data: templateData, isLoading } = ApiGetCall({
- url: template ? `/api/ExecAppDeploymentTemplate?Action=Get&TemplateId=${template}` : null,
- queryKey: template ? ["ExecAppDeploymentTemplate", template, refetchKey] : null,
+ url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null,
+ queryKey: template ? ["ExecAppApprovalTemplate", template, refetchKey] : null,
enabled: !!template,
});
const updatePermissions = ApiPostCall({
urlFromData: true,
- relatedQueryKeys: ["ListAppDeploymentTemplates", "ExecAppDeploymentTemplate"],
+ relatedQueryKeys: ["ListAppApprovalTemplates", "ExecAppApprovalTemplate"],
});
const handleSubmit = (payload) => {
@@ -40,9 +40,9 @@ const Page = () => {
updatePermissions.mutate(
{
- url: "/api/ExecAppDeploymentTemplate?Action=Save",
+ url: "/api/ExecAppApprovalTemplate?Action=Save",
data: updatedPayload,
- queryKey: "ExecAppDeploymentTemplate",
+ queryKey: "ExecAppApprovalTemplate",
},
{
onSuccess: (data) => {
@@ -62,8 +62,8 @@ const Page = () => {
- The requested app deployment template does not exist or was not specified. Please select
- a valid template from the list.
+ The requested app approval template does not exist or was not specified. Please select a
+ valid template from the list.
{
return (
- {
const pageTitle = "Templates";
- const apiUrl = "/api/ListAppDeploymentTemplates";
+ const apiUrl = "/api/ListAppApprovalTemplates";
const actions = [
{
@@ -27,7 +27,7 @@ const Page = () => {
icon: ,
label: "Delete Template",
color: "danger",
- url: "/api/ExecAppDeploymentTemplate",
+ url: "/api/ExecAppApprovalTemplate",
data: {
Action: "Delete",
TemplateId: "TemplateId",
@@ -62,7 +62,7 @@ const Page = () => {
Date: Tue, 20 May 2025 09:43:52 -0400
Subject: [PATCH 029/865] preview improvements
---
.../AppApprovalTemplateForm.jsx | 501 ++++++++++--------
1 file changed, 275 insertions(+), 226 deletions(-)
diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx
index a816639c7739..362d9a40ddf7 100644
--- a/src/components/CippComponents/AppApprovalTemplateForm.jsx
+++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx
@@ -128,17 +128,36 @@ const AppApprovalTemplateForm = ({
}
}, [selectedPermissionSetValue]);
+ // Handle initial data loading for editing and copying
+ useEffect(() => {
+ // When editing or copying, ensure permission data is properly loaded
+ if (isEditing || isCopy) {
+ if (templateData?.[0]?.Permissions) {
+ // Ensure permissions are immediately available for the preview
+ setPermissionsLoaded(true);
+ }
+ }
+ }, [isEditing, isCopy, templateData]);
+
// Fetch all service principals to get display names
const {
data: servicePrincipals,
isLoading: spLoading,
isSuccess: spSuccess,
+ refetch: refetchServicePrincipals,
} = ApiGetCallWithPagination({
url: "/api/ExecServicePrincipals",
queryKey: "execServicePrincipals",
- enabled: permissionsLoaded && !!selectedPermissionSet?.addedFields?.Permissions,
+ enabled: permissionsLoaded || (isEditing && !templateLoading),
});
+ // Refetch service principals when permissions are loaded
+ useEffect(() => {
+ if (permissionsLoaded && selectedPermissionSet?.addedFields?.Permissions) {
+ refetchServicePrincipals();
+ }
+ }, [permissionsLoaded, selectedPermissionSet, refetchServicePrincipals]);
+
// Fetch additional details about the application if needed
const {
data: appDetails,
@@ -256,117 +275,128 @@ const AppApprovalTemplateForm = ({
return (
- {Object.entries(permissions).map(([resourceId, resourcePerms]) => {
- const hasAppPermissions =
- resourcePerms.applicationPermissions && resourcePerms.applicationPermissions.length > 0;
- const hasDelegatedPermissions =
- resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0;
-
- return (
-
-
-
- {getResourceDisplayName(resourceId)}
-
- {resourceId}
-
-
-
- {hasAppPermissions && (
-
+ {permissions &&
+ Object.entries(permissions).map(([resourceId, resourcePerms]) => {
+ // Skip resources with no permissions or invalid data
+ if (
+ !resourcePerms ||
+ (!resourcePerms.applicationPermissions && !resourcePerms.delegatedPermissions)
+ ) {
+ return null;
+ }
+
+ const resourceName = getResourceDisplayName(resourceId);
+ const hasAppPermissions =
+ resourcePerms.applicationPermissions &&
+ resourcePerms.applicationPermissions.length > 0;
+ const hasDelegatedPermissions =
+ resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0;
+
+ return (
+
+
+
+ {resourceName}
- Application Permissions ({resourcePerms.applicationPermissions.length})
+ {resourceId}
-
- {resourcePerms.applicationPermissions.map((perm, idx) => {
- const description =
- getPermissionDescription(resourceId, perm.id, "application") ||
- perm.description ||
- "No description available";
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
- )}
+
- {hasDelegatedPermissions && (
-
-
- Delegated Permissions ({resourcePerms.delegatedPermissions.length})
-
-
- {resourcePerms.delegatedPermissions.map((perm, idx) => {
- const description =
- getPermissionDescription(resourceId, perm.id, "delegated") ||
- perm.description ||
- "No description available";
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
- )}
-
-
- );
- })}
+ {hasAppPermissions && (
+
+
+ Application Permissions ({resourcePerms.applicationPermissions.length})
+
+
+ {resourcePerms.applicationPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "application") ||
+ perm.description ||
+ "No description available";
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {hasDelegatedPermissions && (
+
+
+ Delegated Permissions ({resourcePerms.delegatedPermissions.length})
+
+
+ {resourcePerms.delegatedPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "delegated") ||
+ perm.description ||
+ "No description available";
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+ })}
);
};
@@ -459,7 +489,7 @@ const AppApprovalTemplateForm = ({
Permission Preview
- {selectedPermissionSet && (
+ {selectedPermissionSet?.addedFields?.Permissions && (
- ) : !selectedPermissionSet || !permissionsLoaded ? (
+ ) : !selectedPermissionSet?.addedFields?.Permissions || spLoading ? (
- {isEditing && templateLoading
+ {templateLoading
? "Loading permission details..."
+ : spLoading
+ ? "Loading permission information..."
: "Select a permission set to see what permissions will be consented."}
) : (
@@ -492,7 +524,8 @@ const AppApprovalTemplateForm = ({
variant="outlined"
sx={{ p: 2, height: "100%", overflow: "auto", maxHeight: 500 }}
>
- {!selectedPermissionSet.addedFields?.Permissions ? (
+ {!selectedPermissionSet.addedFields?.Permissions ||
+ Object.keys(selectedPermissionSet.addedFields.Permissions).length === 0 ? (
No permissions data available
) : (
<>
@@ -516,130 +549,146 @@ const AppApprovalTemplateForm = ({
- {Object.entries(selectedPermissionSet.addedFields.Permissions || {})
- .filter(
- ([_, perms]) =>
- perms.applicationPermissions && perms.applicationPermissions.length > 0
- )
- .map(([resourceId, resourcePerms]) => {
- const resourceName =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )?.displayName || resourceId
- : resourceId;
-
- return (
-
-
- {resourceName}
-
- {resourceId}
+ {selectedPermissionSet.addedFields.Permissions &&
+ Object.entries(selectedPermissionSet.addedFields.Permissions)
+ .filter(
+ ([_, perms]) =>
+ perms.applicationPermissions &&
+ perms.applicationPermissions.length > 0
+ )
+ .map(([resourceId, resourcePerms]) => {
+ const resourceName =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )?.displayName || resourceId
+ : resourceId;
+
+ return (
+
+
+ {resourceName}
+
+ {resourceId}
+
-
-
-
- {resourcePerms.applicationPermissions.map((perm, idx) => {
- // Get proper application permission description
- const description =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )?.appRoles?.find((role) => role.id === perm.id)
- ?.description ||
- perm.description ||
- "No description available"
- : perm.description || "No description available";
-
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
-
- );
- })}
+
+
+ {resourcePerms.applicationPermissions.map((perm, idx) => {
+ // Get proper application permission description
+ const description =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )?.appRoles?.find((role) => role.id === perm.id)
+ ?.description ||
+ perm.description ||
+ "No description available"
+ : perm.description || "No description available";
+
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+ {!Object.values(selectedPermissionSet.addedFields.Permissions || {}).some(
+ (perms) =>
+ perms.applicationPermissions && perms.applicationPermissions.length > 0
+ ) && (
+ No application permissions in this template.
+ )}
+ {/* Similar checks for delegated permissions */}
- {Object.entries(selectedPermissionSet.addedFields.Permissions || {})
- .filter(
- ([_, perms]) =>
- perms.delegatedPermissions && perms.delegatedPermissions.length > 0
- )
- .map(([resourceId, resourcePerms]) => {
- const resourceName =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )?.displayName || resourceId
- : resourceId;
-
- return (
-
-
- {resourceName}
-
- {resourceId}
+ {selectedPermissionSet.addedFields.Permissions &&
+ Object.entries(selectedPermissionSet.addedFields.Permissions)
+ .filter(
+ ([_, perms]) =>
+ perms.delegatedPermissions && perms.delegatedPermissions.length > 0
+ )
+ .map(([resourceId, resourcePerms]) => {
+ const resourceName =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )?.displayName || resourceId
+ : resourceId;
+
+ return (
+
+
+ {resourceName}
+
+ {resourceId}
+
-
-
-
- {resourcePerms.delegatedPermissions.map((perm, idx) => {
- // Get proper delegated permission description - prefer userConsentDescription
- const spData =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
+
+
+ {resourcePerms.delegatedPermissions.map((perm, idx) => {
+ // Get proper delegated permission description - prefer userConsentDescription
+ const spData =
+ spSuccess && servicePrincipals?.pages?.[0]?.Results
+ ? servicePrincipals.pages[0].Results.find(
+ (sp) => sp.appId === resourceId
+ )
+ : null;
+
+ const permScope = spData
+ ? spData.publishedPermissionScopes?.find(
+ (scope) => scope.id === perm.id
)
: null;
- const permScope = spData
- ? spData.publishedPermissionScopes?.find(
- (scope) => scope.id === perm.id
- )
- : null;
-
- const description = permScope
- ? permScope.userConsentDescription || permScope.description
- : perm.description || "No description available";
-
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
-
- );
- })}
+ const description = permScope
+ ? permScope.userConsentDescription || permScope.description
+ : perm.description || "No description available";
+
+ return (
+
+ {idx > 0 && }
+
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+ {!Object.values(selectedPermissionSet.addedFields.Permissions || {}).some(
+ (perms) =>
+ perms.delegatedPermissions && perms.delegatedPermissions.length > 0
+ ) && (
+ No delegated permissions in this template.
+ )}
>
From 20fa9bf6c287e750b61e724f08d7366ad8fd9409 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 09:48:32 -0400
Subject: [PATCH 030/865] add save to github action
---
.../applications/templates/index.js | 56 ++++++++++++++++++-
1 file changed, 55 insertions(+), 1 deletion(-)
diff --git a/src/pages/tenant/administration/applications/templates/index.js b/src/pages/tenant/administration/applications/templates/index.js
index 5f17da3cc38f..c8e7e567a617 100644
--- a/src/pages/tenant/administration/applications/templates/index.js
+++ b/src/pages/tenant/administration/applications/templates/index.js
@@ -1,15 +1,24 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Edit, Delete, ContentCopy, Add } from "@mui/icons-material";
+import { Edit, Delete, ContentCopy, Add, GitHub } from "@mui/icons-material";
import tabOptions from "../tabOptions";
import { Button } from "@mui/material";
import Link from "next/link";
+import { ApiGetCall } from "/src/api/ApiCall";
const Page = () => {
const pageTitle = "Templates";
const apiUrl = "/api/ListAppApprovalTemplates";
+ // Fetch GitHub integration status
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
const actions = [
{
icon: ,
@@ -23,6 +32,51 @@ const Page = () => {
color: "info",
link: "/tenant/administration/applications/templates/add?template=[TemplateId]©=true&name=[TemplateName]",
},
+ {
+ icon: ,
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ data: {
+ Action: "UploadTemplate",
+ GUID: "TemplateId",
+ TemplateType: "AppApproval",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: {
+ WriteAccess: true,
+ },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: {
+ required: { value: true, message: "This field is required" },
+ },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this file to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText: "Are you sure you want to save this template to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
{
icon: ,
label: "Delete Template",
From dfd2886bc3ee114d6a1ffe4bc8ed5bfbb4661cf4 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 13:58:52 -0400
Subject: [PATCH 031/865] prettification
---
.../CippAppPermissionBuilder.jsx | 5 +-
.../applications/permission-sets/add.js | 47 ++++++++++---------
.../applications/permission-sets/edit.js | 14 +++++-
.../applications/templates/add.js | 2 +-
4 files changed, 42 insertions(+), 26 deletions(-)
diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx
index 92736b31da2d..f8c9c348838e 100644
--- a/src/components/CippComponents/CippAppPermissionBuilder.jsx
+++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx
@@ -545,7 +545,7 @@ const CippAppPermissionBuilder = ({
return (
<>
{spInfoFetching && }
- {servicePrincipal && spInfoSuccess && (
+ {servicePrincipal && spInfoSuccess && !spInfoFetching && (
<>
Manage the permissions for the {servicePrincipal.displayName}.
@@ -715,7 +715,8 @@ const CippAppPermissionBuilder = ({
{
});
// Get the specified template if template ID is provided
- const { data: templateData, isLoading: templateLoading } = ApiGetCall({
+ const { data: templateData, isFetching: templateFetching } = ApiGetCall({
url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null,
queryKey: template ? ["execAppPermissionTemplate", template, refetchKey] : null,
enabled: !!template,
@@ -106,8 +104,10 @@ const Page = () => {
onSuccess: (data) => {
// Instead of navigating away, stay on the page and refresh
if (copy || !template) {
+ console.log("Copying or creating new template, redirecting to edit page");
+ console.log("New template data:", data);
// If we're copying or creating new, update the URL to edit mode with the new template ID
- const newTemplateId = data[0].TemplateId;
+ const newTemplateId = data.data[0].Metadata.TemplateId;
router.push(
{
pathname: "/tenant/administration/applications/permission-sets/edit",
@@ -146,8 +146,7 @@ const Page = () => {
- {templateLoading && }
- {(!templateLoading || !template) && (
+ {(!templateFetching || !template) && (
<>
{copy
@@ -166,6 +165,7 @@ const Page = () => {
name="templateName"
label="Permission Set Name"
type="textField"
+ required={true}
validators={{ required: "Permission set name is required" }}
/>
@@ -175,7 +175,8 @@ const Page = () => {
({
label: template.TemplateName,
@@ -197,15 +198,23 @@ const Page = () => {
)}
{initialPermissions && (
-
+ <>
+
+ Choose the permissions you want to assign to this permission set. Microsoft
+ Graph is the default Service Principal added and you can choose to add
+ additional Service Principals as needed. Note that some Service Principals do
+ not have any published permissions to choose from.
+
+
+ >
)}
>
)}
@@ -215,10 +224,6 @@ const Page = () => {
);
};
-Page.getLayout = (page) => (
-
- {page}
-
-);
+Page.getLayout = (page) => {page} ;
export default Page;
diff --git a/src/pages/tenant/administration/applications/permission-sets/edit.js b/src/pages/tenant/administration/applications/permission-sets/edit.js
index 7fdfc15f2696..17028c35cd6d 100644
--- a/src/pages/tenant/administration/applications/permission-sets/edit.js
+++ b/src/pages/tenant/administration/applications/permission-sets/edit.js
@@ -109,8 +109,10 @@ const Page = () => {
>
- {isLoading && }
- {!isLoading && initialPermissions && (
+ {isFetching && (
+
+ )}
+ {!isFetching && initialPermissions && (
<>
Modify the permissions in this permission set. Any changes will affect all
@@ -126,9 +128,17 @@ const Page = () => {
name="templateName"
label="Permission Set Name"
type="textField"
+ required={true}
validators={{ required: "Permission set name is required" }}
/>
+
+ Choose the permissions you want to assign to this permission set. Microsoft Graph is
+ the default Service Principal added and you can choose to add additional Service
+ Principals as needed. Note that some Service Principals do not have any published
+ permissions to choose from.
+
+
{
const { data: templateData, isLoading: templateLoading } = ApiGetCall({
url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null,
queryKey: template ? ["ExecAppApprovalTemplate", template, refetchKey] : null,
- enabled: !!template,
+ waiting: !!template,
});
const updatePermissions = ApiPostCall({
From c21f956dc89beb6b0c75b96e0e53bcd5c6f0f6b9 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 14:48:23 -0400
Subject: [PATCH 032/865] fix query keys
---
.../AppApprovalTemplateForm.jsx | 31 ++++---------------
.../CippAppPermissionBuilder.jsx | 2 +-
.../applications/permission-sets/add.js | 4 +--
.../applications/permission-sets/edit.js | 2 +-
.../applications/templates/edit.js | 2 +-
5 files changed, 11 insertions(+), 30 deletions(-)
diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx
index 362d9a40ddf7..1b85d6041a22 100644
--- a/src/components/CippComponents/AppApprovalTemplateForm.jsx
+++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx
@@ -148,7 +148,6 @@ const AppApprovalTemplateForm = ({
} = ApiGetCallWithPagination({
url: "/api/ExecServicePrincipals",
queryKey: "execServicePrincipals",
- enabled: permissionsLoaded || (isEditing && !templateLoading),
});
// Refetch service principals when permissions are loaded
@@ -158,24 +157,6 @@ const AppApprovalTemplateForm = ({
}
}, [permissionsLoaded, selectedPermissionSet, refetchServicePrincipals]);
- // Fetch additional details about the application if needed
- const {
- data: appDetails,
- isLoading: appDetailsLoading,
- isSuccess: appDetailsSuccess,
- } = ApiGetCall({
- url:
- permissionsLoaded && selectedPermissionSet?.addedFields?.Permissions && selectedApp?.value
- ? `/api/ExecServicePrincipals?Id=${selectedApp.value}`
- : null,
- queryKey:
- permissionsLoaded && selectedPermissionSet ? `app-details-${selectedApp?.value}` : null,
- enabled:
- permissionsLoaded &&
- !!selectedPermissionSet?.addedFields?.Permissions &&
- !!selectedApp?.value,
- });
-
const handlePermissionTabChange = (event, newValue) => {
setSelectedPermissionTab(newValue);
};
@@ -231,10 +212,10 @@ const AppApprovalTemplateForm = ({
Object.entries(permissions).forEach(([resourceName, perms]) => {
if (perms.applicationPermissions) {
- appCount += perms.applicationPermissions.length;
+ appCount += perms?.applicationPermissions?.length ?? 0;
}
if (perms.delegatedPermissions) {
- delegatedCount += perms.delegatedPermissions.length;
+ delegatedCount += perms?.delegatedPermissions?.length ?? 0;
}
});
@@ -288,9 +269,9 @@ const AppApprovalTemplateForm = ({
const resourceName = getResourceDisplayName(resourceId);
const hasAppPermissions =
resourcePerms.applicationPermissions &&
- resourcePerms.applicationPermissions.length > 0;
+ resourcePerms?.applicationPermissions?.length > 0;
const hasDelegatedPermissions =
- resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0;
+ resourcePerms.delegatedPermissions && resourcePerms?.delegatedPermissions?.length > 0;
return (
@@ -332,7 +313,7 @@ const AppApprovalTemplateForm = ({
pb: 0.5,
}}
>
- Application Permissions ({resourcePerms.applicationPermissions.length})
+ Application Permissions ({resourcePerms?.applicationPermissions?.length})
{resourcePerms.applicationPermissions.map((perm, idx) => {
@@ -370,7 +351,7 @@ const AppApprovalTemplateForm = ({
pb: 0.5,
}}
>
- Delegated Permissions ({resourcePerms.delegatedPermissions.length})
+ Delegated Permissions ({resourcePerms?.delegatedPermissions?.length})
{resourcePerms.delegatedPermissions.map((perm, idx) => {
diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx
index f8c9c348838e..a092b15b274d 100644
--- a/src/components/CippComponents/CippAppPermissionBuilder.jsx
+++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx
@@ -79,7 +79,7 @@ const CippAppPermissionBuilder = ({
refetch: refetchServicePrincipals,
} = ApiGetCall({
url: "/api/ExecServicePrincipals",
- queryKey: "execServicePrincipals",
+ queryKey: "execServicePrincipalList",
waiting: true,
});
diff --git a/src/pages/tenant/administration/applications/permission-sets/add.js b/src/pages/tenant/administration/applications/permission-sets/add.js
index acdb417c7d5f..6e9ccb56f52e 100644
--- a/src/pages/tenant/administration/applications/permission-sets/add.js
+++ b/src/pages/tenant/administration/applications/permission-sets/add.js
@@ -26,14 +26,14 @@ const Page = () => {
// Get the specified template if template ID is provided
const { data: templateData, isFetching: templateFetching } = ApiGetCall({
url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null,
- queryKey: template ? ["execAppPermissionTemplate", template, refetchKey] : null,
+ queryKey: template ? ["execAppPermissionTemplateDetails", template, refetchKey] : null,
enabled: !!template,
});
// Get all available templates for importing
const { data: allTemplates, isLoading: templatesLoading } = ApiGetCall({
url: "/api/ExecAppPermissionTemplate",
- queryKey: "execAppPermissionTemplate",
+ queryKey: "execAppPermissionTemplateList",
});
useEffect(() => {
diff --git a/src/pages/tenant/administration/applications/permission-sets/edit.js b/src/pages/tenant/administration/applications/permission-sets/edit.js
index 17028c35cd6d..e14272fb7b76 100644
--- a/src/pages/tenant/administration/applications/permission-sets/edit.js
+++ b/src/pages/tenant/administration/applications/permission-sets/edit.js
@@ -25,7 +25,7 @@ const Page = () => {
const {
data: templateData,
- isLoading,
+ isFetching,
refetch,
} = ApiGetCall({
url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null,
diff --git a/src/pages/tenant/administration/applications/templates/edit.js b/src/pages/tenant/administration/applications/templates/edit.js
index e328cb44c409..807717c763cf 100644
--- a/src/pages/tenant/administration/applications/templates/edit.js
+++ b/src/pages/tenant/administration/applications/templates/edit.js
@@ -21,7 +21,7 @@ const Page = () => {
const { data: templateData, isLoading } = ApiGetCall({
url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null,
- queryKey: template ? ["ExecAppApprovalTemplate", template, refetchKey] : null,
+ queryKey: template ? ["ExecAppApprovalTemplateList", template, refetchKey] : null,
enabled: !!template,
});
From 99eceafe368f23e4e84e462bc8fc91641892d555 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 15:06:08 -0400
Subject: [PATCH 033/865] form tweaks
---
.../CippComponents/AppApprovalTemplateForm.jsx | 13 ++-----------
1 file changed, 2 insertions(+), 11 deletions(-)
diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx
index 1b85d6041a22..ca231fccae2d 100644
--- a/src/components/CippComponents/AppApprovalTemplateForm.jsx
+++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx
@@ -386,16 +386,10 @@ const AppApprovalTemplateForm = ({
+ App Approval Template Details
{templateLoading && }
{(!templateLoading || !isEditing) && (
<>
-
- {isCopy
- ? "Create a copy of an existing app approval template with your own modifications."
- : isEditing
- ? "Edit this app approval template."
- : "Create a new app approval template to define application permissions to consent."}
-
App approval templates allow you to define an application with its permissions that
can be deployed to multiple tenants. Select an application and permission set to
@@ -501,10 +495,7 @@ const AppApprovalTemplateForm = ({
: "Select a permission set to see what permissions will be consented."}
) : (
-
+
{!selectedPermissionSet.addedFields?.Permissions ||
Object.keys(selectedPermissionSet.addedFields.Permissions).length === 0 ? (
No permissions data available
From 1c11736039e352e1d2bb4533c9e7b0aaaa52701f Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 17:35:28 -0400
Subject: [PATCH 034/865] app approval template deployment
---
.../AppApprovalTemplateForm.jsx | 438 +-----------------
.../CippAppPermissionBuilder.jsx | 2 +-
.../CippComponents/CippFormComponent.jsx | 1 +
.../CippComponents/CippFormCondition.jsx | 5 +-
.../CippComponents/CippPermissionPreview.jsx | 381 +++++++++++++++
.../CippWizard/CippWizardAppApproval.jsx | 161 +++++--
.../applications/permission-sets/add.js | 2 -
7 files changed, 530 insertions(+), 460 deletions(-)
create mode 100644 src/components/CippComponents/CippPermissionPreview.jsx
diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx
index ca231fccae2d..4efe18a1ecbb 100644
--- a/src/components/CippComponents/AppApprovalTemplateForm.jsx
+++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx
@@ -1,27 +1,10 @@
import React, { useState, useEffect } from "react";
-import {
- Alert,
- Skeleton,
- Stack,
- Typography,
- Button,
- Box,
- Paper,
- List,
- ListItem,
- ListItemText,
- Divider,
- Tab,
- Tabs,
- Chip,
- SvgIcon,
-} from "@mui/material";
-import { ApiGetCall, ApiGetCallWithPagination } from "/src/api/ApiCall";
+import { Alert, Skeleton, Stack, Typography, Button, Box } from "@mui/material";
+import { ApiGetCall } from "/src/api/ApiCall";
import { CippFormComponent } from "./CippFormComponent";
import { CippApiResults } from "./CippApiResults";
import { Grid } from "@mui/system";
-import { ShieldCheckIcon } from "@heroicons/react/24/outline";
-import { CippCardTabPanel } from "./CippCardTabPanel";
+import CippPermissionPreview from "./CippPermissionPreview";
const AppApprovalTemplateForm = ({
formControl,
@@ -34,7 +17,6 @@ const AppApprovalTemplateForm = ({
refetchKey,
}) => {
const [selectedPermissionSet, setSelectedPermissionSet] = useState(null);
- const [selectedPermissionTab, setSelectedPermissionTab] = useState(0);
const [permissionsLoaded, setPermissionsLoaded] = useState(false);
// When templateData changes, update the form
@@ -139,31 +121,8 @@ const AppApprovalTemplateForm = ({
}
}, [isEditing, isCopy, templateData]);
- // Fetch all service principals to get display names
- const {
- data: servicePrincipals,
- isLoading: spLoading,
- isSuccess: spSuccess,
- refetch: refetchServicePrincipals,
- } = ApiGetCallWithPagination({
- url: "/api/ExecServicePrincipals",
- queryKey: "execServicePrincipals",
- });
-
- // Refetch service principals when permissions are loaded
- useEffect(() => {
- if (permissionsLoaded && selectedPermissionSet?.addedFields?.Permissions) {
- refetchServicePrincipals();
- }
- }, [permissionsLoaded, selectedPermissionSet, refetchServicePrincipals]);
-
- const handlePermissionTabChange = (event, newValue) => {
- setSelectedPermissionTab(newValue);
- };
-
// Handle form submission
const handleSubmit = (data) => {
- // Remove console.log in production
const appDisplayName =
data.appId?.addedFields?.displayName ||
(data.appId?.label ? data.appId.label.split(" (")[0] : undefined);
@@ -203,185 +162,6 @@ const AppApprovalTemplateForm = ({
}
};
- // Function to get permission counts for the selected app
- const getPermissionCounts = (permissions) => {
- if (!permissions) return { app: 0, delegated: 0 };
-
- let appCount = 0;
- let delegatedCount = 0;
-
- Object.entries(permissions).forEach(([resourceName, perms]) => {
- if (perms.applicationPermissions) {
- appCount += perms?.applicationPermissions?.length ?? 0;
- }
- if (perms.delegatedPermissions) {
- delegatedCount += perms?.delegatedPermissions?.length ?? 0;
- }
- });
-
- return { app: appCount, delegated: delegatedCount };
- };
-
- // Component to display permissions in a more detailed format
- const PermissionDetails = ({ permissions }) => {
- if (!permissions) return null;
-
- // Helper to get the display name for a resource ID
- const getResourceDisplayName = (resourceId) => {
- if (!spSuccess || !servicePrincipals?.pages?.[0]?.Results) return resourceId;
-
- const foundSp = servicePrincipals?.pages?.[0]?.Results.find((sp) => sp.appId === resourceId);
- return foundSp ? foundSp.displayName : resourceId;
- };
-
- // Helper to get the appropriate permission description from service principals data
- const getPermissionDescription = (resourceId, permissionId, permissionType) => {
- if (!spSuccess || !servicePrincipals?.pages?.[0]?.Results) return null;
-
- const foundSp = servicePrincipals?.pages?.[0]?.Results.find((sp) => sp.appId === resourceId);
- if (!foundSp) return null;
-
- if (permissionType === "application") {
- // For application permissions, use description
- const foundRole = foundSp.appRoles?.find((role) => role.id === permissionId);
- return foundRole?.description || null;
- } else {
- // For delegated permissions, use userConsentDescription
- const foundScope = foundSp.publishedPermissionScopes?.find(
- (scope) => scope.id === permissionId
- );
- return foundScope?.userConsentDescription || foundScope?.description || null;
- }
- };
-
- return (
-
- {permissions &&
- Object.entries(permissions).map(([resourceId, resourcePerms]) => {
- // Skip resources with no permissions or invalid data
- if (
- !resourcePerms ||
- (!resourcePerms.applicationPermissions && !resourcePerms.delegatedPermissions)
- ) {
- return null;
- }
-
- const resourceName = getResourceDisplayName(resourceId);
- const hasAppPermissions =
- resourcePerms.applicationPermissions &&
- resourcePerms?.applicationPermissions?.length > 0;
- const hasDelegatedPermissions =
- resourcePerms.delegatedPermissions && resourcePerms?.delegatedPermissions?.length > 0;
-
- return (
-
-
-
- {resourceName}
-
- {resourceId}
-
-
-
- {hasAppPermissions && (
-
-
- Application Permissions ({resourcePerms?.applicationPermissions?.length})
-
-
- {resourcePerms.applicationPermissions.map((perm, idx) => {
- const description =
- getPermissionDescription(resourceId, perm.id, "application") ||
- perm.description ||
- "No description available";
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
- )}
-
- {hasDelegatedPermissions && (
-
-
- Delegated Permissions ({resourcePerms?.delegatedPermissions?.length})
-
-
- {resourcePerms.delegatedPermissions.map((perm, idx) => {
- const description =
- getPermissionDescription(resourceId, perm.id, "delegated") ||
- perm.description ||
- "No description available";
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
- )}
-
-
- );
- })}
-
- );
- };
-
return (
@@ -461,213 +241,11 @@ const AppApprovalTemplateForm = ({
-
-
- Permission Preview
- {selectedPermissionSet?.addedFields?.Permissions && (
-
-
-
- }
- title="Application/Delegated Permissions"
- />
- )}
-
-
- {templateLoading ? (
-
- ) : !selectedPermissionSet?.addedFields?.Permissions || spLoading ? (
-
- {templateLoading
- ? "Loading permission details..."
- : spLoading
- ? "Loading permission information..."
- : "Select a permission set to see what permissions will be consented."}
-
- ) : (
-
- {!selectedPermissionSet.addedFields?.Permissions ||
- Object.keys(selectedPermissionSet.addedFields.Permissions).length === 0 ? (
- No permissions data available
- ) : (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {selectedPermissionSet.addedFields.Permissions &&
- Object.entries(selectedPermissionSet.addedFields.Permissions)
- .filter(
- ([_, perms]) =>
- perms.applicationPermissions &&
- perms.applicationPermissions.length > 0
- )
- .map(([resourceId, resourcePerms]) => {
- const resourceName =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )?.displayName || resourceId
- : resourceId;
-
- return (
-
-
- {resourceName}
-
- {resourceId}
-
-
-
-
- {resourcePerms.applicationPermissions.map((perm, idx) => {
- // Get proper application permission description
- const description =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )?.appRoles?.find((role) => role.id === perm.id)
- ?.description ||
- perm.description ||
- "No description available"
- : perm.description || "No description available";
-
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
-
- );
- })}
- {!Object.values(selectedPermissionSet.addedFields.Permissions || {}).some(
- (perms) =>
- perms.applicationPermissions && perms.applicationPermissions.length > 0
- ) && (
- No application permissions in this template.
- )}
-
-
-
-
- {/* Similar checks for delegated permissions */}
-
- {selectedPermissionSet.addedFields.Permissions &&
- Object.entries(selectedPermissionSet.addedFields.Permissions)
- .filter(
- ([_, perms]) =>
- perms.delegatedPermissions && perms.delegatedPermissions.length > 0
- )
- .map(([resourceId, resourcePerms]) => {
- const resourceName =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )?.displayName || resourceId
- : resourceId;
-
- return (
-
-
- {resourceName}
-
- {resourceId}
-
-
-
-
- {resourcePerms.delegatedPermissions.map((perm, idx) => {
- // Get proper delegated permission description - prefer userConsentDescription
- const spData =
- spSuccess && servicePrincipals?.pages?.[0]?.Results
- ? servicePrincipals.pages[0].Results.find(
- (sp) => sp.appId === resourceId
- )
- : null;
-
- const permScope = spData
- ? spData.publishedPermissionScopes?.find(
- (scope) => scope.id === perm.id
- )
- : null;
-
- const description = permScope
- ? permScope.userConsentDescription || permScope.description
- : perm.description || "No description available";
-
- return (
-
- {idx > 0 && }
-
-
-
-
- );
- })}
-
-
-
- );
- })}
- {!Object.values(selectedPermissionSet.addedFields.Permissions || {}).some(
- (perms) =>
- perms.delegatedPermissions && perms.delegatedPermissions.length > 0
- ) && (
- No delegated permissions in this template.
- )}
-
-
- >
- )}
-
- )}
-
+
);
diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx
index a092b15b274d..b0a7a4ded9bc 100644
--- a/src/components/CippComponents/CippAppPermissionBuilder.jsx
+++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx
@@ -716,7 +716,7 @@ const CippAppPermissionBuilder = ({
type="autoComplete"
fullWidth
label="Add a Service Principal (optional)"
- placeholder="Select a Service Principal or enter an AppId if not listed (admin required to add new SP)"
+ placeholder="Select a Service Principal or enter an AppId if not listed"
name="servicePrincipal"
createOption={true}
onCreateOption={onCreateServicePrincipal}
diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx
index ba6b3ba9d458..30f97a302684 100644
--- a/src/components/CippComponents/CippFormComponent.jsx
+++ b/src/components/CippComponents/CippFormComponent.jsx
@@ -221,6 +221,7 @@ export const CippFormComponent = (props) => {
(
diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx
index dd2f87997c8c..61a4b2375eb8 100644
--- a/src/components/CippComponents/CippFormCondition.jsx
+++ b/src/components/CippComponents/CippFormCondition.jsx
@@ -171,7 +171,10 @@ export const CippFormCondition = (props) => {
return null;
case "hasValue":
- if (watcher !== undefined && watcher !== null && watcher !== "") {
+ if (
+ (watcher !== undefined && watcher !== null && watcher !== "") ||
+ (watcher?.value !== undefined && watcher?.value !== null && watcher?.value !== "")
+ ) {
return children;
}
if (action === "disable") {
diff --git a/src/components/CippComponents/CippPermissionPreview.jsx b/src/components/CippComponents/CippPermissionPreview.jsx
new file mode 100644
index 000000000000..9f2afeb69e06
--- /dev/null
+++ b/src/components/CippComponents/CippPermissionPreview.jsx
@@ -0,0 +1,381 @@
+import React, { useState, useEffect, useCallback } from "react";
+import {
+ Alert,
+ Skeleton,
+ Stack,
+ Typography,
+ Box,
+ Paper,
+ List,
+ ListItem,
+ ListItemText,
+ Divider,
+ Tab,
+ Tabs,
+ Chip,
+ SvgIcon,
+} from "@mui/material";
+import { ApiGetCall } from "/src/api/ApiCall";
+import { ShieldCheckIcon } from "@heroicons/react/24/outline";
+import { CippCardTabPanel } from "./CippCardTabPanel";
+
+const CippPermissionPreview = ({
+ permissions,
+ title = "Permission Preview",
+ isLoading = false,
+ maxHeight = "100%",
+ showAppIds = true,
+}) => {
+ const [selectedPermissionTab, setSelectedPermissionTab] = useState(0);
+ const [servicePrincipalDetails, setServicePrincipalDetails] = useState({});
+ const [resourceIds, setResourceIds] = useState([]);
+ const [loadingDetails, setLoadingDetails] = useState(false);
+
+ // Extract resource IDs from permissions object
+ useEffect(() => {
+ if (permissions && typeof permissions === "object") {
+ const ids = Object.keys(permissions);
+ setResourceIds(ids);
+ }
+ }, [permissions]);
+
+ // Function to fetch individual service principal details
+ const fetchServicePrincipalDetails = useCallback(async (resourceId) => {
+ try {
+ const response = await fetch(`/api/ExecServicePrincipals?AppId=${resourceId}`);
+ const data = await response.json();
+
+ if (data?.Results) {
+ setServicePrincipalDetails((prev) => ({
+ ...prev,
+ [resourceId]: data.Results,
+ }));
+ }
+ } catch (error) {
+ console.error(`Error fetching details for ${resourceId}:`, error);
+ }
+ }, []);
+
+ // Fetch details for each resource ID
+ useEffect(() => {
+ const fetchAllDetails = async () => {
+ if (resourceIds.length > 0) {
+ setLoadingDetails(true);
+ const promises = resourceIds.map((id) => fetchServicePrincipalDetails(id));
+ await Promise.all(promises);
+ setLoadingDetails(false);
+ }
+ };
+
+ fetchAllDetails();
+ }, [resourceIds, fetchServicePrincipalDetails]);
+
+ const handlePermissionTabChange = (event, newValue) => {
+ setSelectedPermissionTab(newValue);
+ };
+
+ // Function to get permission counts
+ const getPermissionCounts = (permissions) => {
+ if (!permissions) return { app: 0, delegated: 0 };
+
+ let appCount = 0;
+ let delegatedCount = 0;
+
+ Object.entries(permissions).forEach(([resourceName, perms]) => {
+ if (perms.applicationPermissions) {
+ appCount += perms?.applicationPermissions?.length ?? 0;
+ }
+ if (perms.delegatedPermissions) {
+ delegatedCount += perms?.delegatedPermissions?.length ?? 0;
+ }
+ });
+
+ return { app: appCount, delegated: delegatedCount };
+ };
+
+ // Helper to get the display name for a resource ID
+ const getResourceDisplayName = (resourceId) => {
+ const spDetails = servicePrincipalDetails[resourceId];
+ return spDetails?.displayName || resourceId;
+ };
+
+ // Helper to get the appropriate permission description
+ const getPermissionDescription = (resourceId, permissionId, permissionType) => {
+ const spDetails = servicePrincipalDetails[resourceId];
+ if (!spDetails) return null;
+
+ if (permissionType === "application") {
+ const foundRole = spDetails.appRoles?.find((role) => role.id === permissionId);
+ return foundRole?.description || null;
+ } else {
+ const foundScope = spDetails.publishedPermissionScopes?.find(
+ (scope) => scope.id === permissionId
+ );
+ return foundScope?.userConsentDescription || foundScope?.description || null;
+ }
+ };
+
+ // Better checks for permissions object to prevent rendering errors
+ if (isLoading || loadingDetails) {
+
+ return (
+ <>
+ {title}
+
+ >
+ );
+ }
+
+ if (!permissions) {
+ return (
+
+ Select a template with permissions to see what will be consented.
+
+ );
+ }
+
+ // Ensure permissions is an object and has entries
+ if (
+ typeof permissions !== "object" ||
+ permissions === null ||
+ Object.keys(permissions).length === 0
+ ) {
+ return No permissions data available in this template. ;
+ }
+
+ return (
+
+
+ {title}
+
+
+
+ }
+ title="Application/Delegated Permissions"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Object.entries(permissions).map(([resourceId, resourcePerms]) => {
+ const resourceName = getResourceDisplayName(resourceId);
+ const hasAppPermissions =
+ resourcePerms.applicationPermissions &&
+ resourcePerms.applicationPermissions.length > 0;
+ const hasDelegatedPermissions =
+ resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0;
+
+ return (
+
+
+
+ {resourceName}
+ {showAppIds && (
+
+ {resourceId}
+
+ )}
+
+
+ {hasAppPermissions && (
+
+
+ Application Permissions ({resourcePerms.applicationPermissions.length})
+
+
+ {resourcePerms.applicationPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "application") ||
+ perm.description ||
+ "No description available";
+ return (
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {hasDelegatedPermissions && (
+
+
+ Delegated Permissions ({resourcePerms.delegatedPermissions.length})
+
+
+ {resourcePerms.delegatedPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "delegated") ||
+ perm.description ||
+ "No description available";
+ return (
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ {Object.entries(permissions)
+ .filter(
+ ([_, perms]) =>
+ perms.applicationPermissions && perms.applicationPermissions.length > 0
+ )
+ .map(([resourceId, resourcePerms]) => {
+ const resourceName = getResourceDisplayName(resourceId);
+ return (
+
+
+
+ {resourceName}
+ {showAppIds && (
+
+ {resourceId}
+
+ )}
+
+
+ {resourcePerms.applicationPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "application") ||
+ perm.description ||
+ "No description available";
+ return (
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+ {!Object.values(permissions).some(
+ (perms) => perms.applicationPermissions && perms.applicationPermissions.length > 0
+ ) && No application permissions in this template. }
+
+
+
+
+
+ {Object.entries(permissions)
+ .filter(
+ ([_, perms]) => perms.delegatedPermissions && perms.delegatedPermissions.length > 0
+ )
+ .map(([resourceId, resourcePerms]) => {
+ const resourceName = getResourceDisplayName(resourceId);
+ return (
+
+
+
+ {resourceName}
+ {showAppIds && (
+
+ {resourceId}
+
+ )}
+
+
+ {resourcePerms.delegatedPermissions.map((perm, idx) => {
+ const description =
+ getPermissionDescription(resourceId, perm.id, "delegated") ||
+ perm.description ||
+ "No description available";
+ return (
+
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+ {!Object.values(permissions).some(
+ (perms) => perms.delegatedPermissions && perms.delegatedPermissions.length > 0
+ ) && No delegated permissions in this template. }
+
+
+
+
+ );
+};
+
+export default CippPermissionPreview;
diff --git a/src/components/CippWizard/CippWizardAppApproval.jsx b/src/components/CippWizard/CippWizardAppApproval.jsx
index a32eef3a32de..8b0954a9ff60 100644
--- a/src/components/CippWizard/CippWizardAppApproval.jsx
+++ b/src/components/CippWizard/CippWizardAppApproval.jsx
@@ -1,51 +1,160 @@
-import { Stack } from "@mui/material";
+import { Stack, Typography, Alert, Box } from "@mui/material";
import CippWizardStepButtons from "./CippWizardStepButtons";
import { Grid } from "@mui/system";
import CippFormComponent from "../CippComponents/CippFormComponent";
import { getCippValidator } from "../../utils/get-cipp-validator";
import { CippFormCondition } from "../CippComponents/CippFormCondition";
+import { useEffect } from "react";
+import CippPermissionPreview from "../CippComponents/CippPermissionPreview";
+import { useWatch } from "react-hook-form";
+import { CippPropertyListCard } from "../CippCards/CippPropertyListCard";
export const CippWizardAppApproval = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props;
+ // Set default mode to Template
+ useEffect(() => {
+ // Proper initialization of the radio selection
+ const currentValue = formControl.getValues("configMode");
+
+ // If not set or undefined, set it to template
+ if (!currentValue) {
+ formControl.setValue("configMode", "template", {
+ shouldDirty: false,
+ shouldValidate: true,
+ });
+ }
+ }, [formControl]);
+
+ // Watch for the selected template to access permissions
+ const selectedTemplate = useWatch({
+ control: formControl.control,
+ name: "selectedTemplate",
+ });
+
return (
-
-
-
+
+ {/* Mode Selector */}
+
+
+ {/* Template Mode */}
+
+
+
+ Select an app approval template to deploy. Templates contain predefined permissions that
+ will be applied to the application.
+
getCippValidator(value, "guid"),
+ type="autoComplete"
+ name="selectedTemplate"
+ label="Select App Template"
+ api={{
+ url: "/api/ListAppApprovalTemplates",
+ queryKey: "appApprovalTemplates",
+ labelField: (item) => `${item.TemplateName}`,
+ valueField: "TemplateId",
+ addedField: {
+ AppId: "AppId",
+ AppName: "AppName",
+ PermissionSetId: "PermissionSetId",
+ PermissionSetName: "PermissionSetName",
+ Permissions: "Permissions",
+ },
+ showRefresh: true,
}}
- name="AppId"
+ validators={{ required: "A template is required" }}
formControl={formControl}
+ multiple={false}
/>
-
-
-
+
+ {selectedTemplate?.addedFields?.AppName && (
+
+
+
+
+ )}
+
+
+
+ {/* Manual Mode */}
+
+
+ getCippValidator(value, "guid"),
+ }}
+ name="AppId"
+ formControl={formControl}
+ />
+
+
+
+
+
+
+
+
{
onSuccess: (data) => {
// Instead of navigating away, stay on the page and refresh
if (copy || !template) {
- console.log("Copying or creating new template, redirecting to edit page");
- console.log("New template data:", data);
// If we're copying or creating new, update the URL to edit mode with the new template ID
const newTemplateId = data.data[0].Metadata.TemplateId;
router.push(
From 9e142bb3511a0c671365f2fb6bdb821d92d779cc Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 20 May 2025 19:44:20 -0400
Subject: [PATCH 035/865] conditional fields in standards
---
.../CippComponents/CippFormCondition.jsx | 56 +-
.../CippStandards/CippStandardAccordion.jsx | 77 ++-
src/data/standards.json | 510 ++++++++++++++----
3 files changed, 513 insertions(+), 130 deletions(-)
diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx
index 61a4b2375eb8..99e96fbb8157 100644
--- a/src/components/CippComponents/CippFormCondition.jsx
+++ b/src/components/CippComponents/CippFormCondition.jsx
@@ -1,5 +1,6 @@
import { useWatch } from "react-hook-form";
import isEqual from "lodash/isEqual"; // lodash for deep comparison
+import get from "lodash/get"; // Add lodash get for safer property access
import React from "react";
export const CippFormCondition = (props) => {
@@ -13,33 +14,39 @@ export const CippFormCondition = (props) => {
formControl,
disabled = false,
} = props;
+
if (
field === undefined ||
compareValue === undefined ||
children === undefined ||
formControl === undefined
) {
+ console.warn("CippFormCondition: Missing required props", {
+ field,
+ compareValue,
+ children,
+ formControl,
+ });
return null;
}
- let watcher = useWatch({ control: formControl.control, name: field });
+ // Watch the form field value
+ const watcher = useWatch({
+ control: formControl.control,
+ name: field,
+ });
- if (propertyName.includes(".")) {
- propertyName.split(".").forEach((prop) => {
- if (watcher?.[prop] !== undefined) {
- watcher = watcher?.[prop];
- }
- if (compareValue?.[prop] !== undefined) {
- compareValue = compareValue?.[prop];
- }
- });
- } else {
- if (watcher?.[propertyName] !== undefined) {
- watcher = watcher?.[propertyName];
- }
+ // Safer property access
+ let watchedValue = watcher;
+ let compareTargetValue = compareValue;
- if (compareValue?.[propertyName] !== undefined) {
- compareValue = compareValue?.[propertyName];
+ if (propertyName && propertyName !== "value") {
+ if (propertyName.includes(".")) {
+ watchedValue = get(watcher, propertyName);
+ compareTargetValue = get(compareValue, propertyName);
+ } else {
+ watchedValue = watcher?.[propertyName];
+ compareTargetValue = compareValue?.[propertyName];
}
}
@@ -63,6 +70,19 @@ export const CippFormCondition = (props) => {
return disableChildren(children);
}
+ // Improved debugging with more context
+ /*console.log("CippFormCondition", {
+ field,
+ watchedValue,
+ watcher,
+ compareTargetValue,
+ compareValue,
+ compareType,
+ action,
+ propertyName,
+ });*/
+
+ // Evaluation logic
switch (compareType) {
case "regex":
if (watcher?.match(new RegExp(compareValue))) {
@@ -74,7 +94,7 @@ export const CippFormCondition = (props) => {
return null;
case "is":
// Deep comparison for objects and arrays
- if (isEqual(watcher, compareValue)) {
+ if (isEqual(watchedValue, compareTargetValue)) {
return children;
}
if (action === "disable") {
@@ -84,7 +104,7 @@ export const CippFormCondition = (props) => {
case "isNot":
// Deep comparison for objects and arrays (negation)
- if (!isEqual(watcher, compareValue)) {
+ if (!isEqual(watchedValue, compareTargetValue)) {
return children;
}
if (action === "disable") {
diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx
index 8037de3b2e2b..ef43935d4c1a 100644
--- a/src/components/CippStandards/CippStandardAccordion.jsx
+++ b/src/components/CippStandards/CippStandardAccordion.jsx
@@ -37,6 +37,7 @@ import Intune from "../../icons/iconly/bulk/intune";
import GDAPRoles from "/src/data/GDAPRoles";
import timezoneList from "/src/data/timezoneList";
import standards from "/src/data/standards.json";
+import { CippFormCondition } from "../CippComponents/CippFormCondition";
const getAvailableActions = (disabledFeatures) => {
const allActions = [
@@ -108,6 +109,48 @@ const CippStandardAccordion = ({
const addedComponentsFilled =
standard.addedComponent?.every((component) => {
+ // Skip validation for components with conditions
+ if (component.condition) {
+ const conditionField = `${standardName}.${component.condition.field}`;
+ const conditionValue = _.get(watchedValues, conditionField);
+ const compareType = component.condition.compareType || "is";
+ const compareValue = component.condition.compareValue;
+ const propertyName = component.condition.propertyName || "value";
+
+ // Check if condition is met based on the compareType
+ let conditionMet = false;
+ if (propertyName === "value") {
+ switch (compareType) {
+ case "is":
+ conditionMet = _.isEqual(conditionValue, compareValue);
+ break;
+ case "isNot":
+ conditionMet = !_.isEqual(conditionValue, compareValue);
+ break;
+ // Add other compareType cases as needed
+ default:
+ conditionMet = false;
+ }
+ } else if (Array.isArray(conditionValue)) {
+ // Handle array values with propertyName
+ switch (compareType) {
+ case "valueEq":
+ conditionMet = conditionValue.some(
+ (item) => item?.[propertyName] === compareValue
+ );
+ break;
+ // Add other compareType cases for arrays as needed
+ default:
+ conditionMet = false;
+ }
+ }
+
+ // If condition is not met, we don't need to validate this field
+ if (!conditionMet) {
+ return true;
+ }
+ }
+
const isRequired = component.required !== false && component.type !== "switch";
if (!isRequired) return true;
return !!_.get(watchedValues, `${standardName}.${component.name}`);
@@ -425,14 +468,32 @@ const CippStandardAccordion = ({
{hasAddedComponents && (
- {standard.addedComponent?.map((component, idx) => (
-
- ))}
+ {standard.addedComponent?.map((component, idx) =>
+ component?.condition ? (
+
+
+
+ ) : (
+
+ )
+ )}
)}
diff --git a/src/data/standards.json b/src/data/standards.json
index 6af613f0a7eb..bdf95add5f7f 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -41,7 +41,10 @@
{
"name": "standards.AuditLog",
"cat": "Global Standards",
- "tag": ["CIS", "mip_search_auditlog"],
+ "tag": [
+ "CIS",
+ "mip_search_auditlog"
+ ],
"helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.",
"addedComponent": [],
"label": "Enable the Unified Audit Log",
@@ -49,7 +52,10 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Enable-OrganizationCustomization",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.ProfilePhotos",
@@ -99,7 +105,9 @@
"remediate": false
},
"powershellEquivalent": "Portal only",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.Branding",
@@ -161,7 +169,10 @@
{
"name": "standards.EnableCustomerLockbox",
"cat": "Global Standards",
- "tag": ["CIS", "CustomerLockBoxEnabled"],
+ "tag": [
+ "CIS",
+ "CustomerLockBoxEnabled"
+ ],
"helpText": "Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data",
"docsDescription": "Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.",
"addedComponent": [],
@@ -170,7 +181,9 @@
"impactColour": "info",
"addedDate": "2024-01-08",
"powershellEquivalent": "Set-OrganizationConfig -CustomerLockBoxEnabled $true",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.EnablePronouns",
@@ -197,7 +210,9 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgBetaAdminReportSetting -BodyParameter @{displayConcealedNames = $true}",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.DisableGuestDirectory",
@@ -211,7 +226,9 @@
"impactColour": "info",
"addedDate": "2022-05-04",
"powershellEquivalent": "Set-AzureADMSAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa82404023b'",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.DisableBasicAuthSMTP",
@@ -225,12 +242,18 @@
"impactColour": "warning",
"addedDate": "2021-11-16",
"powershellEquivalent": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.ActivityBasedTimeout",
"cat": "Global Standards",
- "tag": ["CIS", "spo_idle_session_timeout"],
+ "tag": [
+ "CIS",
+ "spo_idle_session_timeout"
+ ],
"helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps",
"addedComponent": [
{
@@ -268,7 +291,9 @@
"impactColour": "warning",
"addedDate": "2022-04-13",
"powershellEquivalent": "Portal or Graph API",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.AuthMethodsSettings",
@@ -336,10 +361,53 @@
"helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.",
"docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.",
"addedComponent": [
+ {
+ "type": "select",
+ "multiple": false,
+ "creatable": false,
+ "label": "App Approval Mode",
+ "name": "standards.AppDeploy.mode",
+ "options": [
+ {
+ "label": "Template",
+ "value": "template"
+ },
+ {
+ "label": "Copy Permissions",
+ "value": "copy"
+ }
+ ]
+ },
+ {
+ "type": "autoComplete",
+ "multiple": true,
+ "creatable": false,
+ "label": "Select Applications",
+ "name": "standards.AppDeploy.templateIds",
+ "api": {
+ "url": "/api/ListAppApprovalTemplates",
+ "labelField": "TemplateName",
+ "valueField": "TemplateId",
+ "queryKey": "StdAppApprovalTemplateList",
+ "addedField": {
+ "AppId": "AppId"
+ }
+ },
+ "condition": {
+ "field": "standards.AppDeploy.mode",
+ "compareType": "is",
+ "compareValue": "template"
+ }
+ },
{
"type": "textField",
"name": "standards.AppDeploy.appids",
- "label": "Application IDs, comma separated"
+ "label": "Application IDs, comma separated",
+ "condition": {
+ "field": "standards.AppDeploy.mode",
+ "compareType": "isNot",
+ "compareValue": "template"
+ }
}
],
"label": "Deploy Application",
@@ -361,12 +429,16 @@
"impactColour": "info",
"addedDate": "2023-04-25",
"powershellEquivalent": "Portal or Graph API",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.PWdisplayAppInformationRequiredState",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.",
"docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request",
"addedComponent": [],
@@ -375,7 +447,9 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.allowOTPTokens",
@@ -435,7 +509,9 @@
"impactColour": "info",
"addedDate": "2022-12-08",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.EnableHardwareOAuth",
@@ -495,12 +571,17 @@
"impactColour": "info",
"addedDate": "2022-03-15",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.PasswordExpireDisabled",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS", "PWAgePolicyNew"],
+ "tag": [
+ "CIS",
+ "PWAgePolicyNew"
+ ],
"helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.",
"docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.",
"addedComponent": [],
@@ -509,7 +590,10 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgDomain",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.ExternalMFATrusted",
@@ -545,7 +629,9 @@
{
"name": "standards.DisableTenantCreation",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.",
"docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.",
"addedComponent": [],
@@ -554,12 +640,17 @@
"impactColour": "info",
"addedDate": "2022-11-29",
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.EnableAppConsentRequests",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings",
"docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards",
"addedComponent": [
@@ -574,7 +665,9 @@
"impactColour": "info",
"addedDate": "2023-11-27",
"powershellEquivalent": "Update-MgPolicyAdminConsentRequestPolicy",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.NudgeMFA",
@@ -631,7 +724,9 @@
{
"name": "standards.DisableAppCreation",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Disables the ability for users to create App registrations in the tenant.",
"docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.",
"addedComponent": [],
@@ -640,7 +735,10 @@
"impactColour": "info",
"addedDate": "2024-03-20",
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.DisableSecurityGroupUsers",
@@ -699,12 +797,17 @@
"impactColour": "warning",
"addedDate": "2022-10-20",
"powershellEquivalent": "Graph API",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.OauthConsent",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Disables users from being able to consent to applications, except for those specified in the field below",
"docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.",
"addedComponent": [
@@ -720,12 +823,17 @@
"impactColour": "warning",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.OauthConsentLowSec",
"cat": "Entra (AAD) Standards",
- "tag": ["IntegratedApps"],
+ "tag": [
+ "IntegratedApps"
+ ],
"helpText": "Sets the default oauth consent level so users can consent to applications that have low risks.",
"docsDescription": "Allows users to consent to applications with low assigned risk.",
"label": "Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure)",
@@ -778,7 +886,9 @@
{
"name": "standards.StaleEntraDevices",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days.",
"docsDescription": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)",
"addedComponent": [
@@ -839,7 +949,9 @@
"impactColour": "danger",
"addedDate": "2023-12-18",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.DisableVoice",
@@ -853,7 +965,9 @@
"impactColour": "danger",
"addedDate": "2023-12-18",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.DisableEmail",
@@ -939,7 +1053,9 @@
{
"name": "standards.OutBoundSpamAlert",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Set the Outbound Spam Alert e-mail address",
"docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.",
"addedComponent": [
@@ -954,7 +1070,9 @@
"impactColour": "info",
"addedDate": "2023-05-03",
"powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.MessageExpiration",
@@ -1017,7 +1135,9 @@
"impactColour": "info",
"addedDate": "2024-04-26",
"powershellEquivalent": "Set-RemoteDomain -Identity 'Default' -TNEFEnabled $false",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.FocusedInbox",
@@ -1131,7 +1251,9 @@
{
"name": "standards.SpoofWarn",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA",
"docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)",
"addedComponent": [
@@ -1165,12 +1287,18 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Set-ExternalInOutlook \u2013Enabled $true or $false",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.EnableMailTips",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_mailtipsenabled"],
+ "tag": [
+ "CIS",
+ "exo_mailtipsenabled"
+ ],
"helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements",
"addedComponent": [
{
@@ -1186,7 +1314,10 @@
"impactColour": "info",
"addedDate": "2024-01-14",
"powershellEquivalent": "Set-OrganizationConfig",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.TeamsMeetingsByDefault",
@@ -1236,7 +1367,9 @@
{
"name": "standards.RotateDKIM",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit",
"addedComponent": [],
"label": "Rotate DKIM keys that are 1024 bit to 2048 bit",
@@ -1244,12 +1377,17 @@
"impactColour": "info",
"addedDate": "2023-03-14",
"powershellEquivalent": "Rotate-DkimSigningConfig",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.AddDKIM",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Enables DKIM for all domains that currently support it",
"addedComponent": [],
"label": "Enables DKIM for all domains that currently support it",
@@ -1257,12 +1395,18 @@
"impactColour": "info",
"addedDate": "2023-03-14",
"powershellEquivalent": "New-DkimSigningConfig and Set-DkimSigningConfig",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.EnableMailboxAuditing",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_mailboxaudit"],
+ "tag": [
+ "CIS",
+ "exo_mailboxaudit"
+ ],
"helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.",
"docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.",
"addedComponent": [],
@@ -1271,7 +1415,10 @@
"impactColour": "info",
"addedDate": "2024-01-08",
"powershellEquivalent": "Set-OrganizationConfig -AuditDisabled $false",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.SendReceiveLimitTenant",
@@ -1374,7 +1521,9 @@
{
"name": "standards.EXOOutboundSpamLimits",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ",
"docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.",
"addedComponent": [
@@ -1403,8 +1552,14 @@
"name": "standards.EXOOutboundSpamLimits.ActionWhenThresholdReached",
"label": "Action When Threshold Reached",
"options": [
- { "label": "Alert", "value": "Alert" },
- { "label": "Block User", "value": "BlockUser" },
+ {
+ "label": "Alert",
+ "value": "Alert"
+ },
+ {
+ "label": "Block User",
+ "value": "BlockUser"
+ },
{
"label": "Block user from sending mail for the rest of the day",
"value": "BlockUserForToday"
@@ -1417,12 +1572,18 @@
"impactColour": "info",
"addedDate": "2025-05-13",
"powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy",
- "recommendedBy": ["CIPP", "CIS"]
+ "recommendedBy": [
+ "CIPP",
+ "CIS"
+ ]
},
{
"name": "standards.DisableExternalCalendarSharing",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_individualsharing"],
+ "tag": [
+ "CIS",
+ "exo_individualsharing"
+ ],
"helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.",
"docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.",
"addedComponent": [],
@@ -1431,12 +1592,16 @@
"impactColour": "info",
"addedDate": "2024-01-08",
"powershellEquivalent": "Get-SharingPolicy | Set-SharingPolicy -Enabled $False",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.AutoAddProxy",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Automatically adds all available domains as a proxy address.",
"docsDescription": "Automatically finds all available domain names in the tenant, and tries to add proxy addresses based on the user's UPN to each of these.",
"addedComponent": [],
@@ -1455,7 +1620,10 @@
{
"name": "standards.DisableAdditionalStorageProviders",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_storageproviderrestricted"],
+ "tag": [
+ "CIS",
+ "exo_storageproviderrestricted"
+ ],
"helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.",
"docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.",
"addedComponent": [],
@@ -1464,7 +1632,9 @@
"impactColour": "info",
"addedDate": "2024-01-17",
"powershellEquivalent": "Get-OwaMailboxPolicy | Set-OwaMailboxPolicy -AdditionalStorageProvidersEnabled $False",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.AntiSpamSafeList",
@@ -1566,7 +1736,10 @@
{
"name": "standards.DisableOutlookAddins",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_outlookaddins"],
+ "tag": [
+ "CIS",
+ "exo_outlookaddins"
+ ],
"helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.",
"docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.",
"addedComponent": [],
@@ -1575,7 +1748,9 @@
"impactColour": "warning",
"addedDate": "2024-02-05",
"powershellEquivalent": "Get-ManagementRoleAssignment | Remove-ManagementRoleAssignment",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.SafeSendersDisable",
@@ -1593,7 +1768,9 @@
"impactColour": "warning",
"addedDate": "2023-10-26",
"powershellEquivalent": "Set-MailboxJunkEmailConfiguration",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.DelegateSentItems",
@@ -1627,7 +1804,9 @@
"impactColour": "warning",
"addedDate": "2022-05-25",
"powershellEquivalent": "Set-Mailbox",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.UserSubmissions",
@@ -1669,7 +1848,9 @@
{
"name": "standards.DisableSharedMailbox",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.",
"docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.",
"addedComponent": [],
@@ -1678,12 +1859,19 @@
"impactColour": "warning",
"addedDate": "2021-11-16",
"powershellEquivalent": "Get-Mailbox & Update-MgUser",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.EXODisableAutoForwarding",
"cat": "Exchange Standards",
- "tag": ["CIS", "mdo_autoforwardingmode", "mdo_blockmailforward"],
+ "tag": [
+ "CIS",
+ "mdo_autoforwardingmode",
+ "mdo_blockmailforward"
+ ],
"helpText": "Disables the ability for users to automatically forward e-mails to external recipients.",
"docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.",
"addedComponent": [],
@@ -1692,7 +1880,10 @@
"impactColour": "danger",
"addedDate": "2024-07-26",
"powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'Off'",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.RetentionPolicyTag",
@@ -1773,7 +1964,11 @@
{
"name": "standards.SafeLinksPolicy",
"cat": "Defender Standards",
- "tag": ["CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"],
+ "tag": [
+ "CIS",
+ "mdo_safelinksforemail",
+ "mdo_safelinksforOfficeApps"
+ ],
"helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders",
"addedComponent": [
{
@@ -1805,7 +2000,9 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-SafeLinksPolicy or New-SafeLinksPolicy",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.AntiPhishPolicy",
@@ -2014,12 +2211,19 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-AntiPhishPolicy or New-AntiPhishPolicy",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.SafeAttachmentPolicy",
"cat": "Defender Standards",
- "tag": ["CIS", "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy"],
+ "tag": [
+ "CIS",
+ "mdo_safedocuments",
+ "mdo_commonattachmentsfilter",
+ "mdo_safeattachmentpolicy"
+ ],
"helpText": "This creates a Safe Attachment policy",
"addedComponent": [
{
@@ -2079,12 +2283,16 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-SafeAttachmentPolicy or New-SafeAttachmentPolicy",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.AtpPolicyForO365",
"cat": "Defender Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.",
"addedComponent": [
{
@@ -2100,7 +2308,9 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-AtpPolicyForO365",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.PhishingSimulations",
@@ -2143,7 +2353,12 @@
{
"name": "standards.MalwareFilterPolicy",
"cat": "Defender Standards",
- "tag": ["CIS", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware"],
+ "tag": [
+ "CIS",
+ "mdo_zapspam",
+ "mdo_zapphish",
+ "mdo_zapmalware"
+ ],
"helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.",
"addedComponent": [
{
@@ -2218,7 +2433,9 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-MalwareFilterPolicy or New-MalwareFilterPolicy",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.PhishSimSpoofIntelligence",
@@ -2545,7 +2762,9 @@
"impactColour": "info",
"addedDate": "2023-05-19",
"powershellEquivalent": "Graph API",
- "recommendedBy": ["CIPP"]
+ "recommendedBy": [
+ "CIPP"
+ ]
},
{
"name": "standards.intuneBrandingProfile",
@@ -2669,9 +2888,18 @@
"label": "MDM User Scope?",
"type": "radio",
"options": [
- { "label": "All", "value": "all" },
- { "label": "None", "value": "none" },
- { "label": "Custom Group", "value": "selected" }
+ {
+ "label": "All",
+ "value": "all"
+ },
+ {
+ "label": "None",
+ "value": "none"
+ },
+ {
+ "label": "Custom Group",
+ "value": "selected"
+ }
]
},
{
@@ -2887,7 +3115,9 @@
{
"name": "standards.SPAzureB2B",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled",
"addedComponent": [],
"label": "Enable SharePoint and OneDrive integration with Azure AD B2B",
@@ -2895,12 +3125,16 @@
"impactColour": "info",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -EnableAzureADB2BIntegration $true",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.SPDisallowInfectedFiles",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Ensure Office 365 SharePoint infected files are disallowed for download",
"addedComponent": [],
"label": "Disallow downloading infected files from SharePoint",
@@ -2908,7 +3142,10 @@
"impactColour": "info",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -DisallowInfectedFileDownload $true",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.SPDisableLegacyWorkflows",
@@ -2926,7 +3163,9 @@
{
"name": "standards.SPDirectSharing",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Ensure default link sharing is set to Direct in SharePoint and OneDrive",
"addedComponent": [],
"label": "Default sharing to Direct users",
@@ -2934,12 +3173,17 @@
"impactColour": "warning",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType Direct",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.SPExternalUserExpiration",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Ensure guest access to a site or OneDrive will expire automatically",
"addedComponent": [
{
@@ -2953,12 +3197,16 @@
"impactColour": "warning",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -ExternalUserExpireInDays 30 -ExternalUserExpirationRequired $True",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.SPEmailAttestation",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Ensure re-authentication with verification code is restricted",
"addedComponent": [
{
@@ -2972,7 +3220,10 @@
"impactColour": "warning",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.DisableAddShortcutsToOneDrive",
@@ -3039,7 +3290,10 @@
{
"name": "standards.DisableSharePointLegacyAuth",
"cat": "SharePoint Standards",
- "tag": ["CIS", "spo_legacy_auth"],
+ "tag": [
+ "CIS",
+ "spo_legacy_auth"
+ ],
"helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.",
"docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.",
"addedComponent": [],
@@ -3048,12 +3302,17 @@
"impactColour": "warning",
"addedDate": "2024-02-05",
"powershellEquivalent": "Set-SPOTenant -LegacyAuthProtocolsEnabled $false",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.sharingCapability",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level",
"addedComponent": [
{
@@ -3086,12 +3345,17 @@
"impactColour": "danger",
"addedDate": "2022-06-15",
"powershellEquivalent": "Update-MgBetaAdminSharePointSetting",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.DisableReshare",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access",
"docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level",
"addedComponent": [],
@@ -3100,7 +3364,10 @@
"impactColour": "danger",
"addedDate": "2022-06-15",
"powershellEquivalent": "Update-MgBetaAdminSharePointSetting",
- "recommendedBy": ["CIS", "CIPP"]
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ]
},
{
"name": "standards.DisableUserSiteCreate",
@@ -3164,7 +3431,9 @@
{
"name": "standards.sharingDomainRestriction",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS"
+ ],
"helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.",
"addedComponent": [
{
@@ -3271,7 +3540,9 @@
"impactColour": "info",
"addedDate": "2024-11-12",
"powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers EveryoneInCompanyExcludingGuests -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.TeamsEmailIntegration",
@@ -3291,7 +3562,9 @@
"impactColour": "info",
"addedDate": "2024-07-30",
"powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.TeamsExternalFileSharing",
@@ -3330,7 +3603,9 @@
"impactColour": "info",
"addedDate": "2024-07-28",
"powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false",
- "recommendedBy": ["CIS"]
+ "recommendedBy": [
+ "CIS"
+ ]
},
{
"name": "standards.TeamsEnrollUser",
@@ -3754,11 +4029,26 @@
"label": "Who should this template be assigned to?",
"type": "radio",
"options": [
- { "label": "Do not assign", "value": "On" },
- { "label": "Assign to all users", "value": "allLicensedUsers" },
- { "label": "Assign to all devices", "value": "AllDevices" },
- { "label": "Assign to all users and devices", "value": "AllDevicesAndUsers" },
- { "label": "Assign to Custom Group", "value": "customGroup" }
+ {
+ "label": "Do not assign",
+ "value": "On"
+ },
+ {
+ "label": "Assign to all users",
+ "value": "allLicensedUsers"
+ },
+ {
+ "label": "Assign to all devices",
+ "value": "AllDevices"
+ },
+ {
+ "label": "Assign to all users and devices",
+ "value": "AllDevicesAndUsers"
+ },
+ {
+ "label": "Assign to Custom Group",
+ "value": "customGroup"
+ }
]
},
{
@@ -3833,10 +4123,22 @@
"label": "What state should we deploy this template in?",
"type": "radio",
"options": [
- { "value": "donotchange", "label": "Do not change state" },
- { "value": "Enabled", "label": "Set to enabled" },
- { "value": "Disabled", "label": "Set to disabled" },
- { "value": "enabledForReportingButNotEnforced", "label": "Set to report only" }
+ {
+ "value": "donotchange",
+ "label": "Do not change state"
+ },
+ {
+ "value": "Enabled",
+ "label": "Set to enabled"
+ },
+ {
+ "value": "Disabled",
+ "label": "Set to disabled"
+ },
+ {
+ "value": "enabledForReportingButNotEnforced",
+ "label": "Set to report only"
+ }
]
}
]
@@ -3893,4 +4195,4 @@
}
]
}
-]
+]
\ No newline at end of file
From 03bf5d30ecc191d86ab6b6cccf59fd20ff46176f Mon Sep 17 00:00:00 2001
From: Esco
Date: Wed, 21 May 2025 09:34:48 +0200
Subject: [PATCH 036/865] feat: PhishSimSpoofIntelligence replace switch
---
src/data/standards.json | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/data/standards.json b/src/data/standards.json
index bdf95add5f7f..1a85ae02f851 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -2443,6 +2443,13 @@
"tag": [],
"helpText": "This adds allowed domains to the Spoof Intelligence Allow/Block List.",
"addedComponent": [
+ {
+ "type": "switch",
+ "label": "Remove extra domains from the allow list",
+ "name": "standards.PhishSimSpoofIntelligence.RemoveExtraDomains",
+ "defaultValue": false,
+ "required": false
+ },
{
"type": "autoComplete",
"multiple": true,
From 95e731c5b58b15ac9925f271bb2edd1568634424 Mon Sep 17 00:00:00 2001
From: Esco
Date: Wed, 21 May 2025 10:06:27 +0200
Subject: [PATCH 037/865] feat: PhishingSimulations replace switch
---
src/data/standards.json | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/data/standards.json b/src/data/standards.json
index 1a85ae02f851..b92ad4f12682 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -2341,6 +2341,13 @@
"required": false,
"label": "Phishing Simulation Urls",
"name": "standards.PhishingSimulations.PhishingSimUrls"
+ },
+ {
+ "type": "switch",
+ "label": "Remove extra urls",
+ "name": "standards.PhishingSimulations.RemoveExtraUrls",
+ "defaultValue": false,
+ "required": false
}
],
"label": "Phishing Simulation Configuration",
From 832cacc93a4d3dbba7086f92325314683e31b96c Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Wed, 21 May 2025 14:22:27 +0200
Subject: [PATCH 038/865] remove logging
---
.../CippWizard/CippBaselinesStep.jsx | 119 ++++++++++++++++++
.../CippWizard/CippTenantModeDeploy.jsx | 67 ++--------
src/pages/onboardingv2.js | 4 +-
3 files changed, 128 insertions(+), 62 deletions(-)
create mode 100644 src/components/CippWizard/CippBaselinesStep.jsx
diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx
new file mode 100644
index 000000000000..dafb80088ce1
--- /dev/null
+++ b/src/components/CippWizard/CippBaselinesStep.jsx
@@ -0,0 +1,119 @@
+import { useState } from "react";
+import { Alert, Stack, Typography, FormControl, FormLabel, Box } from "@mui/material";
+import CippFormComponent from "../CippComponents/CippFormComponent";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippFormCondition } from "../CippComponents/CippFormCondition";
+import { ApiGetCall } from "../../api/ApiCall";
+
+export const CippBaselinesStep = (props) => {
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+ const values = formControl.getValues();
+
+ // Fetch available baselines from API
+ const baselinesApi = ApiGetCall({
+ url: "/api/ListCommunityRepos",
+ queryKey: "CommunityRepos",
+ });
+
+ // Create baseline options from the API response
+ const baselineOptions = baselinesApi.isSuccess
+ ? baselinesApi.data?.Results?.map((repo) => ({
+ label: `${repo.Name} (${repo.Owner})`,
+ value: repo.Id,
+ description: repo.Description || "No description available",
+ })) || []
+ : [];
+
+ return (
+
+
+
+
+ Baselines are template configurations that can be used as examples for setting up your
+ environment.
+
+
+ Downloading these baselines will create templates in your CIPP instance. These templates
+ won't make any changes to your environment, but can be used as examples on how to setup
+ environments. Each template library contains multiple templates,
+
+
+ CIPP Templates by CyberDrain contain several example standards, including low,
+ medium, and high priority standards
+
+
+ JoeyV's Conditional Access Baseline contains a Microsoft approved baseline for
+ Conditional Access, following the Microsoft best practices.
+
+
+ OpenIntuneBaseline contains Intune templates, the baseline is a community driven
+ baseline for Intune, based on CIS, NIST, and more benchmarks. It's considered the
+ leading baseline for Intune.
+
+
+
+
+
+
+ Baseline Configuration
+
+
+
+
+
+
+
+
+ Select baselines to download:
+
+ {baselinesApi.isLoading ? (
+ Loading available baselines...
+ ) : baselinesApi.isError ? (
+ Failed to load baselines. Please try again later.
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default CippBaselinesStep;
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index 1e7ad8999a44..ba34e20c0c2f 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -92,14 +92,6 @@ export const CippTenantModeDeploy = (props) => {
loading: true,
});
- // Log the token data for debugging
- console.log("GDAP Auth Success - Token Data:", {
- tenantId: tokenData.tenantId,
- refreshToken: tokenData.refreshToken ? "present" : "missing",
- tenantMode: tokenData.tenantMode,
- allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement,
- });
-
// Explicitly call the updateRefreshToken API
updateRefreshToken.mutate(
{
@@ -146,39 +138,17 @@ export const CippTenantModeDeploy = (props) => {
loading: true,
});
- // Log the token data for debugging
- console.log("Per-Tenant Auth Success - Token Data:", {
- tenantId: tokenData.tenantId,
- tenantMode: tokenData.tenantMode,
- });
-
// For perTenant mode or mixed mode with perTenant auth, add the tenant to the cache
if (tenantMode === "perTenant" || tenantMode === "mixed") {
// Call the AddTenant API to add the tenant to the cache with directTenant status
- addTenant.mutate(
- {
- url: "/api/ExecAddTenant",
- data: {
- tenantId: tokenData.tenantId,
- },
+ console.log(tokenData);
+ addTenant.mutate({
+ url: "/api/ExecAddTenant",
+ data: {
+ tenantId: tokenData.tenantId,
+ access_token: tokenData.accessToken,
},
- {
- onSuccess: (data) => {
- console.log("Add Tenant Success:", data);
- setPerTenantAuthStatus({
- success: true,
- loading: false,
- });
- },
- onError: (error) => {
- console.error("Add Tenant Error:", error);
- setPerTenantAuthStatus({
- success: false,
- loading: false,
- });
- },
- }
- );
+ });
} else {
// If not adding tenant, still update state
setPerTenantAuthStatus({
@@ -206,29 +176,6 @@ export const CippTenantModeDeploy = (props) => {
}
}, [addTenant.isError]);
- // Debug logging for API states
- useEffect(() => {
- if (
- updateRefreshToken.isLoading ||
- updateRefreshToken.isSuccess ||
- updateRefreshToken.isError
- ) {
- console.log("updateRefreshToken state:", {
- isLoading: updateRefreshToken.isLoading,
- isSuccess: updateRefreshToken.isSuccess,
- isError: updateRefreshToken.isError,
- data: updateRefreshToken.data,
- error: updateRefreshToken.error ? getCippError(updateRefreshToken.error) : null,
- });
- }
- }, [
- updateRefreshToken.isLoading,
- updateRefreshToken.isSuccess,
- updateRefreshToken.isError,
- updateRefreshToken.data,
- updateRefreshToken.error,
- ]);
-
return (
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index c5e0a4a89134..afe22004faef 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -5,6 +5,7 @@ import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx";
import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx";
import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx";
import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx";
+import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx";
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -68,8 +69,7 @@ const Page = () => {
{
title: "Step 4",
description: "Baselines",
- component: CippDeploymentStep,
- //give choice to download baselines from repos.
+ component: CippBaselinesStep,
},
{
title: "Step 5",
From 8b1dea6a891fb8159b85dd87433f4c2b5798dceb Mon Sep 17 00:00:00 2001
From: Esco
Date: Wed, 21 May 2025 15:01:59 +0200
Subject: [PATCH 039/865] chore: conditional input fields
---
src/data/standards.json | 35 ++++++++++++++++++++++++++++++-----
1 file changed, 30 insertions(+), 5 deletions(-)
diff --git a/src/data/standards.json b/src/data/standards.json
index bdf95add5f7f..1a64f2dee762 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -2275,7 +2275,12 @@
"type": "textField",
"name": "standards.SafeAttachmentPolicy.RedirectAddress",
"label": "Redirect Address",
- "required": false
+ "required": false,
+ "condition": {
+ "field": "standards.SafeAttachmentPolicy.Redirect",
+ "compareType": "is",
+ "compareValue": true
+ }
}
],
"label": "Default Safe Attachment Policy",
@@ -2413,7 +2418,12 @@
"type": "textField",
"name": "standards.MalwareFilterPolicy.InternalSenderAdminAddress",
"required": false,
- "label": "Internal Sender Admin Address"
+ "label": "Internal Sender Admin Address",
+ "condition": {
+ "field": "standards.MalwareFilterPolicy.EnableInternalSenderAdminNotifications",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "switch",
@@ -2425,7 +2435,12 @@
"type": "textField",
"name": "standards.MalwareFilterPolicy.ExternalSenderAdminAddress",
"required": false,
- "label": "External Sender Admin Address"
+ "label": "External Sender Admin Address",
+ "condition": {
+ "field": "standards.MalwareFilterPolicy.EnableExternalSenderAdminNotifications",
+ "compareType": "is",
+ "compareValue": true
+ }
}
],
"label": "Default Malware Filter Policy",
@@ -2713,7 +2728,12 @@
"creatable": true,
"required": false,
"name": "standards.SpamFilterPolicy.LanguageBlockList",
- "label": "Languages to block (uppercase ISO 639-1 two-letter)"
+ "label": "Languages to block (uppercase ISO 639-1 two-letter)",
+ "condition": {
+ "field": "standards.SpamFilterPolicy.EnableLanguageBlockList",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "switch",
@@ -2727,7 +2747,12 @@
"creatable": true,
"required": false,
"name": "standards.SpamFilterPolicy.RegionBlockList",
- "label": "Regions to block (uppercase ISO 3166-1 two-letter)"
+ "label": "Regions to block (uppercase ISO 3166-1 two-letter)",
+ "condition": {
+ "field": "standards.SpamFilterPolicy.EnableRegionBlockList",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "autoComplete",
From b501099ffcbf74f6a5a9db76843e0c877a0ca124 Mon Sep 17 00:00:00 2001
From: Zac Richards <107489668+Zacgoose@users.noreply.github.com>
Date: Wed, 21 May 2025 23:35:43 +0800
Subject: [PATCH 040/865] camelCaseFixes
---
src/components/CippComponents/BPASyncDialog.jsx | 2 +-
src/pages/cipp/super-admin/function-offloading.js | 2 +-
src/pages/endpoint/applications/list/add.jsx | 4 ++--
src/pages/identity/administration/users/user/index.jsx | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/components/CippComponents/BPASyncDialog.jsx b/src/components/CippComponents/BPASyncDialog.jsx
index 10ae3cecd724..014e033747e6 100644
--- a/src/components/CippComponents/BPASyncDialog.jsx
+++ b/src/components/CippComponents/BPASyncDialog.jsx
@@ -29,7 +29,7 @@ export const BPASyncDialog = ({ createDialog }) => {
const [isSyncing, setIsSyncing] = useState(false);
const bpaSyncResults = ApiPostCall({
- urlfromdata: true,
+ urlFromData: true,
});
const handleForm = (values) => {
diff --git a/src/pages/cipp/super-admin/function-offloading.js b/src/pages/cipp/super-admin/function-offloading.js
index 642c9f48a411..d8dafb638d38 100644
--- a/src/pages/cipp/super-admin/function-offloading.js
+++ b/src/pages/cipp/super-admin/function-offloading.js
@@ -28,7 +28,7 @@ const Page = () => {
});
const deleteOffloadEntry = ApiPostCall({
- urlfromdata: true,
+ urlFromData: true,
relatedQueryKeys: ["execOffloadFunctions"],
});
diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx
index d82b40deeceb..f30d59aa4462 100644
--- a/src/pages/endpoint/applications/list/add.jsx
+++ b/src/pages/endpoint/applications/list/add.jsx
@@ -49,11 +49,11 @@ const ApplicationDeploymentForm = () => {
};
const ChocosearchResults = ApiPostCall({
- urlfromData: true,
+ urlFromData: true,
});
const winGetSearchResults = ApiPostCall({
- urlfromData: true,
+ urlFromData: true,
});
const searchApp = (searchText, type) => {
diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx
index e91db17cadfc..0ef33533aa73 100644
--- a/src/pages/identity/administration/users/user/index.jsx
+++ b/src/pages/identity/administration/users/user/index.jsx
@@ -87,7 +87,7 @@ const Page = () => {
});
const userBulkRequest = ApiPostCall({
- urlfromdata: true,
+ urlFromData: true,
});
useEffect(() => {
From af24c2e0a98535718bd1f5e85a16b06b00c20c49 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 12:13:01 -0400
Subject: [PATCH 041/865] cleanup form defaults
remove values when conditions are not met
---
.../CippComponents/CippFormCondition.jsx | 317 +++++++-----------
.../CippWizard/CippWizardAppApproval.jsx | 14 -
.../CippWizard/CippWizardConfirmation.js | 13 +-
src/pages/tenant/tools/appapproval/index.js | 2 +-
src/utils/get-cipp-formatting.js | 18 +
5 files changed, 150 insertions(+), 214 deletions(-)
diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx
index 99e96fbb8157..962b335f14de 100644
--- a/src/components/CippComponents/CippFormCondition.jsx
+++ b/src/components/CippComponents/CippFormCondition.jsx
@@ -1,7 +1,7 @@
import { useWatch } from "react-hook-form";
import isEqual from "lodash/isEqual"; // lodash for deep comparison
import get from "lodash/get"; // Add lodash get for safer property access
-import React from "react";
+import React, { useEffect } from "react"; // Added useEffect
export const CippFormCondition = (props) => {
let {
@@ -50,6 +50,118 @@ export const CippFormCondition = (props) => {
}
}
+ // Function to recursively extract field names from child components
+ const extractFieldNames = (children) => {
+ const fieldNames = [];
+
+ React.Children.forEach(children, (child) => {
+ if (!React.isValidElement(child)) return;
+
+ // Check if the child is a CippFormComponent with a name prop
+ if (child.props?.name && child.type?.name === "CippFormComponent") {
+ fieldNames.push(child.props.name);
+ }
+
+ // Check if child has nested children
+ if (child.props?.children) {
+ fieldNames.push(...extractFieldNames(child.props.children));
+ }
+ });
+
+ return fieldNames;
+ };
+
+ // Function to check if the condition is met
+ const isConditionMet = () => {
+ switch (compareType) {
+ case "regex":
+ return watcher?.match(new RegExp(compareValue));
+ case "is":
+ return isEqual(watchedValue, compareTargetValue);
+ case "isNot":
+ return !isEqual(watchedValue, compareTargetValue);
+ case "contains":
+ if (Array.isArray(watcher)) {
+ return watcher.includes(compareValue);
+ } else if (typeof watcher === "string") {
+ return watcher.includes(compareValue);
+ } else if (typeof watcher === "object" && watcher !== null) {
+ return compareValue in watcher;
+ }
+ return false;
+ case "doesNotContain":
+ if (watcher === undefined || watcher === null) {
+ return true;
+ } else if (Array.isArray(watcher)) {
+ return !watcher.includes(compareValue);
+ } else if (typeof watcher === "string") {
+ return !watcher.includes(compareValue);
+ } else if (typeof watcher === "object") {
+ return !(compareValue in watcher);
+ }
+ return true;
+ case "greaterThan":
+ return (
+ typeof watcher === "number" && typeof compareValue === "number" && watcher > compareValue
+ );
+ case "lessThan":
+ return (
+ typeof watcher === "number" && typeof compareValue === "number" && watcher < compareValue
+ );
+ case "arrayLength":
+ return (
+ Array.isArray(watcher) &&
+ typeof compareValue === "number" &&
+ watcher.length >= compareValue
+ );
+ case "hasValue":
+ return (
+ (watcher !== undefined && watcher !== null && watcher !== "") ||
+ (watcher?.value !== undefined && watcher?.value !== null && watcher?.value !== "")
+ );
+ case "labelEq":
+ return Array.isArray(watcher) && watcher.some((item) => item?.label === compareValue);
+ case "labelContains":
+ return (
+ Array.isArray(watcher) &&
+ watcher.some(
+ (item) => typeof item?.label === "string" && item.label.includes(compareValue)
+ )
+ );
+ case "valueEq":
+ return Array.isArray(watcher) && watcher.some((item) => item?.value === compareValue);
+ case "valueNotEq":
+ return Array.isArray(watcher) && watcher.some((item) => item?.value !== compareValue);
+ case "valueContains":
+ return (
+ Array.isArray(watcher) &&
+ watcher.some(
+ (item) => typeof item?.value === "string" && item.value.includes(compareValue)
+ )
+ );
+ default:
+ return false;
+ }
+ };
+
+ // Reset field values when condition is not met and action is "hide"
+ useEffect(() => {
+ if (action === "hide" && !isConditionMet()) {
+ const fieldNames = extractFieldNames(children);
+
+ // Reset each field
+ fieldNames.forEach((fieldName) => {
+ // Don't reset if the field doesn't exist in the form
+ if (formControl.getValues(fieldName) !== undefined) {
+ formControl.setValue(fieldName, null, {
+ shouldValidate: false,
+ shouldDirty: false,
+ });
+ }
+ });
+ }
+ }, [watcher, action]);
+
const disableChildren = (children) => {
return React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
@@ -70,201 +182,14 @@ export const CippFormCondition = (props) => {
return disableChildren(children);
}
- // Improved debugging with more context
- /*console.log("CippFormCondition", {
- field,
- watchedValue,
- watcher,
- compareTargetValue,
- compareValue,
- compareType,
- action,
- propertyName,
- });*/
-
- // Evaluation logic
- switch (compareType) {
- case "regex":
- if (watcher?.match(new RegExp(compareValue))) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
- case "is":
- // Deep comparison for objects and arrays
- if (isEqual(watchedValue, compareTargetValue)) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "isNot":
- // Deep comparison for objects and arrays (negation)
- if (!isEqual(watchedValue, compareTargetValue)) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "contains":
- if (Array.isArray(watcher)) {
- if (watcher.includes(compareValue)) {
- return children;
- }
- } else if (typeof watcher === "string") {
- if (watcher.includes(compareValue)) {
- return children;
- }
- } else if (typeof watcher === "object" && watcher !== null && compareValue in watcher) {
- // Check if object contains the key
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "doesNotContain":
- if (Array.isArray(watcher)) {
- if (!watcher.includes(compareValue)) {
- return children;
- }
- } else if (typeof watcher === "string") {
- if (!watcher.includes(compareValue)) {
- return children;
- }
- //extra elsseif; if the value is undefined or null, return children because it does not contain the compareValue
- } else if (watcher === undefined || watcher === null) {
- return children;
- } else if (typeof watcher === "object" && !(compareValue in watcher)) {
- // Check if object does not contain the key
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "greaterThan":
- if (
- typeof watcher === "number" &&
- typeof compareValue === "number" &&
- watcher > compareValue
- ) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "lessThan":
- if (
- typeof watcher === "number" &&
- typeof compareValue === "number" &&
- watcher < compareValue
- ) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "arrayLength":
- if (
- Array.isArray(watcher) &&
- typeof compareValue === "number" &&
- watcher.length >= compareValue
- ) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "hasValue":
- if (
- (watcher !== undefined && watcher !== null && watcher !== "") ||
- (watcher?.value !== undefined && watcher?.value !== null && watcher?.value !== "")
- ) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- /*
- * NEW CASES
- */
- case "labelEq":
- // Checks if any object in array has .label exactly equal to compareValue
- if (Array.isArray(watcher) && watcher.some((item) => item?.label === compareValue)) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "labelContains":
- // Checks if any object in array has a .label that contains compareValue
- if (
- Array.isArray(watcher) &&
- watcher.some((item) => typeof item?.label === "string" && item.label.includes(compareValue))
- ) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
+ // Return based on condition check
+ const conditionMet = isConditionMet();
- case "valueEq":
- // Checks if any object in array has .value exactly equal to compareValue
- if (Array.isArray(watcher) && watcher.some((item) => item?.value === compareValue)) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "valueNotEq":
- // Checks if any object in array has .value exactly equal to compareValue
- if (Array.isArray(watcher) && watcher.some((item) => item?.value !== compareValue)) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- case "valueContains":
- // Checks if any object in array has a .value that contains compareValue
- if (
- Array.isArray(watcher) &&
- watcher.some((item) => typeof item?.value === "string" && item.value.includes(compareValue))
- ) {
- return children;
- }
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
-
- default:
- if (action === "disable") {
- return disableChildren(children);
- }
- return null;
+ if (conditionMet) {
+ return children;
+ } else if (action === "disable") {
+ return disableChildren(children);
+ } else {
+ return null;
}
};
diff --git a/src/components/CippWizard/CippWizardAppApproval.jsx b/src/components/CippWizard/CippWizardAppApproval.jsx
index 8b0954a9ff60..2502fb1a3103 100644
--- a/src/components/CippWizard/CippWizardAppApproval.jsx
+++ b/src/components/CippWizard/CippWizardAppApproval.jsx
@@ -12,20 +12,6 @@ import { CippPropertyListCard } from "../CippCards/CippPropertyListCard";
export const CippWizardAppApproval = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props;
- // Set default mode to Template
- useEffect(() => {
- // Proper initialization of the radio selection
- const currentValue = formControl.getValues("configMode");
-
- // If not set or undefined, set it to template
- if (!currentValue) {
- formControl.setValue("configMode", "template", {
- shouldDirty: false,
- shouldValidate: true,
- });
- }
- }, [formControl]);
-
// Watch for the selected template to access permissions
const selectedTemplate = useWatch({
control: formControl.control,
diff --git a/src/components/CippWizard/CippWizardConfirmation.js b/src/components/CippWizard/CippWizardConfirmation.js
index 3e8ae6c78bff..fcd622d623b6 100644
--- a/src/components/CippWizard/CippWizardConfirmation.js
+++ b/src/components/CippWizard/CippWizardConfirmation.js
@@ -20,11 +20,18 @@ export const CippWizardConfirmation = (props) => {
"addrow",
];
- const tenantEntry = formEntries.find(([key]) => key === "tenantFilter" || key === "tenant");
- const userEntry = formEntries.find(([key]) =>
+ // Filter out null values and undefined values which could be from hidden conditional fields
+ const filteredFormEntries = formEntries.filter(
+ ([_, value]) => value !== null && value !== undefined
+ );
+
+ const tenantEntry = filteredFormEntries.find(
+ ([key]) => key === "tenantFilter" || key === "tenant"
+ );
+ const userEntry = filteredFormEntries.find(([key]) =>
["user", "userPrincipalName", "username"].includes(key)
);
- const filteredEntries = formEntries.filter(
+ const filteredEntries = filteredFormEntries.filter(
([key]) =>
!blacklist.includes(key) &&
key !== "tenantFilter" &&
diff --git a/src/pages/tenant/tools/appapproval/index.js b/src/pages/tenant/tools/appapproval/index.js
index 10067259d287..52e6040e62f9 100644
--- a/src/pages/tenant/tools/appapproval/index.js
+++ b/src/pages/tenant/tools/appapproval/index.js
@@ -38,7 +38,7 @@ const Page = () => {
return (
<>
;
}
+ // handle autocomplete labels
+ if (data?.label && data?.value) {
+ return isText ? data.label : ;
+ }
+
+ // handle array of autocomplete labels
+ if (Array.isArray(data) && data.length > 0 && data[0]?.label && data[0]?.value) {
+ return isText
+ ? data.map((item) => item.label).join(", ")
+ : renderChipList(
+ data.map((item) => {
+ return {
+ label: item.label,
+ };
+ })
+ );
+ }
+
// Handle arrays of strings
if (Array.isArray(data) && data.every((item) => typeof item === "string") && flatten) {
// if string matches json format, parse it
From 1b1984f554cad093d2c75470f9fbf85762485050 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Wed, 21 May 2025 18:51:34 +0200
Subject: [PATCH 042/865] notifications form
---
.../CippComponents/CippNotificationForm.jsx | 186 ++++++++++++++++++
.../CippWizard/CippBaselinesStep.jsx | 46 ++---
.../CippWizard/CippNotificationsStep.jsx | 36 ++++
src/pages/cipp/settings/notifications.js | 162 +--------------
src/pages/onboardingv2.js | 15 +-
5 files changed, 248 insertions(+), 197 deletions(-)
create mode 100644 src/components/CippComponents/CippNotificationForm.jsx
create mode 100644 src/components/CippWizard/CippNotificationsStep.jsx
diff --git a/src/components/CippComponents/CippNotificationForm.jsx b/src/components/CippComponents/CippNotificationForm.jsx
new file mode 100644
index 000000000000..8a1c63ac897b
--- /dev/null
+++ b/src/components/CippComponents/CippNotificationForm.jsx
@@ -0,0 +1,186 @@
+import { useEffect } from "react";
+import { Button, Box, Grid } from "@mui/material";
+import CippFormComponent from "./CippFormComponent";
+import { ApiGetCall } from "../../api/ApiCall";
+import { useDialog } from "../../hooks/use-dialog";
+import { CippApiDialog } from "./CippApiDialog";
+import { useWatch } from "react-hook-form";
+
+export const CippNotificationForm = ({
+ formControl,
+ showTestButton = true,
+ hideButtons = false,
+}) => {
+ const notificationDialog = useDialog();
+
+ // API call to get notification configuration
+ const listNotificationConfig = ApiGetCall({
+ url: "/api/ListNotificationConfig",
+ queryKey: "ListNotificationConfig",
+ });
+
+ // Define log types and severity types
+ const logTypes = [
+ { label: "Updates Status", value: "Updates" },
+ { label: "All Standards", value: "Standards" },
+ { label: "Token Events", value: "TokensUpdater" },
+ { label: "Changing DNS Settings", value: "ExecDnsConfig" },
+ { label: "Adding excluded licenses", value: "ExecExcludeLicenses" },
+ { label: "Adding excluded tenants", value: "ExecExcludeTenant" },
+ { label: "Editing a user", value: "EditUser" },
+ { label: "Adding or deploying applications", value: "ChocoApp" },
+ { label: "Adding autopilot devices", value: "AddAPDevice" },
+ { label: "Editing a tenant", value: "EditTenant" },
+ { label: "Adding an MSP app", value: "AddMSPApp" },
+ { label: "Adding a user", value: "AddUser" },
+ { label: "Adding a group", value: "AddGroup" },
+ { label: "Adding a tenant", value: "NewTenant" },
+ { label: "Executing the offboard wizard", value: "ExecOffboardUser" },
+ ];
+
+ const severityTypes = [
+ { label: "Alert", value: "Alert" },
+ { label: "Error", value: "Error" },
+ { label: "Info", value: "Info" },
+ { label: "Warning", value: "Warning" },
+ { label: "Critical", value: "Critical" },
+ ];
+
+ // Load notification config data into form
+ useEffect(() => {
+ if (listNotificationConfig.isSuccess) {
+ var logsToInclude = [];
+ listNotificationConfig.data?.logsToInclude.map((log) => {
+ var logType = logTypes.find((logType) => logType.value === log);
+ if (logType) {
+ logsToInclude.push(logType);
+ }
+ });
+
+ formControl.reset({
+ ...formControl.getValues(),
+ email: listNotificationConfig.data?.email,
+ webhook: listNotificationConfig.data?.webhook,
+ logsToInclude: logsToInclude,
+ Severity: listNotificationConfig.data?.Severity.map((severity) => {
+ return severityTypes.find((severityType) => severityType.value === severity);
+ }),
+ onePerTenant: listNotificationConfig.data?.onePerTenant,
+ sendtoIntegration: listNotificationConfig.data?.sendtoIntegration,
+ includeTenantId: listNotificationConfig.data?.includeTenantId,
+ });
+ }
+ }, [listNotificationConfig.isSuccess]);
+ const values = useWatch({
+ control: formControl.control,
+ });
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showTestButton && (
+
+
+ Send Test Alert
+
+
+ )}
+
+
+
+ {/* Test Alert Dialog */}
+ {showTestButton && (
+
+ )}
+ >
+ );
+};
+
+export default CippNotificationForm;
diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx
index dafb80088ce1..33fb138ed8d3 100644
--- a/src/components/CippWizard/CippBaselinesStep.jsx
+++ b/src/components/CippWizard/CippBaselinesStep.jsx
@@ -7,22 +7,6 @@ import { ApiGetCall } from "../../api/ApiCall";
export const CippBaselinesStep = (props) => {
const { formControl, onPreviousStep, onNextStep, currentStep } = props;
- const values = formControl.getValues();
-
- // Fetch available baselines from API
- const baselinesApi = ApiGetCall({
- url: "/api/ListCommunityRepos",
- queryKey: "CommunityRepos",
- });
-
- // Create baseline options from the API response
- const baselineOptions = baselinesApi.isSuccess
- ? baselinesApi.data?.Results?.map((repo) => ({
- label: `${repo.Name} (${repo.Owner})`,
- value: repo.Id,
- description: repo.Description || "No description available",
- })) || []
- : [];
return (
@@ -87,21 +71,21 @@ export const CippBaselinesStep = (props) => {
Select baselines to download:
- {baselinesApi.isLoading ? (
- Loading available baselines...
- ) : baselinesApi.isError ? (
- Failed to load baselines. Please try again later.
- ) : (
-
- )}
+ `${option.Name} (${option.Owner})`,
+ valueField: "Id",
+ }}
+ multiple={true}
+ placeholder="Select one or more baselines"
+ />
diff --git a/src/components/CippWizard/CippNotificationsStep.jsx b/src/components/CippWizard/CippNotificationsStep.jsx
new file mode 100644
index 000000000000..dce360eba106
--- /dev/null
+++ b/src/components/CippWizard/CippNotificationsStep.jsx
@@ -0,0 +1,36 @@
+import { Alert, Stack, Typography } from "@mui/material";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippNotificationForm } from "../CippComponents/CippNotificationForm";
+
+export const CippNotificationsStep = (props) => {
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+
+ return (
+
+
+ Notification Settings
+
+ Configure your notification settings. These settings will determine how you receive alerts from CIPP.
+ You can test your configuration using the "Send Test Alert" button.
+
+
+ {/* Use the reusable notification form component */}
+
+
+
+ {/* Use the wizard step buttons for navigation */}
+
+
+ );
+};
+
+export default CippNotificationsStep;
\ No newline at end of file
diff --git a/src/pages/cipp/settings/notifications.js b/src/pages/cipp/settings/notifications.js
index b9fc1cd25aa6..a65eb064a49c 100644
--- a/src/pages/cipp/settings/notifications.js
+++ b/src/pages/cipp/settings/notifications.js
@@ -3,75 +3,18 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import tabOptions from "./tabOptions";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
import { useForm } from "react-hook-form";
-import CippFormComponent from "../../../components/CippComponents/CippFormComponent";
-import { Box, Button, Grid } from "@mui/material";
-import { ApiGetCall } from "../../../api/ApiCall";
-import { useEffect } from "react";
+import { Button } from "@mui/material";
import { useDialog } from "../../../hooks/use-dialog";
-import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog";
+import { CippNotificationForm } from "../../../components/CippComponents/CippNotificationForm";
const Page = () => {
const pageTitle = "Notification Settings";
const notificationDialog = useDialog();
- const listNotificationConfig = ApiGetCall({
- url: "/api/ListNotificationConfig",
- queryKey: "ListNotificationConfig",
- });
-
- const logTypes = [
- { label: "Updates Status", value: "Updates" },
- { label: "All Standards", value: "Standards" },
- { label: "Token Events", value: "TokensUpdater" },
- { label: "Changing DNS Settings", value: "ExecDnsConfig" },
- { label: "Adding excluded licenses", value: "ExecExcludeLicenses" },
- { label: "Adding excluded tenants", value: "ExecExcludeTenant" },
- { label: "Editing a user", value: "EditUser" },
- { label: "Adding or deploying applications", value: "ChocoApp" },
- { label: "Adding autopilot devices", value: "AddAPDevice" },
- { label: "Editing a tenant", value: "EditTenant" },
- { label: "Adding an MSP app", value: "AddMSPApp" },
- { label: "Adding a user", value: "AddUser" },
- { label: "Adding a group", value: "AddGroup" },
- { label: "Adding a tenant", value: "NewTenant" },
- { label: "Executing the offboard wizard", value: "ExecOffboardUser" },
- ];
- const severityTypes = [
- { label: "Alert", value: "Alert" },
- { label: "Error", value: "Error" },
- { label: "Info", value: "Info" },
- { label: "Warning", value: "Warning" },
- { label: "Critical", value: "Critical" },
- ];
-
const formControl = useForm({
mode: "onChange",
});
- useEffect(() => {
- if (listNotificationConfig.isSuccess) {
- var logsToInclude = [];
- listNotificationConfig.data?.logsToInclude.map((log) => {
- var logType = logTypes.find((logType) => logType.value === log);
- if (logType) {
- logsToInclude.push(logType);
- }
- });
-
- formControl.reset({
- email: listNotificationConfig.data?.email,
- webhook: listNotificationConfig.data?.webhook,
- logsToInclude: logsToInclude,
- Severity: listNotificationConfig.data?.Severity.map((severity) => {
- return severityTypes.find((severityType) => severityType.value === severity);
- }),
- onePerTenant: listNotificationConfig.data?.onePerTenant,
- sendtoIntegration: listNotificationConfig.data?.sendtoIntegration,
- includeTenantId: listNotificationConfig.data?.includeTenantId,
- });
- }
- }, [listNotificationConfig.isSuccess]);
-
return (
{
resetForm={false}
postUrl="/api/ExecNotificationConfig"
relatedQueryKeys={["ListNotificationConfig"]}
- isFetching={listNotificationConfig.isFetching}
- addedButtons={
-
- Send Test Alert
-
- }
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
);
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index afe22004faef..e152691fcc77 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -6,6 +6,7 @@ import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOption
import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx";
import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx";
import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx";
+import { CippNotificationsStep } from "../components/CippWizard/CippNotificationsStep.jsx";
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -73,24 +74,18 @@ const Page = () => {
},
{
title: "Step 5",
- description: "Integrations",
- component: CippDeploymentStep,
- //give the choice to configure integrations.
- },
- {
- title: "Step 6",
description: "Notifications",
- component: CippDeploymentStep,
- //explain notifications, test if email is setup,etc.
+ component: CippNotificationsStep,
+ //Show the notification menu (cipp/settings/notifications) without the submit/save button, but with a test button.
},
{
- title: "Step 7",
+ title: "Step 6",
description: "Alerts",
component: CippDeploymentStep,
//show template alerts, allow user to configure them.
},
{
- title: "Step 8",
+ title: "Step 7",
description: "Confirmation",
component: CippWizardConfirmation,
//confirm and finish button, perform tasks, launch checks etc.
From 592d7ed2613704e70c3d476431fba4cf332a2065 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 13:16:12 -0400
Subject: [PATCH 043/865] fix query key
---
src/pages/tenant/administration/app-consent-requests/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/tenant/administration/app-consent-requests/index.js b/src/pages/tenant/administration/app-consent-requests/index.js
index 3bc6c53b58d6..b9944ef24b24 100644
--- a/src/pages/tenant/administration/app-consent-requests/index.js
+++ b/src/pages/tenant/administration/app-consent-requests/index.js
@@ -100,7 +100,7 @@ const Page = () => {
type: "column",
},
]}
- queryKey={`AppConsentRequests-${JSON.stringify(filterParams)}`}
+ queryKey={`AppConsentRequests-${JSON.stringify(filterParams)}-${tenantFilter}`}
apiData={{
...filterParams,
}}
From a209ea0f1f2394d452ef55bff6880aa0b5b710f1 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 14:12:47 -0400
Subject: [PATCH 044/865] Update index.jsx
---
src/pages/endpoint/MEM/list-scripts/index.jsx | 22 +++++++++----------
1 file changed, 10 insertions(+), 12 deletions(-)
diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx
index eb2851b0544f..ee7ed45f4f84 100644
--- a/src/pages/endpoint/MEM/list-scripts/index.jsx
+++ b/src/pages/endpoint/MEM/list-scripts/index.jsx
@@ -18,7 +18,6 @@ import { Search, Close, Save } from "@mui/icons-material";
import { useSettings } from "../../../../hooks/use-settings";
import { Stack } from "@mui/system";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import axios from "axios";
const Page = () => {
const pageTitle = "Scripts";
@@ -94,17 +93,19 @@ const Page = () => {
scriptContent,
runAsAccount,
fileName,
- roleScopeTagIds
+ roleScopeTagIds,
+ scriptType,
} = currentScript;
const patchData = {
TenantFilter: tenantFilter,
ScriptId: id,
+ ScriptType: scriptType,
IntuneScript: JSON.stringify({
runAs32Bit,
id,
displayName,
description,
- scriptContent: scriptBytes.toString("base64"), // Convert to base64
+ scriptContent: scriptBytes.toString("base64"), // Convert to base64
runAsAccount,
fileName,
roleScopeTagIds,
@@ -118,7 +119,7 @@ const Page = () => {
});
if (!response.ok) {
- dispatch (
+ dispatch(
showToast({
title: "Script Save Error",
message: "Your Intune script could not be saved.",
@@ -126,7 +127,7 @@ const Page = () => {
})
);
}
-
+
return response.json();
},
enabled: false,
@@ -139,7 +140,7 @@ const Page = () => {
const { data } = await saveScriptRefetch();
setCodeContentChanged(false);
setCodeOpen(!codeOpen);
- dispatch (
+ dispatch(
showToast({
title: "Script Saved",
message: "Your Intune script has been saved successfully.",
@@ -228,15 +229,12 @@ const Page = () => {
)}
{isSaving && (
-
+
)}
{(scriptIsFetching || scriptIsLoading) && }
- {(!scriptIsFetching && !scriptIsLoading) && (
+ {!scriptIsFetching && !scriptIsLoading && (
{
};
Page.getLayout = (page) => {page} ;
-export default Page;
\ No newline at end of file
+export default Page;
From 6ff9a81b17cde718b5a629355f3ae32416f58916 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 15:23:23 -0400
Subject: [PATCH 045/865] layout tweaks
---
.../CippStandards/CippStandardAccordion.jsx | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx
index ef43935d4c1a..4048981e6cd5 100644
--- a/src/components/CippStandards/CippStandardAccordion.jsx
+++ b/src/components/CippStandards/CippStandardAccordion.jsx
@@ -9,7 +9,6 @@ import {
SvgIcon,
Collapse,
Divider,
- Grid,
Tooltip,
Chip,
TextField,
@@ -26,6 +25,7 @@ import {
Close,
FilterAlt,
} from "@mui/icons-material";
+import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { useWatch } from "react-hook-form";
import _ from "lodash";
@@ -69,7 +69,7 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl })
}
return (
-
+
)}
-
+
{standard.helpText}
@@ -437,9 +437,11 @@ const CippStandardAccordion = ({
{isConfigured ? "Configured" : "Unconfigured"}
- handleRemoveStandard(standardName)}>
-
-
+
+ handleRemoveStandard(standardName)}>
+
+
+
handleAccordionToggle(standardName)}>
-
+
{hasAddedComponents && (
-
+
{standard.addedComponent?.map((component, idx) =>
component?.condition ? (
From 251107d195617b29f989ef52e723a1c8d634b318 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 15:23:33 -0400
Subject: [PATCH 046/865] fix error with null query key
---
src/pages/_app.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/pages/_app.js b/src/pages/_app.js
index f8a805f3180d..3e6ce66b8fef 100644
--- a/src/pages/_app.js
+++ b/src/pages/_app.js
@@ -66,8 +66,13 @@ const App = (props) => {
const queryIsReadyForPersistance = query.state.status === "success";
if (queryIsReadyForPersistance) {
const { queryKey } = query;
+ // Check if queryKey exists and has elements before accessing index 0
+ if (!queryKey || !queryKey.length) {
+ return false;
+ }
+ const queryKeyString = String(queryKey[0] || '');
const excludeFromPersisting = excludeQueryKeys.some((key) =>
- queryKey[0].toString().includes(key)
+ queryKeyString.includes(key)
);
return !excludeFromPersisting;
}
From 046d873448d7e90605128eb538e845bfa88cc282 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 15:24:13 -0400
Subject: [PATCH 047/865] alert config fixes
layout tweaks
fix conditions
fix excluded tenants
---
.../CippComponents/CippFormComponent.jsx | 2 +
.../CippComponents/CippFormCondition.jsx | 55 ++++++---
.../alert-configuration/alert.jsx | 109 ++++++++++--------
3 files changed, 106 insertions(+), 60 deletions(-)
diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx
index 30f97a302684..ac2e1c13eee1 100644
--- a/src/components/CippComponents/CippFormComponent.jsx
+++ b/src/components/CippComponents/CippFormComponent.jsx
@@ -27,7 +27,9 @@ import { CippDataTable } from "../CippTable/CippDataTable";
import React from "react";
// Helper function to convert bracket notation to dot notation
+// Improved to correctly handle nested bracket notations
const convertBracketsToDots = (name) => {
+ if (!name) return "";
return name.replace(/\[(\d+)\]/g, ".$1"); // Replace [0] with .0
};
diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx
index 962b335f14de..fc121753b9ab 100644
--- a/src/components/CippComponents/CippFormCondition.jsx
+++ b/src/components/CippComponents/CippFormCondition.jsx
@@ -30,26 +30,35 @@ export const CippFormCondition = (props) => {
return null;
}
+ // Convert bracket notation to dot notation for array fields if needed
+ const normalizedField = field.replace(/\[(\d+)\]/g, ".$1");
+
// Watch the form field value
const watcher = useWatch({
control: formControl.control,
- name: field,
+ name: normalizedField,
});
- // Safer property access
+ // Safer property access with get for nested paths
let watchedValue = watcher;
let compareTargetValue = compareValue;
if (propertyName && propertyName !== "value") {
- if (propertyName.includes(".")) {
- watchedValue = get(watcher, propertyName);
- compareTargetValue = get(compareValue, propertyName);
- } else {
- watchedValue = watcher?.[propertyName];
- compareTargetValue = compareValue?.[propertyName];
- }
+ watchedValue = get(watcher, propertyName);
+ compareTargetValue = get(compareValue, propertyName);
}
+ /*console.log("CippFormCondition: ", {
+ watcher,
+ watchedValue,
+ compareTargetValue,
+ compareType,
+ compareValue,
+ action,
+ field,
+ propertyName,
+ });*/
+
// Function to recursively extract field names from child components
const extractFieldNames = (children) => {
const fieldNames = [];
@@ -75,29 +84,47 @@ export const CippFormCondition = (props) => {
const isConditionMet = () => {
switch (compareType) {
case "regex":
- return watcher?.match(new RegExp(compareValue));
+ return watcher?.match?.(new RegExp(compareValue));
case "is":
return isEqual(watchedValue, compareTargetValue);
case "isNot":
return !isEqual(watchedValue, compareTargetValue);
case "contains":
if (Array.isArray(watcher)) {
- return watcher.includes(compareValue);
+ return watcher.some((item) => isEqual(item, compareValue));
} else if (typeof watcher === "string") {
return watcher.includes(compareValue);
} else if (typeof watcher === "object" && watcher !== null) {
- return compareValue in watcher;
+ // Handle checking if object contains value or key
+ if (typeof compareValue === "string") {
+ // Check for "value" property containing the string
+ if (watcher.value && typeof watcher.value === "string") {
+ return watcher.value.includes(compareValue);
+ }
+ // Check for "label" property containing the string
+ if (watcher.label && typeof watcher.label === "string") {
+ return watcher.label.includes(compareValue);
+ }
+ // Check if object has the compareValue as a key
+ return compareValue in watcher;
+ } else {
+ return Object.values(watcher).some((val) => isEqual(val, compareValue));
+ }
}
return false;
case "doesNotContain":
if (watcher === undefined || watcher === null) {
return true;
} else if (Array.isArray(watcher)) {
- return !watcher.includes(compareValue);
+ return !watcher.some((item) => isEqual(item, compareValue));
} else if (typeof watcher === "string") {
return !watcher.includes(compareValue);
} else if (typeof watcher === "object") {
- return !(compareValue in watcher);
+ if (typeof compareValue === "string") {
+ return !(compareValue in watcher);
+ } else {
+ return !Object.values(watcher).some((val) => isEqual(val, compareValue));
+ }
}
return true;
case "greaterThan":
diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx
index 6cff52e95ff7..621dce6a7a95 100644
--- a/src/pages/tenant/administration/alert-configuration/alert.jsx
+++ b/src/pages/tenant/administration/alert-configuration/alert.jsx
@@ -1,11 +1,10 @@
-import React, { useState, useEffect, use } from "react";
+import React, { useState, useEffect } from "react";
import {
Box,
Button,
Container,
Stack,
Typography,
- Grid,
Card,
CardActionArea,
CardContent,
@@ -13,7 +12,9 @@ import {
IconButton,
Skeleton,
Divider,
+ Tooltip,
} from "@mui/material";
+import { Grid } from "@mui/system";
import { ArrowLeftIcon } from "@mui/x-date-pickers";
import { useRouter } from "next/router";
import { useForm, useFormState, useWatch } from "react-hook-form";
@@ -32,7 +33,7 @@ import { CippFormCondition } from "../../../../components/CippComponents/CippFor
const AlertWizard = () => {
const apiRequest = ApiPostCall({
- relatedQueryKeys: "ListAlertsQueue",
+ relatedQueryKeys: ["ListAlertsQueue", "ListCurrentAlerts"],
});
const router = useRouter();
const [editAlert, setAlertEdit] = useState(false);
@@ -45,6 +46,7 @@ const AlertWizard = () => {
const existingAlert = ApiGetCall({
url: "/api/ListAlertsQueue",
relatedQueryKeys: "ListAlertsQueue",
+ queryKey: "ListCurrentAlerts",
});
const [recurrenceOptions, setRecurrenceOptions] = useState([
{ value: "30m", label: "Every 30 minutes" },
@@ -152,7 +154,7 @@ const AlertWizard = () => {
formControl.reset({
RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined,
tenantFilter: alert.RawAlert.Tenants,
- excludedTenants: alert.RawAlert.excludedTenants,
+ excludedTenants: alert.excludedTenants,
Actions: alert.RawAlert.Actions,
conditions: alert.RawAlert.Conditions,
logbook: foundLogbook,
@@ -306,7 +308,7 @@ const AlertWizard = () => {
-
+
setAlertType("audit")}>
@@ -318,7 +320,7 @@ const AlertWizard = () => {
-
+
setAlertType("script")}>
@@ -333,14 +335,19 @@ const AlertWizard = () => {
{/* Audit Log Form */}
{alertType === "audit" && (
-
-
+
+
- }
- variant="outlined"
- severity="info"
- >
-
- Loading...
-
-
-
- )}
+
+
+ setFetchingVisible(false)}
+ >
+
+
+ }
+ variant="outlined"
+ severity="info"
+ >
+
+ Loading...
+
+
+
{/* Error alert */}
diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx
index d11ba05bc1ba..abee9306ed85 100644
--- a/src/pages/identity/administration/users/user/conditional-access.jsx
+++ b/src/pages/identity/administration/users/user/conditional-access.jsx
@@ -1,263 +1,263 @@
-import { useState } from "react";
-import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import { useSettings } from "/src/hooks/use-settings";
-import { useRouter } from "next/router";
-import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton";
-import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon";
-import { Mail, Forward, Fingerprint, Launch } from "@mui/icons-material";
-import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout";
-import tabOptions from "./tabOptions";
-import ReactTimeAgo from "react-time-ago";
-import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard";
-import { Box, Stack, Typography, Button, CircularProgress } from "@mui/material";
-import Grid from "@mui/material/Grid";
-import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
-import countryList from "/src/data/countryList";
-import { CippDataTable } from "/src/components/CippTable/CippDataTable";
-import { useForm } from "react-hook-form";
-import CippButtonCard from "../../../../../components/CippCards/CippButtonCard";
-import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
-import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults";
-
-const Page = () => {
- const userSettingsDefaults = useSettings();
- const router = useRouter();
- const { userId } = router.query;
-
- const tenant = userSettingsDefaults.currentTenant;
- const [formParams, setFormParams] = useState(false);
-
- const userRequest = ApiGetCall({
- url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`,
- queryKey: `ListUsers-${userId}`,
- });
-
- // Set the title and subtitle for the layout
- const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading...";
-
- const subtitle = userRequest.isSuccess
- ? [
- {
- icon: ,
- text: ,
- },
- {
- icon: ,
- text: ,
- },
- {
- icon: ,
- text: (
- <>
- Created:
- >
- ),
- },
- {
- icon: ,
- text: (
-
- View in Entra
-
- ),
- },
- ]
- : [];
-
- // Initialize React Hook Form
- const formControl = useForm();
-
- const postRequest = ApiPostCall({
- url: "/api/ExecCACheck",
- relatedQueryKeys: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`,
- });
- const onSubmit = (data) => {
- //add userId and tenantFilter to the object
- data.userId = {};
- data.userId["value"] = userId;
- data.tenantFilter = tenant;
- setFormParams(data);
- postRequest.mutate({
- url: "/api/ExecCACheck",
- data: data,
- queryKey: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`,
- });
- };
-
- return (
-
- {userRequest.isLoading && }
- {userRequest.isSuccess && (
-
-
- {/* Form Section */}
-
-
- Test policies
-
- }
- >
- {/* Form Starts Here */}
-
-
- Test your conditional access policies before putting them in production. The
- returned results will show you if the user is allowed or denied access based on
- the policy.
-
-
-
- {/* Mandatory Parameters */}
- Mandatory Parameters:
- `${option.displayName}`,
- valueField: "id",
- queryKey: `ServicePrincipals-${tenant}`,
- data: {
- Endpoint: "ServicePrincipals",
- manualPagination: true,
- $select: "id,displayName",
- $count: true,
- $orderby: "displayName",
- $top: 999,
- },
- }}
- formControl={formControl}
- />
-
- {/* Optional Parameters */}
- Optional Parameters:
-
- {/* Test from this country */}
- ({
- value: Code,
- label: Name,
- }))}
- formControl={formControl}
- />
-
- {/* Test from this IP */}
-
-
- {/* Device Platform */}
-
-
- {/* Client Application Type */}
-
-
- {/* Sign-in risk level */}
-
-
- {/* User risk level */}
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- );
-};
-
-Page.getLayout = (page) => {page} ;
-
-export default Page;
+import { useState } from "react";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { useSettings } from "/src/hooks/use-settings";
+import { useRouter } from "next/router";
+import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton";
+import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon";
+import { Mail, Forward, Fingerprint, Launch } from "@mui/icons-material";
+import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout";
+import tabOptions from "./tabOptions";
+import ReactTimeAgo from "react-time-ago";
+import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard";
+import { Box, Stack, Typography, Button, CircularProgress } from "@mui/material";
+import Grid from "@mui/material/Grid";
+import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
+import countryList from "/src/data/countryList";
+import { CippDataTable } from "/src/components/CippTable/CippDataTable";
+import { useForm } from "react-hook-form";
+import CippButtonCard from "../../../../../components/CippCards/CippButtonCard";
+import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
+import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults";
+
+const Page = () => {
+ const userSettingsDefaults = useSettings();
+ const router = useRouter();
+ const { userId } = router.query;
+
+ const tenant = userSettingsDefaults.currentTenant;
+ const [formParams, setFormParams] = useState(false);
+
+ const userRequest = ApiGetCall({
+ url: `/api/ListUsers?UserId=${userId}&tenantFilter=${tenant}`,
+ queryKey: `ListUsers-${userId}`,
+ });
+
+ // Set the title and subtitle for the layout
+ const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading...";
+
+ const subtitle = userRequest.isSuccess
+ ? [
+ {
+ icon: ,
+ text: ,
+ },
+ {
+ icon: ,
+ text: ,
+ },
+ {
+ icon: ,
+ text: (
+ <>
+ Created:
+ >
+ ),
+ },
+ {
+ icon: ,
+ text: (
+
+ View in Entra
+
+ ),
+ },
+ ]
+ : [];
+
+ // Initialize React Hook Form
+ const formControl = useForm();
+
+ const postRequest = ApiPostCall({
+ url: "/api/ExecCACheck",
+ relatedQueryKeys: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`,
+ });
+ const onSubmit = (data) => {
+ //add userId and tenantFilter to the object
+ data.userId = {};
+ data.userId["value"] = userId;
+ data.tenantFilter = tenant;
+ setFormParams(data);
+ postRequest.mutate({
+ url: "/api/ExecCACheck",
+ data: data,
+ queryKey: `ExecCACheck-${tenant}-${userId}-${JSON.stringify(formParams)}`,
+ });
+ };
+
+ return (
+
+ {userRequest.isLoading && }
+ {userRequest.isSuccess && (
+
+
+ {/* Form Section */}
+
+
+ Test policies
+
+ }
+ >
+ {/* Form Starts Here */}
+
+
+ Test your conditional access policies before putting them in production. The
+ returned results will show you if the user is allowed or denied access based on
+ the policy.
+
+
+
+ {/* Mandatory Parameters */}
+ Mandatory Parameters:
+ `${option.displayName}`,
+ valueField: "id",
+ queryKey: `ServicePrincipals-${tenant}`,
+ data: {
+ Endpoint: "ServicePrincipals",
+ manualPagination: true,
+ $select: "id,displayName",
+ $count: true,
+ $orderby: "displayName",
+ $top: 999,
+ },
+ }}
+ formControl={formControl}
+ />
+
+ {/* Optional Parameters */}
+ Optional Parameters:
+
+ {/* Test from this country */}
+ ({
+ value: Code,
+ label: Name,
+ }))}
+ formControl={formControl}
+ />
+
+ {/* Test from this IP */}
+
+
+ {/* Device Platform */}
+
+
+ {/* Client Application Type */}
+
+
+ {/* Sign-in risk level */}
+
+
+ {/* User risk level */}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+Page.getLayout = (page) => {page} ;
+
+export default Page;
From e2ae072cb08b16c672a6d672baa029f6f3ce719c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Wed, 21 May 2025 23:45:55 +0200
Subject: [PATCH 051/865] feat: add allTenants support for transport rules page
---
src/pages/email/transport/list-rules/index.js | 13 +++++++++++--
.../security/incidents/list-incidents/index.js | 2 +-
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/pages/email/transport/list-rules/index.js b/src/pages/email/transport/list-rules/index.js
index 0c6128024c6e..5faec982d216 100644
--- a/src/pages/email/transport/list-rules/index.js
+++ b/src/pages/email/transport/list-rules/index.js
@@ -65,12 +65,21 @@ const Page = () => {
actions: actions,
};
- const simpleColumns = ["Name", "State", "Mode", "RuleErrorAction", "WhenChanged", "Comments"];
+ const simpleColumns = [
+ "Name",
+ "State",
+ "Mode",
+ "RuleErrorAction",
+ "WhenChanged",
+ "Comments",
+ "Tenant",
+ ];
return (
{
);
};
-Page.getLayout = (page) => {page} ;
+Page.getLayout = (page) => {page} ;
export default Page;
diff --git a/src/pages/security/incidents/list-incidents/index.js b/src/pages/security/incidents/list-incidents/index.js
index 8db8ce596132..354a86355a8f 100644
--- a/src/pages/security/incidents/list-incidents/index.js
+++ b/src/pages/security/incidents/list-incidents/index.js
@@ -99,6 +99,6 @@ const Page = () => {
);
};
-Page.getLayout = (page) => {page} ;
+Page.getLayout = (page) => {page} ;
export default Page;
From 6123d220c8ac89ea87ac2544930670fac528ad87 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 17:56:00 -0400
Subject: [PATCH 052/865] edit group template support
---
.../CippComponents/CippFormComponent.jsx | 32 +--
.../CippAddGroupTemplateForm.jsx | 203 ++++++++++--------
.../administration/group-templates/edit.jsx | 106 +++++++++
.../administration/group-templates/index.js | 37 +++-
4 files changed, 267 insertions(+), 111 deletions(-)
create mode 100644 src/pages/identity/administration/group-templates/edit.jsx
diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx
index ac2e1c13eee1..b29f6692b8ef 100644
--- a/src/components/CippComponents/CippFormComponent.jsx
+++ b/src/components/CippComponents/CippFormComponent.jsx
@@ -223,20 +223,26 @@ export const CippFormComponent = (props) => {
(
-
- {props.options.map((option, idx) => (
- }
- label={option.label}
- />
- ))}
-
- )}
+ render={({ field }) => {
+ return (
+ field.onChange(e.target.value)}
+ {...other}
+ >
+ {props.options.map((option, idx) => (
+ }
+ label={option.label}
+ />
+ ))}
+
+ );
+ }}
/>
diff --git a/src/components/CippFormPages/CippAddGroupTemplateForm.jsx b/src/components/CippFormPages/CippAddGroupTemplateForm.jsx
index a9362038e9d0..c8e1b4f7af44 100644
--- a/src/components/CippFormPages/CippAddGroupTemplateForm.jsx
+++ b/src/components/CippFormPages/CippAddGroupTemplateForm.jsx
@@ -1,95 +1,108 @@
-import React from "react";
-import { Grid } from "@mui/material";
-import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
-import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition";
-
-const CippAddGroupTemplateForm = (props) => {
- const { formControl } = props;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default CippAddGroupTemplateForm;
+import React, { useEffect } from "react";
+import { Grid } from "@mui/material";
+import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
+import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition";
+
+const CippAddGroupTemplateForm = (props) => {
+ const { formControl } = props;
+
+ // Debug the current form values, especially groupType
+ useEffect(() => {
+ const subscription = formControl.watch((value, { name, type }) => {
+ console.log("Form value changed:", name, value);
+ });
+ return () => subscription.unsubscribe();
+ }, [formControl]);
+
+ return (
+
+ {/* Hidden field to store the template GUID when editing */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Debug output */}
+ Current groupType: {formControl.watch("groupType")}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CippAddGroupTemplateForm;
diff --git a/src/pages/identity/administration/group-templates/edit.jsx b/src/pages/identity/administration/group-templates/edit.jsx
new file mode 100644
index 000000000000..6f7b74c11cdf
--- /dev/null
+++ b/src/pages/identity/administration/group-templates/edit.jsx
@@ -0,0 +1,106 @@
+import { Box, CircularProgress } 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 CippAddGroupTemplateForm from "../../../../components/CippFormPages/CippAddGroupTemplateForm";
+import { useRouter } from "next/router";
+import { ApiGetCall } from "../../../../api/ApiCall";
+import { useEffect } from "react";
+
+const Page = () => {
+ const userSettingsDefaults = useSettings();
+ const router = useRouter();
+ const { id } = router.query;
+
+ const formControl = useForm({
+ mode: "onChange",
+ defaultValues: {
+ tenantFilter: userSettingsDefaults.currentTenant,
+ },
+ });
+
+ // Fetch template data
+ const { data: template, isFetching } = ApiGetCall({
+ url: `/api/ListGroupTemplates?id=${id}`,
+ queryKey: `GroupTemplate-${id}`,
+ waiting: !!id,
+ });
+
+ // Map groupType values to valid radio options
+ const mapGroupType = (type) => {
+ // Map of group types to the corresponding option value
+ const groupTypeMap = {
+ // Standard mappings
+ azurerole: "azurerole",
+ generic: "generic",
+ m365: "m365",
+ dynamic: "dynamic",
+ dynamicdistribution: "dynamicdistribution",
+ distribution: "distribution",
+ security: "security",
+
+ // Additional mappings from possible backend values
+ Unified: "m365",
+ Security: "generic",
+ Distribution: "distribution",
+ "Mail-enabled security": "security",
+ "Mail Enabled Security": "security",
+ "Azure Role Group": "azurerole",
+ "Azure Active Directory Role Group": "azurerole",
+ "Security Group": "generic",
+ "Microsoft 365 Group": "m365",
+ "Microsoft 365 (Unified)": "m365",
+ "Dynamic Group": "dynamic",
+ DynamicMembership: "dynamic",
+ "Dynamic Distribution Group": "dynamicdistribution",
+ DynamicDistribution: "dynamicdistribution",
+ "Distribution List": "distribution",
+ };
+
+ // Return just the value for the radio group, not the label/value pair
+ return groupTypeMap[type] || "generic"; // Default to generic if no mapping exists
+ };
+
+ // Set form values when template data is loaded
+ useEffect(() => {
+ if (template) {
+ const templateData = template[0];
+
+ // Make sure we have the necessary data before proceeding
+ if (templateData) {
+ formControl.reset({
+ ...templateData,
+ groupType: mapGroupType(templateData.groupType),
+ tenantFilter: userSettingsDefaults.currentTenant,
+ });
+ }
+ }
+ }, [template, formControl, userSettingsDefaults.currentTenant]);
+
+ return (
+ <>
+
+ {/* Add debugging output to check what values are set */}
+ {JSON.stringify(formControl.watch(), null, 2)}
+
+
+
+
+
+ >
+ );
+};
+
+Page.getLayout = (page) => {page} ;
+
+export default Page;
diff --git a/src/pages/identity/administration/group-templates/index.js b/src/pages/identity/administration/group-templates/index.js
index 19cdc80add8f..5d0d793d413c 100644
--- a/src/pages/identity/administration/group-templates/index.js
+++ b/src/pages/identity/administration/group-templates/index.js
@@ -1,10 +1,13 @@
import { Button } from "@mui/material";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import { AddBox, RocketLaunch, Delete, GitHub } from "@mui/icons-material";
+import { AddBox, RocketLaunch, Delete, GitHub, Edit } from "@mui/icons-material";
import Link from "next/link";
import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock";
import { ApiGetCall } from "/src/api/ApiCall";
+import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard";
+import { getCippTranslation } from "../../../../utils/get-cipp-translation";
+import { getCippFormatting } from "../../../../utils/get-cipp-formatting";
const Page = () => {
const pageTitle = "Group Templates";
@@ -15,6 +18,11 @@ const Page = () => {
refetchOnReconnect: false,
});
const actions = [
+ {
+ label: "Edit Template",
+ icon: ,
+ link: "/identity/administration/group-templates/edit?id=[GUID]",
+ },
{
label: "Save to GitHub",
type: "POST",
@@ -73,13 +81,36 @@ const Page = () => {
];
const offCanvas = {
- children: (row) => ,
+ children: (data) => {
+ const keys = Object.keys(data).filter(
+ (key) => !key.includes("@odata") && !key.includes("@data")
+ );
+ const properties = [];
+ keys.forEach((key) => {
+ if (data[key] && data[key].length > 0) {
+ properties.push({
+ label: getCippTranslation(key),
+ value: getCippFormatting(data[key], key),
+ });
+ }
+ });
+ return (
+
+ );
+ },
};
return (
@@ -92,7 +123,7 @@ const Page = () => {
>
}
offCanvas={offCanvas}
- simpleColumns={["Displayname", "Description", "groupType", "GUID"]}
+ simpleColumns={["displayName", "description", "groupType", "GUID"]}
/>
);
};
From 28e42b7e40b9f536c7313db1a92923e9f69b7ac7 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 17:57:51 -0400
Subject: [PATCH 053/865] Update index.js
---
src/pages/identity/administration/group-templates/index.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/identity/administration/group-templates/index.js b/src/pages/identity/administration/group-templates/index.js
index 5d0d793d413c..6fe4acd3230a 100644
--- a/src/pages/identity/administration/group-templates/index.js
+++ b/src/pages/identity/administration/group-templates/index.js
@@ -111,6 +111,7 @@ const Page = () => {
title={pageTitle}
apiUrl="/api/ListGroupTemplates"
queryKey="GroupTemplatesList"
+ tenantInTitle={false}
actions={actions}
cardButton={
<>
From 01fe1b9bffc0769dc9be83d8fe1c2dd2e77c7630 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 21:50:28 -0400
Subject: [PATCH 054/865] JIT admin form validation
---
.../CippComponents/CippFormDomainSelector.jsx | 81 ++--
src/components/CippFormPages/CippFormPage.jsx | 6 +-
.../identity/administration/jit-admin/add.jsx | 420 ++++++++++--------
3 files changed, 283 insertions(+), 224 deletions(-)
diff --git a/src/components/CippComponents/CippFormDomainSelector.jsx b/src/components/CippComponents/CippFormDomainSelector.jsx
index f22a5f74e884..db0b1af339ff 100644
--- a/src/components/CippComponents/CippFormDomainSelector.jsx
+++ b/src/components/CippComponents/CippFormDomainSelector.jsx
@@ -1,40 +1,41 @@
-import React from "react";
-import { CippFormComponent } from "./CippFormComponent";
-import { useWatch } from "react-hook-form";
-import { useSettings } from "../../hooks/use-settings";
-
-export const CippFormDomainSelector = ({
- formControl,
- name,
- label,
- allTenants = false,
- type = "multiple",
- ...other
-}) => {
- const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" });
- const selectedTenant = useSettings().currentTenant;
- return (
- `${option.id}`,
- valueField: "id",
- data: {
- Endpoint: "domains",
- manualPagination: true,
- $count: true,
- $top: 99,
- },
- }}
- />
- );
-};
+import React from "react";
+import { CippFormComponent } from "./CippFormComponent";
+import { useWatch } from "react-hook-form";
+import { useSettings } from "../../hooks/use-settings";
+
+export const CippFormDomainSelector = ({
+ formControl,
+ name,
+ label,
+ allTenants = false,
+ type = "multiple",
+ ...other
+}) => {
+ const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" });
+ const selectedTenant = useSettings().currentTenant;
+ return (
+ `${option.id}`,
+ valueField: "id",
+ data: {
+ Endpoint: "domains",
+ manualPagination: true,
+ $count: true,
+ $top: 99,
+ },
+ }}
+ {...other}
+ />
+ );
+};
diff --git a/src/components/CippFormPages/CippFormPage.jsx b/src/components/CippFormPages/CippFormPage.jsx
index d22877dfb139..448eb0604498 100644
--- a/src/components/CippFormPages/CippFormPage.jsx
+++ b/src/components/CippFormPages/CippFormPage.jsx
@@ -11,7 +11,6 @@ import {
CardActions,
} from "@mui/material";
import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
-import Head from "next/head";
import { ApiPostCall } from "../../api/ApiCall";
import { CippApiResults } from "../CippComponents/CippApiResults";
import { useEffect } from "react";
@@ -71,6 +70,11 @@ const CippFormPage = (props) => {
}, [postCall.isSuccess]);
const handleSubmit = () => {
+ formControl.trigger();
+ // Check if the form is valid before proceeding
+ if (!isValid) {
+ return;
+ }
const values = customDataformatter
? customDataformatter(formControl.getValues())
: formControl.getValues();
diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx
index 0efe13f00e24..3a5a1a232852 100644
--- a/src/pages/identity/administration/jit-admin/add.jsx
+++ b/src/pages/identity/administration/jit-admin/add.jsx
@@ -1,183 +1,237 @@
-import { Box, Divider, Grid } from "@mui/material";
-import CippFormPage from "../../../../components/CippFormPages/CippFormPage";
-import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector";
-import { useForm } from "react-hook-form";
-import CippFormComponent from "../../../../components/CippComponents/CippFormComponent";
-import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition";
-import gdaproles from "/src/data/GDAPRoles.json";
-import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector";
-import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector";
-const Page = () => {
- const formControl = useForm({ Mode: "onChange" });
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ({ label: role.Name, value: role.ObjectId }))}
- formControl={formControl}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-Page.getLayout = (page) => {page} ;
-
-export default Page;
+import { Box, Divider, Grid } from "@mui/material";
+import CippFormPage from "../../../../components/CippFormPages/CippFormPage";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector";
+import { useForm } from "react-hook-form";
+import CippFormComponent from "../../../../components/CippComponents/CippFormComponent";
+import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition";
+import gdaproles from "/src/data/GDAPRoles.json";
+import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector";
+import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector";
+const Page = () => {
+ const formControl = useForm({ Mode: "onChange" });
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (!option?.value) {
+ return "Domain is required";
+ }
+ return true;
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (!value) {
+ return "Start date is required";
+ }
+ return true;
+ },
+ }}
+ />
+
+
+ {
+ const startDate = formControl.getValues("startDate");
+ if (!value) {
+ return "End date is required";
+ }
+ if (new Date(value) < new Date(startDate)) {
+ return "End date must be after start date";
+ }
+ return true;
+ },
+ }}
+ />
+
+
+ ({ label: role.Name, value: role.ObjectId }))}
+ formControl={formControl}
+ required={true}
+ validators={{
+ validate: (options) => {
+ if (!options?.length) {
+ return "At least one role is required";
+ }
+ return true;
+ },
+ }}
+ />
+
+
+
+
+
+ {
+ if (!option?.value) {
+ return "Expiration action is required";
+ }
+ return true;
+ },
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+Page.getLayout = (page) => {page} ;
+
+export default Page;
From 9dfecb98ddd444ec40284867b8f77e5468796ed6 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 21 May 2025 22:01:51 -0400
Subject: [PATCH 055/865] fix console errors
---
src/components/CippComponents/CippFormComponent.jsx | 7 +++++--
src/components/CippTable/CIPPTableToptoolbar.js | 5 ++---
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx
index b29f6692b8ef..95d917654483 100644
--- a/src/components/CippComponents/CippFormComponent.jsx
+++ b/src/components/CippComponents/CippFormComponent.jsx
@@ -46,6 +46,7 @@ export const CippFormComponent = (props) => {
label,
labelLocation = "behind", // Default location for switches
defaultValue,
+ helperText,
...other
} = props;
const { errors } = useFormState({ control: formControl.control });
@@ -194,9 +195,9 @@ export const CippFormComponent = (props) => {
{get(errors, convertedName, {})?.message}
- {other.helperText && (
+ {helperText && (
- {other.helperText}
+ {helperText}
)}
>
@@ -268,6 +269,7 @@ export const CippFormComponent = (props) => {
label={label}
multiple={false}
onChange={(value) => field.onChange(value?.value)}
+ helperText={helperText}
/>
)}
/>
@@ -294,6 +296,7 @@ export const CippFormComponent = (props) => {
defaultValue={field.value}
label={label}
onChange={(value) => field.onChange(value)}
+ helperText={helperText}
/>
)}
/>
diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js
index 5245c72e5a3a..caad18cd9a7c 100644
--- a/src/components/CippTable/CIPPTableToptoolbar.js
+++ b/src/components/CippTable/CIPPTableToptoolbar.js
@@ -365,6 +365,7 @@ export const CIPPTableToptoolbar = ({
{api?.url === "/api/ListGraphRequest" && (
{
filterPopover.handleClose();
setFilterCanvasVisible(true);
@@ -448,9 +449,7 @@ export const CIPPTableToptoolbar = ({
- {mdDown && (
-
- )}
+ {mdDown && }
>
{
//add a little icon with how many rows are selected
From b7b5f3b1bf84af5ec150888663db00e23a7490c5 Mon Sep 17 00:00:00 2001
From: Zac Richards <107489668+Zacgoose@users.noreply.github.com>
Date: Thu, 22 May 2025 12:10:41 +0800
Subject: [PATCH 056/865] hide bulk requests correctly
---
.../CippTable/CIPPTableToptoolbar.js | 62 ++++++++++---------
1 file changed, 32 insertions(+), 30 deletions(-)
diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js
index caad18cd9a7c..bbbc942a0853 100644
--- a/src/components/CippTable/CIPPTableToptoolbar.js
+++ b/src/components/CippTable/CIPPTableToptoolbar.js
@@ -74,6 +74,10 @@ export const CIPPTableToptoolbar = ({
const handleActionMenuOpen = (event) => setActionMenuAnchor(event.currentTarget);
const handleActionMenuClose = () => setActionMenuAnchor(null);
+ const getBulkActions = (actions) => {
+ return actions?.filter((action) => !action.link && !action?.hideBulk) || [];
+ };
+
useEffect(() => {
//if usedData changes, deselect all rows
table.toggleAllRowsSelected(false);
@@ -486,7 +490,7 @@ export const CIPPTableToptoolbar = ({
)}
- {actions && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && (
+ {actions && getBulkActions(actions).length > 0 && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && (
<>
- {actions
- ?.filter((action) => !action.link && !action?.hideBulk)
- .map((action, index) => (
- {
- setActionData({
- data: table.getSelectedRowModel().rows.map((row) => row.original),
- action: action,
- ready: true,
- });
+ {getBulkActions(actions).map((action, index) => (
+ {
+ setActionData({
+ data: table.getSelectedRowModel().rows.map((row) => row.original),
+ action: action,
+ ready: true,
+ });
- if (action?.noConfirm && action.customFunction) {
- table
- .getSelectedRowModel()
- .rows.map((row) =>
- action.customFunction(row.original.original, action, {})
- );
- } else {
- createDialog.handleOpen();
- popover.handleClose();
- }
- }}
- >
-
- {action.icon}
-
- {action.label}
-
- ))}
+ if (action?.noConfirm && action.customFunction) {
+ table
+ .getSelectedRowModel()
+ .rows.map((row) =>
+ action.customFunction(row.original.original, action, {})
+ );
+ } else {
+ createDialog.handleOpen();
+ popover.handleClose();
+ }
+ }}
+ >
+
+ {action.icon}
+
+ {action.label}
+
+ ))}
>
)}
From 358daa7fc6e358c5ec60337c58c718db5aa69267 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Thu, 22 May 2025 11:40:15 -0400
Subject: [PATCH 057/865] add form validation
---
.../conditional/deploy-vacation/add.jsx | 34 +++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx
index dda5279515f2..20479ba9adf9 100644
--- a/src/pages/tenant/conditional/deploy-vacation/add.jsx
+++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx
@@ -57,6 +57,7 @@ const Page = () => {
name="UserId"
multiple={false}
validators={{ required: "Picking a user is required" }}
+ required={true}
/>
@@ -74,7 +75,15 @@ const Page = () => {
}}
multiple={false}
formControl={formControl}
- validators={{ required: "Picking a policy is required" }}
+ validators={{
+ validate: (option) => {
+ if (!option?.value) {
+ return "Picking a policy is required";
+ }
+ return true;
+ },
+ }}
+ required={true}
/>
@@ -86,6 +95,15 @@ const Page = () => {
name="startDate"
dateTimeType="dateTime"
formControl={formControl}
+ required={true}
+ validators={{
+ validate: (value) => {
+ if (!value) {
+ return "Start date is required";
+ }
+ return true;
+ },
+ }}
/>
@@ -97,7 +115,19 @@ const Page = () => {
name="endDate"
dateTimeType="dateTime"
formControl={formControl}
- validators={{ required: "Picking an end date is required" }}
+ required={true}
+ validators={{
+ validate: (value) => {
+ const startDate = formControl.getValues("startDate");
+ if (!value) {
+ return "End date is required";
+ }
+ if (startDate && value && new Date(value * 1000) < new Date(startDate * 1000)) {
+ return "End date must be after start date";
+ }
+ return true;
+ },
+ }}
/>
From a597ce05f708cbcfac334e10005d1b9f25d78c93 Mon Sep 17 00:00:00 2001
From: lsmith090 <47199231+lsmith090@users.noreply.github.com>
Date: Thu, 22 May 2025 12:09:11 -0400
Subject: [PATCH 058/865] Update _app.js
---
src/pages/_app.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/pages/_app.js b/src/pages/_app.js
index 3e6ce66b8fef..5b544487c04d 100644
--- a/src/pages/_app.js
+++ b/src/pages/_app.js
@@ -63,8 +63,8 @@ const App = (props) => {
buster: "v1",
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
- const queryIsReadyForPersistance = query.state.status === "success";
- if (queryIsReadyForPersistance) {
+ const queryIsReadyForPersistence = query.state.status === "success";
+ if (queryIsReadyForPersistence) {
const { queryKey } = query;
// Check if queryKey exists and has elements before accessing index 0
if (!queryKey || !queryKey.length) {
@@ -76,7 +76,7 @@ const App = (props) => {
);
return !excludeFromPersisting;
}
- return queryIsReadyForPersistance;
+ return queryIsReadyForPersistence;
},
},
});
From 5b16c68c864de45833f8c67ef5e69f47ba347af6 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Thu, 22 May 2025 13:40:24 -0400
Subject: [PATCH 059/865] set default to contains
implements #4130
---
src/components/CippTable/CippDataTable.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js
index 6c2269f0f6c1..e4e897e2664c 100644
--- a/src/components/CippTable/CippDataTable.js
+++ b/src/components/CippTable/CippDataTable.js
@@ -376,6 +376,7 @@ export const CippDataTable = (props) => {
}
},
},
+ globalFilterFn: "contains",
enableGlobalFilterModes: true,
renderGlobalFilterModeMenuItems: ({ internalFilterOptions, onSelectFilterMode }) => {
// add custom filter options
@@ -448,14 +449,14 @@ export const CippDataTable = (props) => {
if (filters && Array.isArray(filters) && filters.length > 0 && memoizedColumns.length > 0) {
// Make sure the table and columns are ready
setTimeout(() => {
- if (table && typeof table.setColumnFilters === 'function') {
- const formattedFilters = filters.map(filter => ({
+ if (table && typeof table.setColumnFilters === "function") {
+ const formattedFilters = filters.map((filter) => ({
id: filter.id || filter.columnId,
- value: filter.value
+ value: filter.value,
}));
table.setColumnFilters(formattedFilters);
}
- },);
+ });
}
}, [filters, memoizedColumns, table]);
From 4ef55cb114cb96ad9426580114906f643a2a761b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 23 May 2025 01:16:18 +0200
Subject: [PATCH 060/865] enable form reset after adding a contact
---
src/pages/email/administration/contacts/add.jsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/email/administration/contacts/add.jsx b/src/pages/email/administration/contacts/add.jsx
index 00c8becbe01a..ee789696e783 100644
--- a/src/pages/email/administration/contacts/add.jsx
+++ b/src/pages/email/administration/contacts/add.jsx
@@ -28,6 +28,7 @@ const AddContact = () => {
title="Add Contact"
backButtonTitle="Contacts Overview"
postUrl="/api/AddContact"
+ resetForm={true}
customDataformatter={(values) => {
// Add tenantDomain to the payload
return {
From 0a7ec7208b4e86b4dd978edca0cc716dd052db1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 23 May 2025 01:24:20 +0200
Subject: [PATCH 061/865] fix: update title in CippFormPage to specify 'Guest
User'
---
src/pages/identity/administration/users/invite.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/identity/administration/users/invite.jsx b/src/pages/identity/administration/users/invite.jsx
index cd755cfd9909..8b8c755e1049 100644
--- a/src/pages/identity/administration/users/invite.jsx
+++ b/src/pages/identity/administration/users/invite.jsx
@@ -19,7 +19,7 @@ const Page = () => {
From 3c84136b422fe4b95bb53c7cc95f6c1bf4c164d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 23 May 2025 12:38:11 +0200
Subject: [PATCH 062/865] Feat: Add option to set password never expires in
user actions
---
.../CippComponents/CippUserActions.jsx | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx
index 1f7dea46433e..5f6c190e474b 100644
--- a/src/components/CippComponents/CippUserActions.jsx
+++ b/src/components/CippComponents/CippUserActions.jsx
@@ -8,6 +8,7 @@ import {
Email,
ForwardToInbox,
GroupAdd,
+ LockClock,
LockOpen,
LockPerson,
LockReset,
@@ -285,6 +286,29 @@ export const CippUserActions = () => {
confirmText: "Are you sure you want to reset the password for this user?",
multiPost: false,
},
+ {
+ label: "Set Password Never Expires",
+ type: "POST",
+ icon: ,
+ url: "/api/ExecPasswordNeverExpires",
+ data: { userId: "id", userPrincipalName: "userPrincipalName" },
+ fields: [
+ {
+ type: "autoComplete",
+ name: "PasswordPolicy",
+ label: "Password Policy",
+ options: [
+ { label: "Disable Password Expiration", value: "DisablePasswordExpiration" },
+ { label: "Enable Password Expiration", value: "None" },
+ ],
+ multiple: false,
+ creatable: false,
+ },
+ ],
+ 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.",
+ multiPost: false,
+ },
{
label: "Clear Immutable ID",
type: "POST",
From d2806ababd4715d9ce92a8d8b7942b32ec424069 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 23 May 2025 14:06:30 +0200
Subject: [PATCH 063/865] feat: add validation and new options to defender
deployment
---
.../security/defender/deployment/index.js | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js
index 8a3007ec7a99..cfc039e956e8 100644
--- a/src/pages/security/defender/deployment/index.js
+++ b/src/pages/security/defender/deployment/index.js
@@ -287,6 +287,7 @@ const DeployDefenderForm = () => {
{ label: "Assign to all users and devices", value: "AllDevicesAndUsers" },
]}
formControl={formControl}
+ validators={{ required: "Assignment must be selected" }}
row
/>
@@ -315,11 +316,29 @@ const DeployDefenderForm = () => {
ASR Rules
Set Attack Surface Reduction Rules
+
+
{
name="ASR.WMIPersistence"
formControl={formControl}
/>
+
{
name="ASR.BlockOfficeApps"
formControl={formControl}
/>
+
{
name="ASR.blockJSVB"
formControl={formControl}
/>
+
{
{ label: "Assign to all users and devices", value: "AllDevicesAndUsers" },
]}
formControl={formControl}
+ validators={{ required: "Assignment must be selected" }}
row
/>
From 842ddf5859847b2f5a8f9f3217d9d7833be899bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 23 May 2025 14:17:39 +0200
Subject: [PATCH 064/865] feat: add 'Warn mode' option to ASR rules too cause
why not
---
src/pages/security/defender/deployment/index.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js
index cfc039e956e8..6129a3e6e0d6 100644
--- a/src/pages/security/defender/deployment/index.js
+++ b/src/pages/security/defender/deployment/index.js
@@ -323,6 +323,7 @@ const DeployDefenderForm = () => {
options={[
{ label: "Audit mode", value: "audit" },
{ label: "Block mode", value: "block" },
+ { label: "Warn mode", value: "warn" },
]}
formControl={formControl}
validators={{ required: "Mode must be selected" }}
From 42a6310de068af95242947eaddb7bd6e5a5ffc04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Fri, 23 May 2025 14:23:18 +0200
Subject: [PATCH 065/865] change to be the order its listed in the portal to
help my crippling need for everything to make sense
---
src/pages/security/defender/deployment/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js
index 6129a3e6e0d6..3f9de31aeb81 100644
--- a/src/pages/security/defender/deployment/index.js
+++ b/src/pages/security/defender/deployment/index.js
@@ -321,8 +321,8 @@ const DeployDefenderForm = () => {
label=""
name="ASR.Mode"
options={[
- { label: "Audit mode", value: "audit" },
{ label: "Block mode", value: "block" },
+ { label: "Audit mode", value: "audit" },
{ label: "Warn mode", value: "warn" },
]}
formControl={formControl}
From 3aa6f3084937bedfd14bb202258dc82a6f2cbf99 Mon Sep 17 00:00:00 2001
From: David Szpunar
Date: Fri, 23 May 2025 17:57:00 -0400
Subject: [PATCH 066/865] fix: update helpText for GitHub integration fix
Repositorities typo
---
src/data/Extensions.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/data/Extensions.json b/src/data/Extensions.json
index 3c61cbbba574..badbfc046cf9 100644
--- a/src/data/Extensions.json
+++ b/src/data/Extensions.json
@@ -841,7 +841,7 @@
"logo": "/assets/integrations/github.png",
"logoDark": "/assets/integrations/github_dark.png",
"description": "Enable the GitHub integration to manage your repositories from CIPP.",
- "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositorities functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.",
+ "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositories functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.",
"links": [
{
"name": "GitHub Token",
From 6d9a6bd7b48e877529521a57ee4f773d307ecdcd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Sat, 24 May 2025 14:44:07 +0200
Subject: [PATCH 067/865] feat: re-add functionality to remove users from
groups in edit mode
---
.../CippFormPages/CippAddEditUser.jsx | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx
index 8912c6a4f807..a7bbc02a35d0 100644
--- a/src/components/CippFormPages/CippAddEditUser.jsx
+++ b/src/components/CippFormPages/CippAddEditUser.jsx
@@ -352,6 +352,28 @@ const CippAddEditUser = (props) => {
/>
)}
+ {formType === "edit" && (
+ // Unable to only list groups that the user is not already a member of, as only 20 members are returned by the API.
+ // This means that the user may be a member of a group that is not listed here, and will not be able to be removed from it. -Bobby
+
+
+
+ )}
{/* Schedule User Creation */}
{formType === "add" && (
From 11ee6a1f4ed7ca5a61da285b9461f67887d7bf95 Mon Sep 17 00:00:00 2001
From: ngms-psh
Date: Sat, 24 May 2025 23:21:57 +0200
Subject: [PATCH 068/865] Added option for tooltip in CippInfoBar component
---
src/components/CippCards/CippInfoBar.jsx | 49 ++++++++++++++++--------
1 file changed, 34 insertions(+), 15 deletions(-)
diff --git a/src/components/CippCards/CippInfoBar.jsx b/src/components/CippCards/CippInfoBar.jsx
index 28ca740199f7..9be1efd190b3 100644
--- a/src/components/CippCards/CippInfoBar.jsx
+++ b/src/components/CippCards/CippInfoBar.jsx
@@ -1,5 +1,5 @@
import React, { useState } from "react";
-import { Box, Card, Stack, SvgIcon, Typography, Skeleton } from "@mui/material";
+import { Box, Card, Stack, SvgIcon, Typography, Skeleton, Tooltip } from "@mui/material";
import Grid from "@mui/material/Grid";
import { CippOffCanvas } from "../CippComponents/CippOffCanvas";
import { CippPropertyListCard } from "./CippPropertyListCard";
@@ -45,20 +45,39 @@ export const CippInfoBar = ({ data, isFetching }) => {
{item.icon}
)}
- {
- if (!item?.icon) {
- return { pl: 2 };
- }
- }}
- >
-
- {item.name}
-
-
- {isFetching ? : item.data}
-
-
+ {item?.toolTip ? (
+
+ {
+ if (!item?.icon) {
+ return { pl: 2 };
+ }
+ }}
+ >
+
+ {item.name}
+
+
+ {isFetching ? : item.data}
+
+
+
+ ) : (
+ {
+ if (!item?.icon) {
+ return { pl: 2 };
+ }
+ }}
+ >
+
+ {item.name}
+
+
+ {isFetching ? : item.data}
+
+
+ )}
{item.offcanvas && (
From 6076a732e18734b04b5d72f0bd24e2e9bace79c2 Mon Sep 17 00:00:00 2001
From: ngms-psh
Date: Sat, 24 May 2025 23:23:19 +0200
Subject: [PATCH 069/865] Added page for list/edit and add Quarantine Policies
---
src/layouts/config.js | 4 +
.../list-quarantine-policies/add.jsx | 148 ++++++
.../list-quarantine-policies/index.js | 436 ++++++++++++++++++
3 files changed, 588 insertions(+)
create mode 100644 src/pages/email/spamfilter/list-quarantine-policies/add.jsx
create mode 100644 src/pages/email/spamfilter/list-quarantine-policies/index.js
diff --git a/src/layouts/config.js b/src/layouts/config.js
index b1d9e72b9623..d1dbcdd943c0 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -342,6 +342,10 @@ export const nativeMenuItems = [
title: "Connection filter templates",
path: "/email/spamfilter/list-connectionfilter-templates",
},
+ {
+ title: "Quarantine Policies",
+ path: "/email/spamfilter/list-quarantine-policies",
+ },
],
},
{
diff --git a/src/pages/email/spamfilter/list-quarantine-policies/add.jsx b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx
new file mode 100644
index 000000000000..d84ecdbe00f9
--- /dev/null
+++ b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx
@@ -0,0 +1,148 @@
+import React, { useEffect } from "react";
+import { Grid, Divider } from "@mui/material";
+import { useForm, useWatch } 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 { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector";
+
+const AddPolicy = () => {
+ const formControl = useForm({
+ mode: "onChange",
+ defaultValues: {
+ selectedTenants: [],
+ TemplateList: null,
+ PowerShellCommand: "",
+ },
+ });
+
+ const templateListVal = useWatch({ control: formControl.control, name: "TemplateList" });
+
+ useEffect(() => {
+ if (templateListVal?.value) {
+ formControl.setValue("PowerShellCommand", JSON.stringify(templateListVal?.value));
+ }
+ }, [templateListVal, formControl]);
+
+ // Watch the value of QuarantineNotification
+ const quarantineNotification = useWatch({
+ control: formControl.control,
+ name: "QuarantineNotification",
+ });
+
+ return (
+
+
+
+
+
+
+ {/* */}
+
+ {/* TemplateList, can be added later. But did not seem necessary with so few settings */}
+ {/*
+ option,
+ url: "/api/ListSpamFilterTemplates",
+ }}
+ placeholder="Select a template or enter PowerShell JSON manually"
+ />
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+AddPolicy.getLayout = (page) => {page} ;
+
+export default AddPolicy;
diff --git a/src/pages/email/spamfilter/list-quarantine-policies/index.js b/src/pages/email/spamfilter/list-quarantine-policies/index.js
new file mode 100644
index 000000000000..176bdb11c534
--- /dev/null
+++ b/src/pages/email/spamfilter/list-quarantine-policies/index.js
@@ -0,0 +1,436 @@
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import CippButtonCard from "/src/components/CippCards/CippButtonCard";
+import { CippInfoBar } from "/src/components/CippCards/CippInfoBar";
+import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx";
+import { Alert, Typography, Stack, Tooltip, IconButton, SvgIcon, Button } from "@mui/material";
+import { Grid } from "@mui/system";
+import Link from "next/link";
+import {
+ AccessTime,
+ CorporateFare,
+ AlternateEmail,
+ Language,
+ Sync,
+ RocketLaunch,
+ Edit,
+ Delete,
+} from "@mui/icons-material";
+import { useSettings } from "/src/hooks/use-settings";
+import { useDialog } from "/src/hooks/use-dialog";
+import { ApiGetCall } from "/src/api/ApiCall";
+import { useState, useRef } from "react";
+
+const Page = () => {
+ const pageTitle = "Quarantine Policies";
+ const { currentTenant } = useSettings();
+
+ const createDialog = useDialog();
+
+ // Use ApiGetCall directly as a hook
+ const GlobalQuarantinePolicy = ApiGetCall({
+ url: "/api/ListQuarantinePolicy",
+ data: { tenantFilter: currentTenant, type: "GlobalQuarantinePolicy" },
+ queryKey: "GlobalQuarantinePolicy",
+ });
+
+ // Get the policy data regardless of array or object
+ const globalQuarantineData = Array.isArray(GlobalQuarantinePolicy.data)
+ ? GlobalQuarantinePolicy.data[0]
+ : GlobalQuarantinePolicy.data;
+
+ const hasGlobalQuarantinePolicyData = !!globalQuarantineData;
+
+
+ if (hasGlobalQuarantinePolicyData) {
+ globalQuarantineData.EndUserSpamNotificationFrequency =
+ globalQuarantineData?.EndUserSpamNotificationFrequency === "P1D"
+ ? "Daily"
+ : globalQuarantineData?.EndUserSpamNotificationFrequency === "P7D"
+ ? "Weekly"
+ : globalQuarantineData?.EndUserSpamNotificationFrequency === "PT4H"
+ ? "4 hours"
+ : globalQuarantineData?.EndUserSpamNotificationFrequency
+ }
+
+ const multiLanguagePropertyItems = hasGlobalQuarantinePolicyData
+ ? (
+ Array.isArray(globalQuarantineData?.MultiLanguageSetting) && globalQuarantineData.MultiLanguageSetting.length > 0
+ ? globalQuarantineData.MultiLanguageSetting.map((language, idx) => ({
+ language: language == "Default" ? "English_USA"
+ : language == "English" ? "English_GB"
+ : language,
+ senderDisplayName:
+ globalQuarantineData.MultiLanguageSenderName[idx] &&
+ globalQuarantineData.MultiLanguageSenderName[idx].trim() !== ""
+ ? globalQuarantineData.MultiLanguageSenderName[idx]
+ : "None",
+ subject:
+ globalQuarantineData.EsnCustomSubject[idx] &&
+ globalQuarantineData.EsnCustomSubject[idx].trim() !== ""
+ ? globalQuarantineData.EsnCustomSubject[idx]
+ : "None",
+ disclaimer:
+ globalQuarantineData.MultiLanguageCustomDisclaimer[idx] &&
+ globalQuarantineData.MultiLanguageCustomDisclaimer[idx].trim() !== ""
+ ? globalQuarantineData.MultiLanguageCustomDisclaimer[idx]
+ : "None",
+ }))
+ : [
+ {
+ language: "None",
+ senderDisplayName: "None",
+ subject: "None",
+ disclaimer: "None",
+ },
+ ]
+ )
+ : [];
+
+ const buttonCardActions = [
+ <>
+ }>
+ Edit Settings
+
+
+ {
+ GlobalQuarantinePolicy.refetch();
+ }}
+ >
+
+
+
+
+
+ >
+ ];
+
+ // Actions to perform (Delete Policy)
+ const actions = [
+ {
+ label: "Edit Policy",
+ type: "POST",
+ url: "/api/EditQuarantinePolicy?type=QuarantinePolicy",
+ setDefaultValues: true,
+ fields: [
+ {
+ type: "textField",
+ name: "Name",
+ label: "Policy Name",
+ disabled: true,
+ },
+ {
+ type: "autoComplete",
+ name: "ReleaseActionPreference",
+ label: "Select release action preference",
+ multiple : false,
+ creatable : false,
+ options: [
+ { label: "Release", value: "Release" },
+ { label: "Request Release", value: "RequestRelease" },
+ ],
+ },
+ {
+ type: "switch",
+ name: "Delete",
+ label: "Delete",
+ },
+ {
+ type: "switch",
+ name: "Preview",
+ label: "Preview",
+ },
+ {
+ type: "switch",
+ name: "BlockSender",
+ label: "Block Sender",
+ },
+ {
+ type: "switch",
+ name: "AllowSender",
+ label: "Allow Sender",
+ },
+ {
+ type: "switch",
+ name: "QuarantineNotification",
+ label: "Quarantine Notification",
+ },
+ {
+ type: "switch",
+ name: "IncludeMessagesFromBlockedSenderAddress",
+ label: "Include Messages From Blocked Sender Address",
+ },
+ ],
+ data: { Identity: "Guid", Action: "!Edit" },
+ confirmText: "Update Quarantine Policy '[Name]'? Policy Name cannot be changed.",
+ multiPost: false,
+ icon: ,
+ color: "info",
+ condition: (row) => row.Guid != "00000000-0000-0000-0000-000000000000",
+ },
+ {
+ label: "Delete Policy",
+ type: "POST",
+ icon: ,
+ url: "/api/RemoveQuarantinePolicy",
+ data: {
+ Name: "Name",
+ Identity: "Guid",
+ },
+ confirmText: (
+ <>
+
+ Are you sure you want to delete this policy?
+
+
+ Note: This will delete the Quarantine policy, even if it is currently in use.
+ Removing the Admin and User Access it applies to emails.
+
+
+ Confirm the Quarantine is not applied in any of the following policies:
+
+ Anti-phishing
+ Anti-spam
+ Anti-malware
+ Safe Attachments
+
+
+ >
+ ),
+ condition: (row) => row.Guid != "00000000-0000-0000-0000-000000000000",
+ },
+ ];
+
+ // Off-canvas structure: displays extended details and includes actions (Enable/Disable Rule)
+ const offCanvas = {
+ extendedInfoFields: [
+ "Id", // Policy Name/Id
+ "Name", // Policy Name
+ "EndUserQuarantinePermissions",
+ "Guid",
+ "Builtin",
+ "WhenCreated", // Creation Date
+ "WhenChanged", // Last Modified Date
+ ],
+ actions: actions,
+ };
+
+ const filterList = [
+ {
+ filterName: "Custom Policies",
+ value: [{ id: "Builtin", value: "No" }],
+ type: "column",
+ },
+ {
+ filterName: "Built-in Policies",
+ value: [{ id: "Builtin", value: "Yes" }],
+ type: "column",
+ },
+ ];
+
+
+ const customLanguageOffcanvas =
+ multiLanguagePropertyItems && multiLanguagePropertyItems.length > 0
+ ? {
+ offcanvas: {
+ title: "Custom Language Settings",
+ propertyItems: multiLanguagePropertyItems.map((item, idx) => ({
+ label: "",
+ value: (
+
+
+ {item.language}
+
+ }
+ cardSx={{ mb: 2 }}
+ >
+
+ {Object.entries(item)
+ .filter(([key]) => key !== "language")
+ .map(([key, value]) => (
+
+
+ {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
+
+
+ {value}
+
+
+ ))}
+
+
+ ),
+ })),
+ }
+ }
+ : {};
+
+ // Simplified columns for the table
+ const simpleColumns = [
+ "Name",
+ "ReleaseActionPreference",
+ "Delete",
+ "Preview",
+ "BlockSender",
+ "AllowSender",
+ "QuarantineNotification",
+ "IncludeMessagesFromBlockedSenderAddress",
+ "WhenCreated",
+ "WhenChanged",
+ ];
+
+
+
+ // Prepare data for CippInfoBar as a const to clean up the code
+ const infoBarData = [
+ {
+ icon: ,
+ data: globalQuarantineData?.EndUserSpamNotificationFrequency,
+ name: "Notification Frequency",
+ },
+ {
+ icon: ,
+ data: hasGlobalQuarantinePolicyData
+ ? (globalQuarantineData?.OrganizationBrandingEnabled
+ ? "Enabled"
+ : "Disabled"
+ )
+ : "n/a",
+ name: "Branding",
+ },
+ {
+ icon: ,
+ data: hasGlobalQuarantinePolicyData
+ ? (globalQuarantineData?.EndUserSpamNotificationCustomFromAddress
+ ? globalQuarantineData?.EndUserSpamNotificationCustomFromAddress
+ : "None")
+ : "n/a" ,
+ name: "Custom Sender Address",
+ },
+ {
+ icon: ,
+ toolTip: "More Info",
+ data: hasGlobalQuarantinePolicyData
+ ? (
+ multiLanguagePropertyItems.length > 0
+ ? multiLanguagePropertyItems.map(item => item.language).join(", ")
+ : "None"
+ )
+ : "n/a",
+ name: "Custom Language",
+ ...customLanguageOffcanvas,
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ Deploy Custom Policy
+
+ >
+ }
+ />
+
+ >
+ );
+};
+
+// Layout configuration: ensure page uses DashboardLayout
+Page.getLayout = (page) => {page} ;
+
+export default Page;
From ddeb80ece6539297d65dbb5bf38d6c7154e43769 Mon Sep 17 00:00:00 2001
From: ngms-psh
Date: Sat, 24 May 2025 23:25:12 +0200
Subject: [PATCH 070/865] Removed unused imports
---
src/pages/email/spamfilter/list-quarantine-policies/index.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/pages/email/spamfilter/list-quarantine-policies/index.js b/src/pages/email/spamfilter/list-quarantine-policies/index.js
index 176bdb11c534..3e3151657dbd 100644
--- a/src/pages/email/spamfilter/list-quarantine-policies/index.js
+++ b/src/pages/email/spamfilter/list-quarantine-policies/index.js
@@ -19,7 +19,6 @@ import {
import { useSettings } from "/src/hooks/use-settings";
import { useDialog } from "/src/hooks/use-dialog";
import { ApiGetCall } from "/src/api/ApiCall";
-import { useState, useRef } from "react";
const Page = () => {
const pageTitle = "Quarantine Policies";
From b0c0c2ac9c41974bc13cf39758ec297069f06708 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 26 May 2025 12:35:25 +0200
Subject: [PATCH 071/865] wizard steps
---
src/components/CippWizard/CippAlertsStep.jsx | 89 +++++++++++++++++++
.../CippWizard/CippBaselinesStep.jsx | 5 +-
.../CippWizard/CippNotificationsStep.jsx | 3 +-
src/pages/onboardingv2.js | 7 +-
4 files changed, 96 insertions(+), 8 deletions(-)
create mode 100644 src/components/CippWizard/CippAlertsStep.jsx
diff --git a/src/components/CippWizard/CippAlertsStep.jsx b/src/components/CippWizard/CippAlertsStep.jsx
new file mode 100644
index 000000000000..eef7f31df811
--- /dev/null
+++ b/src/components/CippWizard/CippAlertsStep.jsx
@@ -0,0 +1,89 @@
+import { Alert, Stack, Typography, Box, Grid } from "@mui/material";
+import CippFormComponent from "../CippComponents/CippFormComponent";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippFormTenantSelector } from "../CippComponents/CippFormTenantSelector";
+import { CippFormCondition } from "../CippComponents/CippFormCondition";
+import alertList from "../../data/alerts.json";
+
+export const CippAlertsStep = (props) => {
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+
+ const postExecutionOptions = [
+ { label: "Webhook", value: "Webhook" },
+ { label: "Email", value: "Email" },
+ { label: "PSA", value: "PSA" },
+ ];
+
+ const recurrenceOptions = [
+ { value: "30m", label: "Every 30 minutes" },
+ { value: "1h", label: "Every hour" },
+ { value: "4h", label: "Every 4 hours" },
+ { value: "1d", label: "Every 1 day" },
+ { value: "7d", label: "Every 7 days" },
+ { value: "30d", label: "Every 30 days" },
+ { value: "365d", label: "Every 365 days" },
+ ];
+
+ return (
+
+
+ Almost done
+
+
+ There's a couple more things that you can configure outside of the wizard, let's list
+ some of them;
+
+
+
+ CIPP has the ability to send alerts to your PSA, Webhook or Email. You can configure
+ these settings under > Tenant Administration > Alert Configuration.
+
+
+
+
+ If you imported baselines, or want to set tenants to your own baseline, you should
+ check out our standards under these settings under > Tenant Administration >
+ Standards.
+
+
+
+
+ If you want to use our integrations, you should set these up under > CIPP >
+ Integrations. Some examples are CSP integrations, Password Pusher, PSA, and more.
+
+
+
+
+ You can deploy Windows Applications too, directly using intune. We have Chocolately,
+ WinGet, and RMM apps under > Intune > Applications. Some examples are CSP
+ integrations, Password Pusher, PSA, and more.
+
+
+
+
+ You can deploy Windows Applications too, directly using intune. We have Chocolately,
+ WinGet, and RMM apps under > Intune > Applications. Some examples are CSP
+ integrations, Password Pusher, PSA, and more.
+
+
+
+
+ Tenants can be grouped, and you can implement custom variables for your tenants under
+ WinGet, and RMM apps under > Tenant Administrator > Administration > Tenants.
+
+
+
+
+
+
+
+ );
+};
+
+export default CippAlertsStep;
diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx
index 33fb138ed8d3..9c6248802867 100644
--- a/src/components/CippWizard/CippBaselinesStep.jsx
+++ b/src/components/CippWizard/CippBaselinesStep.jsx
@@ -1,9 +1,7 @@
-import { useState } from "react";
import { Alert, Stack, Typography, FormControl, FormLabel, Box } from "@mui/material";
import CippFormComponent from "../CippComponents/CippFormComponent";
import { CippWizardStepButtons } from "./CippWizardStepButtons";
import { CippFormCondition } from "../CippComponents/CippFormCondition";
-import { ApiGetCall } from "../../api/ApiCall";
export const CippBaselinesStep = (props) => {
const { formControl, onPreviousStep, onNextStep, currentStep } = props;
@@ -14,7 +12,8 @@ export const CippBaselinesStep = (props) => {
Baselines are template configurations that can be used as examples for setting up your
- environment.
+ environment. Don't want to configure these yet? No problem! You can find the templates
+ at Tools - Community Repositories
Downloading these baselines will create templates in your CIPP instance. These templates
diff --git a/src/components/CippWizard/CippNotificationsStep.jsx b/src/components/CippWizard/CippNotificationsStep.jsx
index 6954bfeff417..64ac65059344 100644
--- a/src/components/CippWizard/CippNotificationsStep.jsx
+++ b/src/components/CippWizard/CippNotificationsStep.jsx
@@ -12,7 +12,8 @@ export const CippNotificationsStep = (props) => {
Configure your notification settings. These settings will determine how you receive alerts
from CIPP. You can test your configuration using the "Send Test Alert" button. Don't want
- to setup notifications yet? You can skip this step and configure it later.
+ to setup notifications yet? You can skip this step and configure it later via Application
+ Settings - Notifications
{/* Use the reusable notification form component */}
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index e152691fcc77..bcfeabeba62c 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -7,6 +7,7 @@ import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx";
import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx";
import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx";
import { CippNotificationsStep } from "../components/CippWizard/CippNotificationsStep.jsx";
+import { CippAlertsStep } from "../components/CippWizard/CippAlertsStep.jsx";
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -76,13 +77,11 @@ const Page = () => {
title: "Step 5",
description: "Notifications",
component: CippNotificationsStep,
- //Show the notification menu (cipp/settings/notifications) without the submit/save button, but with a test button.
},
{
title: "Step 6",
- description: "Alerts",
- component: CippDeploymentStep,
- //show template alerts, allow user to configure them.
+ description: "Next Steps",
+ component: CippAlertsStep,
},
{
title: "Step 7",
From b4a08bd807dde57f08ccab7cf7658533f2119e65 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 26 May 2025 18:32:40 +0200
Subject: [PATCH 072/865] wizard changes
---
.../CippComponents/CippTranslations.jsx | 3 +
src/components/CippWizard/CippAlertsStep.jsx | 8 +-
.../CippWizard/CippBaselinesStep.jsx | 5 +-
.../CippWizard/CippTenantModeDeploy.jsx | 281 +++++++-----------
src/pages/onboardingv2.js | 2 +-
src/utils/get-cipp-formatting.js | 16 +
6 files changed, 135 insertions(+), 180 deletions(-)
diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx
index fef71fa8ccb0..ebdbca9238bf 100644
--- a/src/components/CippComponents/CippTranslations.jsx
+++ b/src/components/CippComponents/CippTranslations.jsx
@@ -46,4 +46,7 @@ export const CippTranslations = {
prohibitSendReceiveQuotaInBytes: "Quota",
ClientId: "Client ID",
html_url: "URL",
+ sendtoIntegration: "Send Notifications to Integration",
+ includeTenantId: "Include Tenant ID in Notifications",
+ logsToInclude: "Logs to Include in notifications",
};
diff --git a/src/components/CippWizard/CippAlertsStep.jsx b/src/components/CippWizard/CippAlertsStep.jsx
index eef7f31df811..914db4fa5ae3 100644
--- a/src/components/CippWizard/CippAlertsStep.jsx
+++ b/src/components/CippWizard/CippAlertsStep.jsx
@@ -69,7 +69,13 @@ export const CippAlertsStep = (props) => {
Tenants can be grouped, and you can implement custom variables for your tenants under
- WinGet, and RMM apps under > Tenant Administrator > Administration > Tenants.
+ WinGet, and RMM apps under Tenant Administrator > Administration > Tenants.
+
+
+
+
+ Have an enterprise app you want to deploy? Check out our tools {" "}
+ section. This menu also contains useful things such as our geo-ip lookup, and more.
diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx
index 9c6248802867..0343e8a33f1c 100644
--- a/src/components/CippWizard/CippBaselinesStep.jsx
+++ b/src/components/CippWizard/CippBaselinesStep.jsx
@@ -80,7 +80,10 @@ export const CippBaselinesStep = (props) => {
queryKey: `ListBaselines`,
url: "/api/ListCommunityRepos",
labelField: (option) => `${option.Name} (${option.Owner})`,
- valueField: "Id",
+ valueField: "FullName",
+ addedFields: {
+ templateRepoBranch: "main",
+ },
}}
multiple={true}
placeholder="Select one or more baselines"
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index ba34e20c0c2f..0bdb5382bd6f 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -21,7 +21,7 @@ import { getCippError } from "../../utils/get-cipp-error";
export const CippTenantModeDeploy = (props) => {
const { formControl, currentStep, onPreviousStep, onNextStep } = props;
- const [tenantMode, setTenantMode] = useState("GDAP");
+ const [tenantMode, setTenantMode] = useState("mixed");
const [allowPartnerTenantManagement, setAllowPartnerTenantManagement] = useState(false);
const [gdapAuthStatus, setGdapAuthStatus] = useState({
success: false,
@@ -45,44 +45,12 @@ export const CippTenantModeDeploy = (props) => {
// Update authenticated tenants list when tenantList changes
useEffect(() => {
- if (tenantList.data && (tenantMode === "perTenant" || tenantMode === "mixed")) {
+ if (tenantList.data) {
setAuthenticatedTenants(tenantList.data);
}
- }, [tenantList.data, tenantMode]);
+ }, [tenantList.data]);
- // Handle tenant mode change
- const handleTenantModeChange = (selectedOption) => {
- if (selectedOption) {
- setTenantMode(selectedOption.value);
- // Reset auth status when changing modes
- setGdapAuthStatus({
- success: false,
- loading: false,
- });
- setPerTenantAuthStatus({
- success: false,
- loading: false,
- });
- // Reset partner tenant management option
- setAllowPartnerTenantManagement(false);
- }
- };
-
- // Tenant mode options
- const tenantModeOptions = [
- {
- label: "GDAP - Uses your partner center to connect to tenants",
- value: "GDAP",
- },
- {
- label: "Per Tenant - Add each tenant individually",
- value: "perTenant",
- },
- {
- label: "Mixed - Use Partner Center and add tenants individually",
- value: "mixed",
- },
- ];
+ // Tenant mode is always set to "mixed"
// Handle GDAP authentication success
const handleGdapAuthSuccess = (tokenData) => {
@@ -121,13 +89,8 @@ export const CippTenantModeDeploy = (props) => {
}
);
- // Allow user to proceed to next step for GDAP mode
- if (tenantMode === "GDAP") {
- formControl.setValue("tenantModeSet", true);
- } else if (tenantMode === "mixed") {
- // For mixed mode, allow proceeding if either authentication is successful
- formControl.setValue("tenantModeSet", true);
- }
+ // Allow user to proceed to next step
+ formControl.setValue("tenantModeSet", true);
};
// Handle perTenant authentication success
@@ -138,32 +101,22 @@ export const CippTenantModeDeploy = (props) => {
loading: true,
});
- // For perTenant mode or mixed mode with perTenant auth, add the tenant to the cache
- if (tenantMode === "perTenant" || tenantMode === "mixed") {
- // Call the AddTenant API to add the tenant to the cache with directTenant status
- console.log(tokenData);
- addTenant.mutate({
- url: "/api/ExecAddTenant",
- data: {
- tenantId: tokenData.tenantId,
- access_token: tokenData.accessToken,
- },
- });
- } else {
- // If not adding tenant, still update state
- setPerTenantAuthStatus({
- success: true,
- loading: false,
- });
- }
+ // Add the tenant to the cache
+ // Call the AddTenant API to add the tenant to the cache with directTenant status
+ console.log(tokenData);
+ addTenant.mutate({
+ url: "/api/ExecAddTenant",
+ data: {
+ tenantId: tokenData.tenantId,
+ access_token: tokenData.accessToken,
+ },
+ });
// Allow user to proceed to next step
formControl.setValue("tenantModeSet", true);
- // Refresh tenant list for perTenant and mixed modes
- if (tenantMode === "perTenant" || tenantMode === "mixed") {
- tenantList.refetch();
- }
+ // Refresh tenant list
+ tenantList.refetch();
};
// Handle API error
@@ -180,36 +133,22 @@ export const CippTenantModeDeploy = (props) => {
- Select how you want to connect to your tenants. You have three options:
+ CIPP can connect to your Microsoft 365 tenants in two ways:
- GDAP: Use delegated administration (recommended)
+ Use GDAP delegated administration through partner center. This option is best when you
+ are a Microsoft Partner managing multiple tenants, and want to add tenants to your
+ CIPP environment without needing to authenticate to each tenant separately.
- Per Tenant: Authenticate to each tenant individually
-
-
- Mixed: Use both GDAP and per-tenant authentication
+ Authenticate to individual tenants separately. This option is best when you are not a
+ Microsoft Partner, or when you have tenants that are not added to your GDAP
+ environment.
- {/* Tenant mode selection */}
-
-
- Tenant Connection Mode
-
- option.value === tenantMode)}
- onChange={handleTenantModeChange}
- multiple={false}
- required={true}
- />
-
-
{/* Show API results at top level for visibility across all modes */}
@@ -217,105 +156,93 @@ export const CippTenantModeDeploy = (props) => {
{/* GDAP Authentication Section */}
- {(tenantMode === "GDAP" || tenantMode === "mixed") && (
-
-
- Partner Tenant
-
+
+
+ Partner Tenant
+
- {/* GDAP Partner Tenant Management Switch */}
- setAllowPartnerTenantManagement(e.target.checked)}
- color="primary"
- />
- }
- label="Allow management of the partner tenant."
- />
+ {/* GDAP Partner Tenant Management Switch */}
+ setAllowPartnerTenantManagement(e.target.checked)}
+ color="primary"
+ />
+ }
+ label="Allow management of the partner tenant."
+ />
- {/* Always show authenticate button */}
-
-
- {
- // Add the tenantMode and allowPartnerTenantManagement parameters to the tokenData
- const updatedTokenData = {
- ...tokenData,
- tenantMode: tenantMode === "mixed" ? "GDAP" : tenantMode,
- allowPartnerTenantManagement: allowPartnerTenantManagement,
- };
- handleGdapAuthSuccess(updatedTokenData);
- }}
- buttonText={
- tenantMode === "mixed" ? "Connect to GDAP" : "Authenticate with Microsoft GDAP"
- }
- showSuccessAlert={false}
- />
-
-
+ {/* Always show authenticate button */}
+
+
+ {
+ // Add the tenantMode and allowPartnerTenantManagement parameters to the tokenData
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: "GDAP",
+ allowPartnerTenantManagement: allowPartnerTenantManagement,
+ };
+ handleGdapAuthSuccess(updatedTokenData);
+ }}
+ buttonText="Connect using GDAP (Recommended)"
+ showSuccessAlert={false}
+ />
+
- )}
+
{/* Per Tenant Authentication Section */}
- {(tenantMode === "perTenant" || tenantMode === "mixed") && (
-
-
- Per-Tenant Authentication
-
-
-
- {tenantMode === "mixed"
- ? "Click the button below to connect to individual tenants. You can authenticate to multiple tenants one by one."
- : "You can click the button below to authenticate to a tenant. Perform this authentication for every tenant you wish to manage using CIPP."}
-
+
+
+ Per-Tenant Authentication
+
- {/* Show authenticate button */}
-
-
- {
- // Add the tenantMode parameter to the tokenData
- const updatedTokenData = {
- ...tokenData,
- tenantMode: tenantMode === "mixed" ? "perTenant" : tenantMode,
- };
- handlePerTenantAuthSuccess(updatedTokenData);
- }}
- buttonText={
- tenantMode === "mixed"
- ? "Connect to Separate Tenants"
- : "Authenticate with Microsoft"
- }
- showSuccessAlert={false}
- />
-
-
+
+ Click the button below to connect to individual tenants. You can authenticate to multiple
+ tenants by repeating this step for each tenant you want to add.
+
- {/* List authenticated tenants for perTenant and mixed modes */}
- {(tenantMode === "perTenant" || tenantMode === "mixed") &&
- authenticatedTenants.length > 0 && (
-
-
- Authenticated Tenants
-
-
-
- {authenticatedTenants.map((tenant, index) => (
-
-
-
- ))}
-
-
-
- )}
+ {/* Show authenticate button */}
+
+
+ {
+ // Add the tenantMode parameter to the tokenData
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: "perTenant",
+ };
+ handlePerTenantAuthSuccess(updatedTokenData);
+ }}
+ buttonText="Connect to Separate Tenants"
+ showSuccessAlert={false}
+ />
+
- )}
+
+ {/* List authenticated tenants */}
+ {authenticatedTenants.length > 0 && (
+
+
+ Authenticated Tenants
+
+
+
+ {authenticatedTenants.map((tenant, index) => (
+
+
+
+ ))}
+
+
+
+ )}
+
{
backButton={false}
steps={steps}
wizardTitle="Setup Wizard"
- postUrl={"/api/ExecSAMSetup"}
+ postUrl={"/api/ExecCombinedSetup"}
/>
>
);
diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js
index fac1ea1aae28..0774fbefd3e8 100644
--- a/src/utils/get-cipp-formatting.js
+++ b/src/utils/get-cipp-formatting.js
@@ -86,6 +86,22 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
);
};
+ if (cellName === "baselineOption") {
+ return "Download Baseline";
+ }
+
+ if (cellName === "Severity" || cellName === "logsToInclude") {
+ if (Array.isArray(data)) {
+ return isText ? data.join(", ") : renderChipList(data);
+ } else {
+ return isText ? (
+ data
+ ) : (
+
+ );
+ }
+ }
+
//if the cellName starts with portal_, return text, or a link with an icon
if (cellName.startsWith("portal_")) {
const IconComponent = portalIcons[cellName];
From 264bc1a979638ffbd10fbb4a172064ab0b757991 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?=
Date: Mon, 26 May 2025 18:35:46 +0200
Subject: [PATCH 073/865] add goose code: implement user group filtering in
CippAddEditUser component
---
.../CippFormPages/CippAddEditUser.jsx | 65 +++++++++++++------
1 file changed, 46 insertions(+), 19 deletions(-)
diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx
index a7bbc02a35d0..610c1a5d1f19 100644
--- a/src/components/CippFormPages/CippAddEditUser.jsx
+++ b/src/components/CippFormPages/CippAddEditUser.jsx
@@ -9,11 +9,14 @@ import Grid from "@mui/material/Grid";
import { ApiGetCall } from "../../api/ApiCall";
import { useSettings } from "../../hooks/use-settings";
import { useWatch } from "react-hook-form";
-import { useEffect } from "react";
+import { use, useEffect, useMemo } from "react";
+import { useRouter } from "next/router";
const CippAddEditUser = (props) => {
const { formControl, userSettingsDefaults, formType = "add" } = props;
const tenantDomain = useSettings().currentTenant;
+ const router = useRouter();
+ const { userId } = router.query;
const integrationSettings = ApiGetCall({
url: "/api/ListExtensionsConfig",
queryKey: "ListExtensionsConfig",
@@ -21,6 +24,36 @@ const CippAddEditUser = (props) => {
refetchOnReconnect: false,
});
+ // Get all groups the is the user is a member of
+ const userGroups = ApiGetCall({
+ url: `/api/ListUserGroups?userId=${userId}&tenantFilter=${tenantDomain}`,
+ queryKey: `User-${userId}-Groups-${tenantDomain}`,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ waiting: !!userId,
+ });
+
+ // Get all groups for the tenant
+ const tenantGroups = ApiGetCall({
+ url: `/api/ListGroups?tenantFilter=${tenantDomain}`,
+ queryKey: `ListGroups-${tenantDomain}`,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ waiting: !!userId,
+ });
+
+ // Make new list of groups by removing userGroups from tenantGroups
+ const filteredTenantGroups = useMemo(() => {
+ if (tenantGroups.isSuccess && userGroups.isSuccess) {
+ const tenantGroupsList = tenantGroups?.data || [];
+
+ return tenantGroupsList.filter(
+ (tenantGroup) => !userGroups?.data?.some((userGroup) => userGroup.id === tenantGroup.id)
+ );
+ }
+ return [];
+ }, [tenantGroups.isSuccess, userGroups.isSuccess, tenantGroups.data, userGroups.data]);
+
const watcher = useWatch({ control: formControl.control });
useEffect(() => {
//if watch.firstname changes, and watch.lastname changes, set displayname to firstname + lastname
@@ -339,37 +372,31 @@ const CippAddEditUser = (props) => {
label="Add to Groups"
name="AddToGroups"
multiple={true}
- api={{
- url: "/api/ListGroups",
- queryKey: `ListGroups-${tenantDomain}`,
- labelField: "displayName",
- valueField: "id",
- addedField: {
- calculatedGroupType: "calculatedGroupType",
+ options={filteredTenantGroups?.map((tenantGroup) => ({
+ label: tenantGroup.displayName,
+ value: tenantGroup.id,
+ addedFields: {
+ calculatedGroupType: tenantGroup.calculatedGroupType,
},
- }}
+ }))}
formControl={formControl}
/>
)}
{formType === "edit" && (
- // Unable to only list groups that the user is not already a member of, as only 20 members are returned by the API.
- // This means that the user may be a member of a group that is not listed here, and will not be able to be removed from it. -Bobby
({
+ label: userGroups.DisplayName,
+ value: userGroups.id,
+ addedFields: {
+ calculatedGroupType: userGroups.calculatedGroupType,
},
- }}
+ }))}
formControl={formControl}
/>
From 9141d66a854a1e850a0e2e2eb9fad7f0996fd916 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 26 May 2025 21:28:29 +0200
Subject: [PATCH 074/865] tenant stuff
---
.../CippComponents/CippFormComponent.jsx | 4 +
.../CippWizard/CIPPDeploymentStep.jsx | 235 +-----------------
.../CippWizard/CippTenantModeDeploy.jsx | 3 -
src/components/CippWizard/CippWizard.jsx | 42 +++-
.../CippWizard/CippWizardStepButtons.jsx | 5 +-
src/components/CippWizard/wizard-steps.js | 4 +-
src/pages/onboardingv2.js | 26 +-
7 files changed, 68 insertions(+), 251 deletions(-)
diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx
index f42f5b1fbb16..5f5b3f71d46f 100644
--- a/src/components/CippComponents/CippFormComponent.jsx
+++ b/src/components/CippComponents/CippFormComponent.jsx
@@ -138,9 +138,13 @@ export const CippFormComponent = (props) => {
type="password"
variant="filled"
fullWidth
+ InputLabelProps={{
+ shrink: true,
+ }}
{...other}
{...formControl.register(convertedName, { ...validators })}
label={label}
+ defaultValue={defaultValue}
/>
diff --git a/src/components/CippWizard/CIPPDeploymentStep.jsx b/src/components/CippWizard/CIPPDeploymentStep.jsx
index 6ecb6d560a4e..7a6553b9323c 100644
--- a/src/components/CippWizard/CIPPDeploymentStep.jsx
+++ b/src/components/CippWizard/CIPPDeploymentStep.jsx
@@ -1,244 +1,29 @@
-import { useEffect, useState } from "react";
-import {
- Alert,
- Button,
- Grid,
- Link,
- Stack,
- Typography,
- Skeleton,
- Box,
- CircularProgress,
- SvgIcon,
-} from "@mui/material";
+import { useEffect } from "react";
+import { Stack, Typography } from "@mui/material";
import CippFormComponent from "../CippComponents/CippFormComponent";
import { CippWizardStepButtons } from "./CippWizardStepButtons";
-import { ApiGetCall } from "../../api/ApiCall";
-import CippButtonCard from "../CippCards/CippButtonCard";
-import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard";
-import { CheckCircle } from "@mui/icons-material";
-import CippPermissionCheck from "../CippSettings/CippPermissionCheck";
-import { useQueryClient } from "@tanstack/react-query";
-import { CippApiResults } from "../CippComponents/CippApiResults";
import { CIPPDeploymentUpdateTokens } from "./CIPPDeploymentUpdateTokens";
export const CippDeploymentStep = (props) => {
- const queryClient = useQueryClient();
const { formControl, onPreviousStep, onNextStep, currentStep } = props;
const values = formControl.getValues();
-
- const [currentStepState, setCurrentStepState] = useState(1);
- const [pollingStep, setPollingStep] = useState(1);
- const [approvalUrl, setApprovalUrl] = useState(true);
-
- const startSetupApi = ApiGetCall({
- url: "/api/ExecSAMSetup?CreateSAM=true&partnersetup=true",
- queryKey: "startSAMSetup",
- });
-
- const checkSetupStatusApi = ApiGetCall({
- url: `/api/ExecSAMSetup?CheckSetupProcess=true&step=${pollingStep}`,
- queryKey: `checkSetupStep${pollingStep}`,
- waiting: !pollingStep,
- });
- useEffect(() => {
- if (
- startSetupApi.data &&
- startSetupApi.data.step === 1 &&
- values.selectedOption === "CreateApp"
- ) {
- formControl.register("wizardStatus", {
- required: true,
- });
- formControl.setValue("noSubmitButton", true);
- setPollingStep(1);
- setCurrentStepState(1);
- }
- }, [startSetupApi.data]);
-
+
+ // Use useEffect to set form values instead of doing it during render
useEffect(() => {
- if (pollingStep && values.selectedOption === "CreateApp") {
- const intervalId = setInterval(() => {
- if (!checkSetupStatusApi.isFetching) {
- checkSetupStatusApi.refetch();
- }
- }, 5000);
- return () => clearInterval(intervalId);
+ if (values.selectedOption === "Manual") {
+ formControl.setValue("setKeys", true);
}
- }, [pollingStep, checkSetupStatusApi]);
-
- useEffect(() => {
- if (checkSetupStatusApi.data) {
- const { step, message, url, code } = checkSetupStatusApi.data;
- if (url) {
- setApprovalUrl(url);
- }
- if (step === 2) {
- setCurrentStepState(2);
- setPollingStep(2);
- } else if (step >= 3) {
- setCurrentStepState(4);
- setPollingStep(null);
- formControl.setValue(
- "wizardStatus",
- "You've executed the Setup Wizard. You may now navigate away from this wizard."
- );
- formControl.trigger();
- }
- }
- }, [checkSetupStatusApi.data, currentStepState]);
-
- const openPopup = (url) => {
- const width = 500;
- const height = 500;
- const left = window.screen.width / 2 - width / 2;
- const top = window.screen.height / 2 - height / 2;
- window.open(url, "_blank", `width=${width},height=${height},left=${left},top=${top}`);
- };
+ }, [values.selectedOption, formControl]);
+
return (
- {values.selectedOption === "CreateApp" && (
- <>
-
- To run this setup you will need the following prerequisites:
-
- A CIPP Service Account. For more information on how to create a service account,
- click{" "}
-
- here
-
-
- (Temporary) Global Administrator permissions for the CIPP Service Account
-
- Multi-factor authentication enabled for the CIPP Service Account, with no trusted
- locations or other exclusions.
-
-
- {currentStepState >= 1 && (
-
- Step 1: Create Application
-
- {currentStepState <= 1 ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- variant="outlined"
- isFetching={startSetupApi.isLoading}
- CardButton={
- = 2}
- variant="contained"
- color="primary"
- onClick={() => openPopup("https://microsoft.com/devicelogin")}
- >
- Login to Microsoft
-
- }
- >
-
- Click the button below and enter the provided code. This creates the CIPP
- Application Registration in your tenant that allows you to access the Graph API.
- Login using your CIPP Service Account.
-
- {startSetupApi.isLoading ? (
-
- ) : (
-
- )}
-
- )}
- {currentStepState >= 2 && (
-
- Step 2: Approve Permissions
-
- {currentStepState <= 2 ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- CardButton={
- = 4 ||
- !approvalUrl ||
- !approvalUrl.startsWith("https://login") ||
- typeof approvalUrl !== "string"
- }
- onClick={() => openPopup(approvalUrl)}
- >
- Open Approval Link
-
- }
- >
-
- Step 2: Approvals Required
-
-
- Please open the link below and provide the required approval, this allows the app
- specific permissions shown in the next screen. Login using your CIPP Service
- Account.
-
-
- )}
-
- {/* Final Step 4 Card */}
- {currentStepState >= 4 && }
-
- {
- setPollingStep(1);
- setCurrentStepState(1);
- setApprovalUrl(null);
- queryClient.removeQueries("startSAMSetup");
- queryClient.removeQueries("checkSetupStep1");
- setTimeout(() => {
- startSetupApi.refetch();
- }, 200);
- }}
- >
- Start Over
-
-
-
-
-
-
- >
- )}
-
{values.selectedOption === "UpdateTokens" && (
)}
{values.selectedOption === "Manual" && (
<>
- {formControl.setValue("setKeys", true)}
You may enter your secrets below. Leave fields blank to retain existing values.
@@ -278,7 +63,8 @@ export const CippDeploymentStep = (props) => {
placeholder="Enter the application secret. Leave blank to retain previous key."
validators={{
validate: (value) => {
- const secretRegex = /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/;
+ const secretRegex =
+ /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/;
return (
value === "" ||
secretRegex.test(value) ||
@@ -307,7 +93,6 @@ export const CippDeploymentStep = (props) => {
currentStep={currentStep}
onPreviousStep={onPreviousStep}
onNextStep={onNextStep}
- noNextButton={values.selectedOption === "UpdateTokens"}
formControl={formControl}
noSubmitButton={true}
/>
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index 0bdb5382bd6f..3d01eb4d5885 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -73,14 +73,12 @@ export const CippTenantModeDeploy = (props) => {
},
{
onSuccess: (data) => {
- console.log("Update Refresh Token Success:", data);
setGdapAuthStatus({
success: true,
loading: false,
});
},
onError: (error) => {
- console.error("Update Refresh Token Error:", error);
setGdapAuthStatus({
success: false,
loading: false,
@@ -103,7 +101,6 @@ export const CippTenantModeDeploy = (props) => {
// Add the tenant to the cache
// Call the AddTenant API to add the tenant to the cache with directTenant status
- console.log(tokenData);
addTenant.mutate({
url: "/api/ExecAddTenant",
data: {
diff --git a/src/components/CippWizard/CippWizard.jsx b/src/components/CippWizard/CippWizard.jsx
index f0cc5f3013b3..3a892548afad 100644
--- a/src/components/CippWizard/CippWizard.jsx
+++ b/src/components/CippWizard/CippWizard.jsx
@@ -2,10 +2,28 @@ import { useCallback, useMemo, useState } from "react";
import { Card, CardContent, Container, Stack } from "@mui/material";
import Grid from "@mui/material/Grid2";
import { WizardSteps } from "./wizard-steps";
-import { useForm } from "react-hook-form";
+import { useForm, useWatch } from "react-hook-form";
export const CippWizard = (props) => {
const { postUrl, orientation = "horizontal", steps } = props;
+
+ const formControl = useForm({ mode: "onChange", defaultValues: props.initialState });
+ const formWatcher = useWatch({
+ control: formControl.control,
+ });
+
+ const stepsWithVisibility = useMemo(() => {
+ return steps.filter((step) => {
+ if (step.hideStepWhen) {
+ return !step.hideStepWhen(formWatcher);
+ }
+ if (step.showStepWhen) {
+ return step.showStepWhen(formWatcher);
+ }
+ return true;
+ });
+ }, [steps, formWatcher]);
+
const [activeStep, setActiveStep] = useState(0);
const handleBack = useCallback(() => {
setActiveStep((prevState) => (prevState > 0 ? prevState - 1 : prevState));
@@ -14,25 +32,25 @@ export const CippWizard = (props) => {
const handleNext = useCallback(() => {
setActiveStep((prevState) => (prevState < steps.length - 1 ? prevState + 1 : prevState));
}, []);
- const formControl = useForm({ mode: "onChange", defaultValues: props.initialState });
+
const content = useMemo(() => {
- const StepComponent = steps[activeStep].component;
+ const StepComponent = stepsWithVisibility[activeStep].component;
return (
);
- }, [activeStep, handleNext, handleBack, steps, formControl]);
+ }, [activeStep, handleNext, handleBack, stepsWithVisibility, formControl]);
return (
@@ -44,7 +62,7 @@ export const CippWizard = (props) => {
postUrl={postUrl}
activeStep={activeStep}
orientation={orientation}
- steps={steps}
+ steps={stepsWithVisibility}
/>
@@ -59,7 +77,7 @@ export const CippWizard = (props) => {
postUrl={postUrl}
activeStep={activeStep}
orientation={orientation}
- steps={steps}
+ steps={stepsWithVisibility}
/>
{content}
diff --git a/src/components/CippWizard/CippWizardStepButtons.jsx b/src/components/CippWizard/CippWizardStepButtons.jsx
index 7f5b0b264bc4..fd80d7c872fa 100644
--- a/src/components/CippWizard/CippWizardStepButtons.jsx
+++ b/src/components/CippWizard/CippWizardStepButtons.jsx
@@ -24,10 +24,9 @@ export const CippWizardStepButtons = (props) => {
const newData = {};
Object.keys(values).forEach((key) => {
const value = values[key];
- if (replacementBehaviour !== "removeNulls") {
+ // Only add non-null values if removeNulls is specified
+ if (replacementBehaviour !== "removeNulls" || value !== null) {
newData[key] = value;
- } else if (row[value] !== undefined) {
- newData[key] = row[value];
}
});
sendForm.mutate({ url: postUrl, data: newData });
diff --git a/src/components/CippWizard/wizard-steps.js b/src/components/CippWizard/wizard-steps.js
index 70c2d1e910e2..61e1c84d572c 100644
--- a/src/components/CippWizard/wizard-steps.js
+++ b/src/components/CippWizard/wizard-steps.js
@@ -127,7 +127,9 @@ export const WizardSteps = (props) => {
{steps.map((step) => (
- {step.title}
+
+ {`Step ${steps.indexOf(step) ? steps.indexOf(step) + 1 : 1}`}
+
{step.description}
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index beb89ff5812c..ee7f73445035 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -13,7 +13,6 @@ import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/
const Page = () => {
const steps = [
{
- title: "Step 1",
description: "Onboarding",
component: CippWizardOptionsList,
componentProps: {
@@ -59,32 +58,45 @@ const Page = () => {
},
},
{
- title: "Step 2",
description: "Application",
component: CippSAMDeploy,
+ showStepWhen: (values) =>
+ values?.selectedOption === "CreateApp" || values?.selectedOption === "FirstSetup",
},
{
- title: "Step 3",
description: "Tenants",
component: CippTenantModeDeploy,
+ showStepWhen: (values) =>
+ values?.selectedOption === "CreateApp" ||
+ values?.selectedOption === "FirstSetup" ||
+ values?.selectedOption === "AddTenant",
},
{
- title: "Step 4",
description: "Baselines",
component: CippBaselinesStep,
+ showStepWhen: (values) => values?.selectedOption === "FirstSetup",
},
{
- title: "Step 5",
description: "Notifications",
component: CippNotificationsStep,
+ showStepWhen: (values) => values?.selectedOption === "FirstSetup",
},
{
- title: "Step 6",
description: "Next Steps",
component: CippAlertsStep,
+ showStepWhen: (values) => values?.selectedOption === "FirstSetup",
+ },
+ {
+ description: "Refresh Tokens",
+ component: CippDeploymentStep,
+ showStepWhen: (values) => values?.selectedOption === "UpdateTokens",
+ },
+ {
+ description: "Manually enter credentials",
+ component: CippDeploymentStep,
+ showStepWhen: (values) => values?.selectedOption === "Manual",
},
{
- title: "Step 7",
description: "Confirmation",
component: CippWizardConfirmation,
//confirm and finish button, perform tasks, launch checks etc.
From 04cdbe772d867b366b3348e358137d7a8f64cc21 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 26 May 2025 21:55:20 +0200
Subject: [PATCH 075/865] push
---
.../CippWizard/CIPPDeploymentUpdateTokens.jsx | 35 +----------
.../CippWizard/CippWizardConfirmation.jsx | 63 ++++++++++++-------
src/layouts/config.js | 2 +-
3 files changed, 41 insertions(+), 59 deletions(-)
diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
index 6bfa70481719..1f9782a00955 100644
--- a/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
+++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
@@ -7,7 +7,6 @@ import { CippApiResults } from "../CippComponents/CippApiResults";
import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
- const values = formControl.getValues();
const [tokens, setTokens] = useState(null);
// Get application ID information for the card header
@@ -29,7 +28,7 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
variant="outlined"
title={
- Update Tokens (MSAL Style)
+ Update Tokens
{appId.isLoading ? (
@@ -52,40 +51,8 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
Click the button to refresh the Graph token for your tenants using popup authentication.
This method opens a popup window where you can sign in to your Microsoft account.
- {formControl.setValue("noSubmitButton", true)}
-
-
- Update Tokens (Device Code Flow)
-
- {appId.isLoading ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- CardButton={
-
- }
- >
-
- Device code flow test
-
-
);
};
diff --git a/src/components/CippWizard/CippWizardConfirmation.jsx b/src/components/CippWizard/CippWizardConfirmation.jsx
index 3e8ae6c78bff..78877fc3b0bb 100644
--- a/src/components/CippWizard/CippWizardConfirmation.jsx
+++ b/src/components/CippWizard/CippWizardConfirmation.jsx
@@ -1,7 +1,7 @@
-import { Card, Stack, Grid } from "@mui/material";
+import { Card, Stack, Grid, Typography } from "@mui/material";
import { PropertyList } from "../property-list";
-import CippWizardStepButtons from "./CippWizardStepButtons";
import { PropertyListItem } from "../property-list-item";
+import CippWizardStepButtons from "./CippWizardStepButtons";
import { getCippTranslation } from "../../utils/get-cipp-translation";
import { getCippFormatting } from "../../utils/get-cipp-formatting";
@@ -9,7 +9,7 @@ export const CippWizardConfirmation = (props) => {
const { postUrl, lastStep, formControl, onPreviousStep, onNextStep, currentStep } = props;
const formValues = formControl.getValues();
const formEntries = Object.entries(formValues);
- //remove all entries in "blacklist" from showing on confirmation page
+
const blacklist = [
"selectedOption",
"GUID",
@@ -24,6 +24,7 @@ export const CippWizardConfirmation = (props) => {
const userEntry = formEntries.find(([key]) =>
["user", "userPrincipalName", "username"].includes(key)
);
+
const filteredEntries = formEntries.filter(
([key]) =>
!blacklist.includes(key) &&
@@ -46,28 +47,42 @@ export const CippWizardConfirmation = (props) => {
return (
-
-
-
-
- {firstHalf.map(([key, value]) => {
- const formattedValue = getCippFormatting(value, key);
- const label = getCippTranslation(key);
- return ;
- })}
-
-
-
-
- {secondHalf.map(([key, value]) => {
- const formattedValue = getCippFormatting(value, key);
- const label = getCippTranslation(key);
- return ;
- })}
-
+ {firstHalf.length === 0 ? (
+
+
+
+ You've completed the steps in this wizard. Hit submit to save your changes.
+
+
+
+ ) : (
+
+
+
+
+ {firstHalf.map(([key, value]) => (
+
+ ))}
+
+
+
+
+ {secondHalf.map(([key, value]) => (
+
+ ))}
+
+
-
-
+
+ )}
Date: Mon, 26 May 2025 22:06:45 +0200
Subject: [PATCH 076/865] Update CippNotificationForm.jsx
---
src/components/CippComponents/CippNotificationForm.jsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/components/CippComponents/CippNotificationForm.jsx b/src/components/CippComponents/CippNotificationForm.jsx
index 49819df91ce9..d9189c226855 100644
--- a/src/components/CippComponents/CippNotificationForm.jsx
+++ b/src/components/CippComponents/CippNotificationForm.jsx
@@ -162,6 +162,11 @@ export const CippNotificationForm = ({
name: "sendWebhookNow",
label: "Send Webhook Now",
},
+ {
+ type: "switch",
+ name: "sendPsaNow",
+ label: "Send to PSA Now",
+ },
]}
api={{
confirmText:
From bbe7bb5f8fdc70e5a9e130b3c281e9b264854e01 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 26 May 2025 22:54:14 +0200
Subject: [PATCH 077/865] fixes displayname bug
---
.../CippComponents/CippAutocomplete.jsx | 6 +-
.../CippWizard/CippWizardGroupTemplates.jsx | 3 +-
src/data/standards.json | 398 ++++--------------
3 files changed, 98 insertions(+), 309 deletions(-)
diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx
index 4edb340549ba..9b95fb101bdb 100644
--- a/src/components/CippComponents/CippAutocomplete.jsx
+++ b/src/components/CippComponents/CippAutocomplete.jsx
@@ -158,7 +158,11 @@ export const CippAutoComplete = (props) => {
label:
typeof api?.labelField === "function"
? api.labelField(option)
- : option[api?.labelField],
+ : option[api?.labelField]
+ ? option[api?.labelField]
+ : option[api?.altLabelField] ||
+ option[api?.valueField] ||
+ "No label found - Are you missing a labelField?",
value:
typeof api?.valueField === "function"
? api.valueField(option)
diff --git a/src/components/CippWizard/CippWizardGroupTemplates.jsx b/src/components/CippWizard/CippWizardGroupTemplates.jsx
index 215d22a0509f..2ac2d6435cd6 100644
--- a/src/components/CippWizard/CippWizardGroupTemplates.jsx
+++ b/src/components/CippWizard/CippWizardGroupTemplates.jsx
@@ -42,7 +42,8 @@ export const CippWizardGroupTemplates = (props) => {
excludeTenantFilter: true,
url: "/api/ListGroupTemplates",
queryKey: "ListGroupTemplates",
- labelField: (option) => `${option.Displayname} (${option.groupType})`,
+ labelField: (option) =>
+ `${option.Displayname || option.displayName} (${option.groupType})`,
valueField: "GUID",
addedField: {
groupType: "groupType",
diff --git a/src/data/standards.json b/src/data/standards.json
index dbcf12a132a6..4dba29061348 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -41,10 +41,7 @@
{
"name": "standards.AuditLog",
"cat": "Global Standards",
- "tag": [
- "CIS",
- "mip_search_auditlog"
- ],
+ "tag": ["CIS", "mip_search_auditlog"],
"helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.",
"addedComponent": [],
"label": "Enable the Unified Audit Log",
@@ -52,10 +49,7 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Enable-OrganizationCustomization",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.ProfilePhotos",
@@ -105,9 +99,7 @@
"remediate": false
},
"powershellEquivalent": "Portal only",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.Branding",
@@ -169,10 +161,7 @@
{
"name": "standards.EnableCustomerLockbox",
"cat": "Global Standards",
- "tag": [
- "CIS",
- "CustomerLockBoxEnabled"
- ],
+ "tag": ["CIS", "CustomerLockBoxEnabled"],
"helpText": "Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data",
"docsDescription": "Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.",
"addedComponent": [],
@@ -181,9 +170,7 @@
"impactColour": "info",
"addedDate": "2024-01-08",
"powershellEquivalent": "Set-OrganizationConfig -CustomerLockBoxEnabled $true",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.EnablePronouns",
@@ -210,9 +197,7 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgBetaAdminReportSetting -BodyParameter @{displayConcealedNames = $true}",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.DisableGuestDirectory",
@@ -226,9 +211,7 @@
"impactColour": "info",
"addedDate": "2022-05-04",
"powershellEquivalent": "Set-AzureADMSAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa82404023b'",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.DisableBasicAuthSMTP",
@@ -242,18 +225,12 @@
"impactColour": "warning",
"addedDate": "2021-11-16",
"powershellEquivalent": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.ActivityBasedTimeout",
"cat": "Global Standards",
- "tag": [
- "CIS",
- "spo_idle_session_timeout"
- ],
+ "tag": ["CIS", "spo_idle_session_timeout"],
"helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps",
"addedComponent": [
{
@@ -291,9 +268,7 @@
"impactColour": "warning",
"addedDate": "2022-04-13",
"powershellEquivalent": "Portal or Graph API",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.AuthMethodsSettings",
@@ -429,16 +404,12 @@
"impactColour": "info",
"addedDate": "2023-04-25",
"powershellEquivalent": "Portal or Graph API",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.PWdisplayAppInformationRequiredState",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.",
"docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request",
"addedComponent": [],
@@ -447,9 +418,7 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.allowOTPTokens",
@@ -509,9 +478,7 @@
"impactColour": "info",
"addedDate": "2022-12-08",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.EnableHardwareOAuth",
@@ -571,17 +538,12 @@
"impactColour": "info",
"addedDate": "2022-03-15",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.PasswordExpireDisabled",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS",
- "PWAgePolicyNew"
- ],
+ "tag": ["CIS", "PWAgePolicyNew"],
"helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.",
"docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.",
"addedComponent": [],
@@ -590,10 +552,7 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgDomain",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.ExternalMFATrusted",
@@ -629,9 +588,7 @@
{
"name": "standards.DisableTenantCreation",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.",
"docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.",
"addedComponent": [],
@@ -640,17 +597,12 @@
"impactColour": "info",
"addedDate": "2022-11-29",
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.EnableAppConsentRequests",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings",
"docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards",
"addedComponent": [
@@ -665,9 +617,7 @@
"impactColour": "info",
"addedDate": "2023-11-27",
"powershellEquivalent": "Update-MgPolicyAdminConsentRequestPolicy",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.NudgeMFA",
@@ -724,9 +674,7 @@
{
"name": "standards.DisableAppCreation",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Disables the ability for users to create App registrations in the tenant.",
"docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.",
"addedComponent": [],
@@ -735,10 +683,7 @@
"impactColour": "info",
"addedDate": "2024-03-20",
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.DisableSecurityGroupUsers",
@@ -797,17 +742,12 @@
"impactColour": "warning",
"addedDate": "2022-10-20",
"powershellEquivalent": "Graph API",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.OauthConsent",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Disables users from being able to consent to applications, except for those specified in the field below",
"docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.",
"addedComponent": [
@@ -823,17 +763,12 @@
"impactColour": "warning",
"addedDate": "2021-11-16",
"powershellEquivalent": "Update-MgPolicyAuthorizationPolicy",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.OauthConsentLowSec",
"cat": "Entra (AAD) Standards",
- "tag": [
- "IntegratedApps"
- ],
+ "tag": ["IntegratedApps"],
"helpText": "Sets the default oauth consent level so users can consent to applications that have low risks.",
"docsDescription": "Allows users to consent to applications with low assigned risk.",
"label": "Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure)",
@@ -886,9 +821,7 @@
{
"name": "standards.StaleEntraDevices",
"cat": "Entra (AAD) Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days.",
"docsDescription": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)",
"addedComponent": [
@@ -949,9 +882,7 @@
"impactColour": "danger",
"addedDate": "2023-12-18",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.DisableVoice",
@@ -965,9 +896,7 @@
"impactColour": "danger",
"addedDate": "2023-12-18",
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.DisableEmail",
@@ -1053,9 +982,7 @@
{
"name": "standards.OutBoundSpamAlert",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Set the Outbound Spam Alert e-mail address",
"docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.",
"addedComponent": [
@@ -1070,9 +997,7 @@
"impactColour": "info",
"addedDate": "2023-05-03",
"powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.MessageExpiration",
@@ -1135,9 +1060,7 @@
"impactColour": "info",
"addedDate": "2024-04-26",
"powershellEquivalent": "Set-RemoteDomain -Identity 'Default' -TNEFEnabled $false",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.FocusedInbox",
@@ -1251,9 +1174,7 @@
{
"name": "standards.SpoofWarn",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA",
"docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)",
"addedComponent": [
@@ -1287,18 +1208,12 @@
"impactColour": "info",
"addedDate": "2021-11-16",
"powershellEquivalent": "Set-ExternalInOutlook \u2013Enabled $true or $false",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.EnableMailTips",
"cat": "Exchange Standards",
- "tag": [
- "CIS",
- "exo_mailtipsenabled"
- ],
+ "tag": ["CIS", "exo_mailtipsenabled"],
"helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements",
"addedComponent": [
{
@@ -1314,10 +1229,7 @@
"impactColour": "info",
"addedDate": "2024-01-14",
"powershellEquivalent": "Set-OrganizationConfig",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.TeamsMeetingsByDefault",
@@ -1367,9 +1279,7 @@
{
"name": "standards.RotateDKIM",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit",
"addedComponent": [],
"label": "Rotate DKIM keys that are 1024 bit to 2048 bit",
@@ -1377,17 +1287,12 @@
"impactColour": "info",
"addedDate": "2023-03-14",
"powershellEquivalent": "Rotate-DkimSigningConfig",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.AddDKIM",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Enables DKIM for all domains that currently support it",
"addedComponent": [],
"label": "Enables DKIM for all domains that currently support it",
@@ -1395,18 +1300,12 @@
"impactColour": "info",
"addedDate": "2023-03-14",
"powershellEquivalent": "New-DkimSigningConfig and Set-DkimSigningConfig",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.EnableMailboxAuditing",
"cat": "Exchange Standards",
- "tag": [
- "CIS",
- "exo_mailboxaudit"
- ],
+ "tag": ["CIS", "exo_mailboxaudit"],
"helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.",
"docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.",
"addedComponent": [],
@@ -1415,10 +1314,7 @@
"impactColour": "info",
"addedDate": "2024-01-08",
"powershellEquivalent": "Set-OrganizationConfig -AuditDisabled $false",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.SendReceiveLimitTenant",
@@ -1521,9 +1417,7 @@
{
"name": "standards.EXOOutboundSpamLimits",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ",
"docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.",
"addedComponent": [
@@ -1572,18 +1466,12 @@
"impactColour": "info",
"addedDate": "2025-05-13",
"powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy",
- "recommendedBy": [
- "CIPP",
- "CIS"
- ]
+ "recommendedBy": ["CIPP", "CIS"]
},
{
"name": "standards.DisableExternalCalendarSharing",
"cat": "Exchange Standards",
- "tag": [
- "CIS",
- "exo_individualsharing"
- ],
+ "tag": ["CIS", "exo_individualsharing"],
"helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.",
"docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.",
"addedComponent": [],
@@ -1592,16 +1480,12 @@
"impactColour": "info",
"addedDate": "2024-01-08",
"powershellEquivalent": "Get-SharingPolicy | Set-SharingPolicy -Enabled $False",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.AutoAddProxy",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Automatically adds all available domains as a proxy address.",
"docsDescription": "Automatically finds all available domain names in the tenant, and tries to add proxy addresses based on the user's UPN to each of these.",
"addedComponent": [],
@@ -1620,10 +1504,7 @@
{
"name": "standards.DisableAdditionalStorageProviders",
"cat": "Exchange Standards",
- "tag": [
- "CIS",
- "exo_storageproviderrestricted"
- ],
+ "tag": ["CIS", "exo_storageproviderrestricted"],
"helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.",
"docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.",
"addedComponent": [],
@@ -1632,9 +1513,7 @@
"impactColour": "info",
"addedDate": "2024-01-17",
"powershellEquivalent": "Get-OwaMailboxPolicy | Set-OwaMailboxPolicy -AdditionalStorageProvidersEnabled $False",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.AntiSpamSafeList",
@@ -1736,10 +1615,7 @@
{
"name": "standards.DisableOutlookAddins",
"cat": "Exchange Standards",
- "tag": [
- "CIS",
- "exo_outlookaddins"
- ],
+ "tag": ["CIS", "exo_outlookaddins"],
"helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.",
"docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.",
"addedComponent": [],
@@ -1748,9 +1624,7 @@
"impactColour": "warning",
"addedDate": "2024-02-05",
"powershellEquivalent": "Get-ManagementRoleAssignment | Remove-ManagementRoleAssignment",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.SafeSendersDisable",
@@ -1768,9 +1642,7 @@
"impactColour": "warning",
"addedDate": "2023-10-26",
"powershellEquivalent": "Set-MailboxJunkEmailConfiguration",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.DelegateSentItems",
@@ -1804,9 +1676,7 @@
"impactColour": "warning",
"addedDate": "2022-05-25",
"powershellEquivalent": "Set-Mailbox",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.UserSubmissions",
@@ -1848,9 +1718,7 @@
{
"name": "standards.DisableSharedMailbox",
"cat": "Exchange Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.",
"docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.",
"addedComponent": [],
@@ -1859,19 +1727,12 @@
"impactColour": "warning",
"addedDate": "2021-11-16",
"powershellEquivalent": "Get-Mailbox & Update-MgUser",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.EXODisableAutoForwarding",
"cat": "Exchange Standards",
- "tag": [
- "CIS",
- "mdo_autoforwardingmode",
- "mdo_blockmailforward"
- ],
+ "tag": ["CIS", "mdo_autoforwardingmode", "mdo_blockmailforward"],
"helpText": "Disables the ability for users to automatically forward e-mails to external recipients.",
"docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.",
"addedComponent": [],
@@ -1880,10 +1741,7 @@
"impactColour": "danger",
"addedDate": "2024-07-26",
"powershellEquivalent": "Set-HostedOutboundSpamFilterPolicy -AutoForwardingMode 'Off'",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.RetentionPolicyTag",
@@ -1964,11 +1822,7 @@
{
"name": "standards.SafeLinksPolicy",
"cat": "Defender Standards",
- "tag": [
- "CIS",
- "mdo_safelinksforemail",
- "mdo_safelinksforOfficeApps"
- ],
+ "tag": ["CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"],
"helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders",
"addedComponent": [
{
@@ -2000,9 +1854,7 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-SafeLinksPolicy or New-SafeLinksPolicy",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.AntiPhishPolicy",
@@ -2215,19 +2067,12 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-AntiPhishPolicy or New-AntiPhishPolicy",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.SafeAttachmentPolicy",
"cat": "Defender Standards",
- "tag": [
- "CIS",
- "mdo_safedocuments",
- "mdo_commonattachmentsfilter",
- "mdo_safeattachmentpolicy"
- ],
+ "tag": ["CIS", "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy"],
"helpText": "This creates a Safe Attachment policy",
"addedComponent": [
{
@@ -2293,16 +2138,12 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-SafeAttachmentPolicy or New-SafeAttachmentPolicy",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.AtpPolicyForO365",
"cat": "Defender Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.",
"addedComponent": [
{
@@ -2318,9 +2159,7 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-AtpPolicyForO365",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.PhishingSimulations",
@@ -2370,12 +2209,7 @@
{
"name": "standards.MalwareFilterPolicy",
"cat": "Defender Standards",
- "tag": [
- "CIS",
- "mdo_zapspam",
- "mdo_zapphish",
- "mdo_zapmalware"
- ],
+ "tag": ["CIS", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware"],
"helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.",
"addedComponent": [
{
@@ -2461,9 +2295,7 @@
"impactColour": "info",
"addedDate": "2024-03-25",
"powershellEquivalent": "Set-MalwareFilterPolicy or New-MalwareFilterPolicy",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.PhishSimSpoofIntelligence",
@@ -2790,7 +2622,7 @@
"powershellEquivalent": "New-HostedContentFilterPolicy or Set-HostedContentFilterPolicy",
"recommendedBy": []
},
- {
+ {
"name": "standards.QuarantineTemplate",
"cat": "Defender Standards",
"disabledFeatures": {
@@ -2893,9 +2725,7 @@
"impactColour": "info",
"addedDate": "2023-05-19",
"powershellEquivalent": "Graph API",
- "recommendedBy": [
- "CIPP"
- ]
+ "recommendedBy": ["CIPP"]
},
{
"name": "standards.intuneBrandingProfile",
@@ -3246,9 +3076,7 @@
{
"name": "standards.SPAzureB2B",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled",
"addedComponent": [],
"label": "Enable SharePoint and OneDrive integration with Azure AD B2B",
@@ -3256,16 +3084,12 @@
"impactColour": "info",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -EnableAzureADB2BIntegration $true",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.SPDisallowInfectedFiles",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Ensure Office 365 SharePoint infected files are disallowed for download",
"addedComponent": [],
"label": "Disallow downloading infected files from SharePoint",
@@ -3273,10 +3097,7 @@
"impactColour": "info",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -DisallowInfectedFileDownload $true",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.SPDisableLegacyWorkflows",
@@ -3294,9 +3115,7 @@
{
"name": "standards.SPDirectSharing",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Ensure default link sharing is set to Direct in SharePoint and OneDrive",
"addedComponent": [],
"label": "Default sharing to Direct users",
@@ -3304,17 +3123,12 @@
"impactColour": "warning",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType Direct",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.SPExternalUserExpiration",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Ensure guest access to a site or OneDrive will expire automatically",
"addedComponent": [
{
@@ -3328,16 +3142,12 @@
"impactColour": "warning",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -ExternalUserExpireInDays 30 -ExternalUserExpirationRequired $True",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.SPEmailAttestation",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Ensure re-authentication with verification code is restricted",
"addedComponent": [
{
@@ -3351,10 +3161,7 @@
"impactColour": "warning",
"addedDate": "2024-07-09",
"powershellEquivalent": "Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.DisableAddShortcutsToOneDrive",
@@ -3421,10 +3228,7 @@
{
"name": "standards.DisableSharePointLegacyAuth",
"cat": "SharePoint Standards",
- "tag": [
- "CIS",
- "spo_legacy_auth"
- ],
+ "tag": ["CIS", "spo_legacy_auth"],
"helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.",
"docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.",
"addedComponent": [],
@@ -3433,17 +3237,12 @@
"impactColour": "warning",
"addedDate": "2024-02-05",
"powershellEquivalent": "Set-SPOTenant -LegacyAuthProtocolsEnabled $false",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.sharingCapability",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level",
"addedComponent": [
{
@@ -3476,17 +3275,12 @@
"impactColour": "danger",
"addedDate": "2022-06-15",
"powershellEquivalent": "Update-MgBetaAdminSharePointSetting",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.DisableReshare",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access",
"docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level",
"addedComponent": [],
@@ -3495,10 +3289,7 @@
"impactColour": "danger",
"addedDate": "2022-06-15",
"powershellEquivalent": "Update-MgBetaAdminSharePointSetting",
- "recommendedBy": [
- "CIS",
- "CIPP"
- ]
+ "recommendedBy": ["CIS", "CIPP"]
},
{
"name": "standards.DisableUserSiteCreate",
@@ -3562,9 +3353,7 @@
{
"name": "standards.sharingDomainRestriction",
"cat": "SharePoint Standards",
- "tag": [
- "CIS"
- ],
+ "tag": ["CIS"],
"helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.",
"addedComponent": [
{
@@ -3671,9 +3460,7 @@
"impactColour": "info",
"addedDate": "2024-11-12",
"powershellEquivalent": "Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting $false -AllowAnonymousUsersToStartMeeting $false -AutoAdmittedUsers EveryoneInCompanyExcludingGuests -AllowPSTNUsersToBypassLobby $false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode $DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl $false",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.TeamsEmailIntegration",
@@ -3693,9 +3480,7 @@
"impactColour": "info",
"addedDate": "2024-07-30",
"powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.TeamsExternalFileSharing",
@@ -3734,9 +3519,7 @@
"impactColour": "info",
"addedDate": "2024-07-28",
"powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGoogleDrive $false -AllowShareFile $false -AllowBox $false -AllowDropBox $false -AllowEgnyte $false",
- "recommendedBy": [
- "CIS"
- ]
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.TeamsEnrollUser",
@@ -4320,10 +4103,11 @@
"api": {
"url": "/api/ListGroupTemplates",
"labelField": "Displayname",
+ "altLabelField": "displayName",
"valueField": "GUID",
"queryKey": "ListGroupTemplates"
}
}
]
}
-]
\ No newline at end of file
+]
From 9d764d7d3f8fe99c7ce00e726d456e899e3da55a Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Mon, 26 May 2025 23:57:12 +0200
Subject: [PATCH 078/865] frontend fix for new gdap wizard
---
.../CippComponents/CIPPM365OAuthButton.jsx | 54 -------------------
1 file changed, 54 deletions(-)
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx
index 83eae0dc7ebe..29265aed2d1c 100644
--- a/src/components/CippComponents/CIPPM365OAuthButton.jsx
+++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx
@@ -144,16 +144,6 @@ export const CIPPM365OAuthButton = ({
const pollForToken = async () => {
// Check if popup was closed
- if (popup && popup.closed) {
- clearInterval(checkPopupClosed);
- setAuthError({
- errorCode: "user_cancelled",
- errorMessage: "Authentication was cancelled. Please try again.",
- timestamp: new Date().toISOString(),
- });
- setAuthInProgress(false);
- return;
- }
// Check if we've exceeded the expiration time
if (Date.now() - startTime >= expiresIn * 1000) {
@@ -208,19 +198,6 @@ export const CIPPM365OAuthButton = ({
}
};
- // Also monitor for popup closing as a fallback
- const checkPopupClosed = setInterval(() => {
- if (popup && popup.closed) {
- clearInterval(checkPopupClosed);
- setAuthInProgress(false);
- setAuthError({
- errorCode: "user_cancelled",
- errorMessage: "Authentication was cancelled. Please try again.",
- timestamp: new Date().toISOString(),
- });
- }
- }, 1000);
-
// Start polling
setTimeout(pollForToken, pollInterval * 1000);
} catch (error) {
@@ -555,37 +532,6 @@ export const CIPPM365OAuthButton = ({
}, 500);
// Also monitor for popup closing as a fallback
- const checkPopupClosed = setInterval(() => {
- if (popup.closed) {
- clearInterval(checkPopupClosed);
- clearInterval(checkPopupLocation);
-
- // If authentication is still in progress when popup closes, it's an error
- if (authInProgress) {
- const errorMessage = "Authentication was cancelled. Please try again.";
- const error = {
- errorCode: "user_cancelled",
- errorMessage: errorMessage,
- timestamp: new Date().toISOString(),
- };
- setAuthError(error);
- if (onAuthError) onAuthError(error);
-
- // Ensure we're not showing any previous success state
- setTokens({
- accessToken: null,
- refreshToken: null,
- accessTokenExpiresOn: null,
- refreshTokenExpiresOn: null,
- username: null,
- tenantId: null,
- onmicrosoftDomain: null,
- });
- }
-
- setAuthInProgress(false);
- }
- }, 1000);
};
// Auto-start device code retrieval if requested
From 034074fdc03f73ccf1608445d4a4a11212df8137 Mon Sep 17 00:00:00 2001
From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
Date: Tue, 27 May 2025 00:21:07 +0200
Subject: [PATCH 079/865] test
---
staticwebapp.config.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/staticwebapp.config.json b/staticwebapp.config.json
index a19bc9f9580c..1f57342751ca 100644
--- a/staticwebapp.config.json
+++ b/staticwebapp.config.json
@@ -35,8 +35,8 @@
"redirect": "/.auth/logout?post_logout_redirect_uri=/LogoutRedirect"
},
{
- "route": "/api/ExecSAMSetup",
- "allowedRoles": ["authenticated", "anonymous"]
+ "route": "/authredirect",
+ "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"]
},
{
"route": "/LogoutRedirect",
From 996f12adc4c8048fd9bf389407fc895e49a73d5f Mon Sep 17 00:00:00 2001
From: Zac Richards <107489668+Zacgoose@users.noreply.github.com>
Date: Tue, 27 May 2025 10:01:28 +0800
Subject: [PATCH 080/865] Grid prop cleanup - round 1
---
src/components/CippCards/CippDomainCards.jsx | 18 +++---
src/components/CippCards/CippInfoBar.jsx | 2 +-
.../CippCards/CippUniversalSearch.jsx | 2 +-
.../CippComponents/CippCentralSearch.jsx | 2 +-
.../CippFormPages/CippAddEditUser.jsx | 28 ++++-----
.../CippFormPages/CippAddGroupForm.jsx | 8 +--
.../CippAddGroupTemplateForm.jsx | 6 +-
.../CippFormPages/CippInviteGuest.jsx | 8 +--
.../CippFormPages/CippSchedulerForm.jsx | 20 +++----
.../CippIntegrationTenantMapping.jsx | 4 +-
.../CippSettings/CippCustomRoles.jsx | 4 +-
.../CippSettings/CippRoleAddEdit.jsx | 4 +-
.../CippStandards/CippStandardDialog.jsx | 2 +-
src/components/CippWizard/CippWizard.jsx | 6 +-
.../CippWizard/CippWizardAutopilotOptions.jsx | 2 +-
.../CippWizard/CippWizardBulkOptions.jsx | 2 +-
.../CippWizard/CippWizardCSVImport.jsx | 4 +-
.../CippWizard/CippWizardConfirmation.jsx | 4 +-
.../CippWizard/CippWizardOffboarding.jsx | 4 +-
src/components/CippWizard/CustomerForm.jsx | 2 +-
src/pages/401.js | 2 +-
src/pages/404.js | 2 +-
src/pages/500.js | 2 +-
src/pages/authredirect.js | 2 +-
src/pages/cipp/advanced/exchange-cmdlets.js | 6 +-
src/pages/cipp/integrations/index.js | 2 +-
src/pages/cipp/logs/index.js | 2 +-
src/pages/cipp/preferences.js | 2 +-
src/pages/cipp/settings/backend.js | 2 +-
src/pages/cipp/settings/index.js | 10 ++--
src/pages/cipp/settings/partner-webhooks.js | 10 ++--
src/pages/cipp/settings/permissions.js | 6 +-
.../cipp/super-admin/function-offloading.js | 2 +-
src/pages/cipp/super-admin/tenant-mode.js | 4 +-
.../email/administration/contacts/add.jsx | 10 ++--
.../email/administration/contacts/edit.jsx | 26 ++++-----
.../administration/mailboxes/addshared.jsx | 6 +-
.../tenant-allow-block-lists/add.jsx | 12 ++--
.../resources/management/list-rooms/add.jsx | 6 +-
.../resources/management/list-rooms/edit.jsx | 58 +++++++++----------
.../spamfilter/list-connectionfilter/add.jsx | 2 +-
.../email/spamfilter/list-spamfilter/add.jsx | 2 +-
.../email/transport/list-connectors/add.jsx | 2 +-
src/pages/email/transport/list-rules/add.jsx | 2 +-
src/pages/endpoint/applications/list/add.jsx | 38 ++++++------
.../autopilot/add-status-page/index.js | 2 +-
src/pages/fullPageLoading.js | 2 +-
.../identity/administration/groups/edit.jsx | 2 +-
.../identity/administration/jit-admin/add.jsx | 30 +++++-----
.../administration/users/user/bec.jsx | 2 +-
.../users/user/conditional-access.jsx | 6 +-
.../administration/users/user/devices.jsx | 2 +-
.../administration/users/user/exchange.jsx | 2 +-
.../administration/users/user/index.jsx | 2 +-
.../identity/reports/signin-report/index.js | 8 +--
src/pages/index.js | 18 +++---
src/pages/loading.js | 2 +-
.../security/defender/deployment/index.js | 12 ++--
src/pages/teams-share/teams/list-team/add.jsx | 4 +-
.../administration/securescore/index.js | 8 +--
.../administration/securescore/table.js | 2 +-
.../tenant/administration/tenants/edit.js | 4 +-
.../administration/tenants/groups/add.js | 2 +-
.../administration/tenants/groups/edit.js | 2 +-
src/pages/tenant/backup/backup-wizard/add.jsx | 26 ++++-----
.../tenant/backup/backup-wizard/restore.jsx | 16 ++---
.../conditional/deploy-vacation/add.jsx | 4 +-
src/pages/tenant/gdap-management/index.js | 2 +-
.../tenant/gdap-management/invites/add.js | 2 +-
.../gdap-management/onboarding/start.js | 2 +-
src/pages/tenant/gdap-management/roles/add.js | 6 +-
.../tenant/standards/bpa-report/builder.js | 6 +-
src/pages/tenant/standards/bpa-report/view.js | 4 +-
.../tenant/tools/graph-explorer/index.js | 2 +-
src/pages/tools/templatelib/index.jsx | 8 +--
src/pages/unauthenticated.js | 2 +-
src/sections/dashboard/account/account-2fa.js | 6 +-
.../dashboard/account/account-details.js | 6 +-
.../dashboard/account/account-password.js | 6 +-
.../dashboard/components/onboarding/wizard.js | 6 +-
.../dashboard/components/stats/stats-1.js | 4 +-
81 files changed, 285 insertions(+), 285 deletions(-)
diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx
index 03fd08306afc..23298ce4864e 100644
--- a/src/components/CippCards/CippDomainCards.jsx
+++ b/src/components/CippCards/CippDomainCards.jsx
@@ -563,7 +563,7 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false })
{domain && (
<>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
{enableHttps && (
-
+
{
{displayedResults.map((item, key) => (
-
+
))}
diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx
index a3a8c214b5af..d12ca1075159 100644
--- a/src/components/CippComponents/CippCentralSearch.jsx
+++ b/src/components/CippComponents/CippCentralSearch.jsx
@@ -111,7 +111,7 @@ export const CippCentralSearch = ({ handleClose, open }) => {
filteredItems.length > 0 ? (
{filteredItems.map((item, index) => (
-
+
handleCardClick(item.path)}
diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx
index 8912c6a4f807..c39510851702 100644
--- a/src/components/CippFormPages/CippAddEditUser.jsx
+++ b/src/components/CippFormPages/CippAddEditUser.jsx
@@ -5,7 +5,7 @@ import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormD
import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector";
import countryList from "/src/data/countryList.json";
import { CippFormLicenseSelector } from "/src/components/CippComponents/CippFormLicenseSelector";
-import Grid from "@mui/material/Grid";
+import { Grid } from "@mui/system";
import { ApiGetCall } from "../../api/ApiCall";
import { useSettings } from "../../hooks/use-settings";
import { useWatch } from "react-hook-form";
@@ -31,7 +31,7 @@ const CippAddEditUser = (props) => {
return (
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
return (
-
+
{
fullWidth
/>
-
+
{
fullWidth
/>
-
+
{
}}
/>
-
+
{
{/* Hidden field to store the template GUID when editing */}
-
+
{
fullWidth
/>
-
+
{
fullWidth
/>
-
+
{
return (
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
formControl={formControl}
/>
-
+
{
{(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && (
)}
-
+
{
/>
-
+
{
/>
-
+
{
}}
/>
-
+
{
}}
/>
-
+
{
/>
{selectedCommand?.addedFields?.Synopsis && (
-
+
PowerShell Command:
@@ -279,10 +279,10 @@ const CippSchedulerForm = (props) => {
))}
-
+
-
+
{
compareValue={true}
formControl={formControl}
>
-
+
{
/>
-
+
{
mb: 3,
}}
>
-
+
{
-
+
{
)}
-
+
{selectedRole && selectedTenant?.length > 0 && (
<>
Allowed Tenants
diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx
index 08ea7fcd821c..3e0b9cac2357 100644
--- a/src/components/CippSettings/CippRoleAddEdit.jsx
+++ b/src/components/CippSettings/CippRoleAddEdit.jsx
@@ -13,7 +13,7 @@ import {
Skeleton,
} from "@mui/material";
-import Grid from "@mui/material/Grid2";
+import { Grid } from "@mui/system";
import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "../../api/ApiCall";
import { CippOffCanvas } from "/src/components/CippComponents/CippOffCanvas";
import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector";
@@ -505,7 +505,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => {
)}
-
+
{selectedEntraGroup && (
This role will be assigned to the Entra Group:{" "}
diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx
index 5bc741800718..a53839e2ef86 100644
--- a/src/components/CippStandards/CippStandardDialog.jsx
+++ b/src/components/CippStandards/CippStandardDialog.jsx
@@ -88,7 +88,7 @@ const CippStandardDialog = ({
) : (
Object.keys(categories).map((category) =>
filterStandards(categories[category]).map((standard) => (
-
+
{
{orientation === "vertical" ? (
-
+
{
steps={stepsWithVisibility}
/>
-
+
{content}
diff --git a/src/components/CippWizard/CippWizardAutopilotOptions.jsx b/src/components/CippWizard/CippWizardAutopilotOptions.jsx
index 154eebbab74b..0ffe9294f99a 100644
--- a/src/components/CippWizard/CippWizardAutopilotOptions.jsx
+++ b/src/components/CippWizard/CippWizardAutopilotOptions.jsx
@@ -8,7 +8,7 @@ export const CippWizardAutopilotOptions = (props) => {
<>
-
+
{
<>
-
+
{
{fields.map((field) => (
<>
-
+
{
>
))}
-
+
handleAddItem()}>
Add Item
diff --git a/src/components/CippWizard/CippWizardConfirmation.jsx b/src/components/CippWizard/CippWizardConfirmation.jsx
index 8fd49a14f19f..04f15c63baa3 100644
--- a/src/components/CippWizard/CippWizardConfirmation.jsx
+++ b/src/components/CippWizard/CippWizardConfirmation.jsx
@@ -65,7 +65,7 @@ export const CippWizardConfirmation = (props) => {
) : (
-
+
{firstHalf.map(([key, value]) => (
{
))}
-
+
{secondHalf.map(([key, value]) => (
{
compareType="is"
compareValue={true}
>
-
+
Scheduled Offboarding Date
{
/>
-
+
Send results to:
{
return (
{fields.map((field, index) => (
-
+
(
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+
(
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+
{
alignItems="center"
sx={{ height: "100%" }}
>
-
+
(
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+
{
{/* Tenant Filter */}
-
+
{/* Compliance Filter */}
-
+
{
/>
{/* AsApp Filter */}
-
+
{
}
return (
-
+
{
{/* Date Filter */}
-
+
{
{backendInfo.map((item) => (
-
+
))}
diff --git a/src/pages/cipp/settings/index.js b/src/pages/cipp/settings/index.js
index 430a436fd51e..399bf7a2aafd 100644
--- a/src/pages/cipp/settings/index.js
+++ b/src/pages/cipp/settings/index.js
@@ -12,19 +12,19 @@ const Page = () => {
return (
-
+
-
+
-
+
-
+
-
+
diff --git a/src/pages/cipp/settings/partner-webhooks.js b/src/pages/cipp/settings/partner-webhooks.js
index 2a5c38ddae47..8adde7cb4b09 100644
--- a/src/pages/cipp/settings/partner-webhooks.js
+++ b/src/pages/cipp/settings/partner-webhooks.js
@@ -132,7 +132,7 @@ const Page = () => {
}
>
-
+
Subscribe to Microsoft Partner center webhooks to enable automatic tenant onboarding and
alerting. Updating the settings will replace any existing webhook subscription with one
@@ -147,7 +147,7 @@ const Page = () => {
for more information on the webhook types.
-
+
{
showDivider={false}
/>
-
+
{
formControl={formControl}
/>
-
+
{
/>
{testRunning && (
-
+
{
-
+
-
+
-
+
diff --git a/src/pages/cipp/super-admin/function-offloading.js b/src/pages/cipp/super-admin/function-offloading.js
index d8dafb638d38..ed94497c8047 100644
--- a/src/pages/cipp/super-admin/function-offloading.js
+++ b/src/pages/cipp/super-admin/function-offloading.js
@@ -4,7 +4,7 @@ import tabOptions from "./tabOptions";
import CippFormPage from "/src/components/CippFormPages/CippFormPage";
import { useForm } from "react-hook-form";
import { Alert, Typography, Link } from "@mui/material";
-import Grid from "@mui/material/Grid2";
+import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall";
import { useEffect } from "react";
diff --git a/src/pages/cipp/super-admin/tenant-mode.js b/src/pages/cipp/super-admin/tenant-mode.js
index 45b1872e9444..3c023466b037 100644
--- a/src/pages/cipp/super-admin/tenant-mode.js
+++ b/src/pages/cipp/super-admin/tenant-mode.js
@@ -58,7 +58,7 @@ const Page = () => {
queryKey={["execPartnerMode", "TenantSelector"]}
>
-
+
The configuration settings below should only be modified by a super admin. Super admins
can configure what tenant mode CIPP operates in. See{" "}
@@ -72,7 +72,7 @@ const Page = () => {
for more information on how to configure these modes and what they mean.
-
+
{
>
{/* Display Name */}
-
+
{
{/* First Name and Last Name */}
-
+
{
formControl={formControl}
/>
-
+
{
{/* Email */}
-
+
{
{/* Hide from GAL */}
-
+
{
>
{/* Display Name */}
-
+
{
{/* First Name and Last Name */}
-
+
{
formControl={formControl}
/>
-
+
{
{/* Email */}
-
+
{
{/* Hide from GAL */}
-
+
{
{/* Company Information */}
-
+
{
formControl={formControl}
/>
-
+
{
{/* Address Information */}
-
+
{
formControl={formControl}
/>
-
+
-
+
{
formControl={formControl}
/>
-
+
{
{/* Phone Numbers */}
-
+
{
formControl={formControl}
/>
-
+
{
}}
>
-
+
{
{/* Email */}
-
+
{
formControl={formControl}
/>
-
+