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 ( +
+ + + {!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} + + + + + + ) : 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={ - <> - - - {!/^[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 ( +
+ + + {!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} + + + + + + ) : 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 = ({ - {!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 (
- - {!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} )} +
); }; 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={ + + + { + 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]}
    • + )} + + ))} +
    + + )} +
    +
    + + + + + + + ); +}; + +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. + + + + + )} {(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? ( @@ -255,30 +290,7 @@ export const Layout = (props) => { ) : ( - <> - - Setup Wizard - - - - - {!setupCompleted && ( - - - - Setup has not been completed. - - - - - )} - {children} - + <>{children} )}