diff --git a/.vscode/launch.json b/.vscode/launch.json index df82b6059e48..b0363910187c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,6 +27,13 @@ "request": "launch", "name": "Launch Chrome Debugger", "url": "http://localhost:4280" + }, + { + "type": "PowerShell", + "name": "Launch in Windows Terminal", + "request": "launch", + "cwd": "${cwd}", + "script": ". '${cwd}\\Tools\\Start-CippDevEmulators.ps1'" } ], "compounds": [ diff --git a/Tools/Start-CippDevEmulators.ps1 b/Tools/Start-CippDevEmulators.ps1 new file mode 100644 index 000000000000..b4f6ca696ffe --- /dev/null +++ b/Tools/Start-CippDevEmulators.ps1 @@ -0,0 +1,4 @@ +Write-Host "Starting CIPP Dev Emulators" +$Path = (Get-Item $PSScriptRoot).Parent.Parent.FullName +wt --title CIPP`; new-tab --title 'Azurite' -d $Path pwsh -c azurite`; new-tab --title 'FunctionApp' -d $Path\CIPP-API pwsh -c func start`; new-tab --title 'CIPP Frontend' -d $Path\CIPP pwsh -c npm run start`; new-tab --title 'SWA' -d $Path\CIPP pwsh -c npm run start-swa + diff --git a/package-lock.json b/package-lock.json index 490fb9a392b6..bc2ed9783377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cipp", - "version": "5.6.1", + "version": "5.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cipp", - "version": "5.6.1", + "version": "5.8.5", "license": "AGPL-3.0", "dependencies": { "@coreui/chartjs": "^3.0.0", diff --git a/package.json b/package.json index 332ac7504f6e..039c0b97dd85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "5.7.0", + "version": "5.9.3", "description": "The CyberDrain Improved Partner Portal is a portal to help manage administration for Microsoft Partners.", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/MFAStates.json b/public/MFAStates.json new file mode 100644 index 000000000000..fcaf8dc6ceb3 --- /dev/null +++ b/public/MFAStates.json @@ -0,0 +1,14 @@ +[ + { + "value": "disabled", + "label": "Disabled" + }, + { + "value": "enabled", + "label": "Enabled" + }, + { + "value": "enforced", + "label": "Enforced" + } +] diff --git a/public/version_latest.txt b/public/version_latest.txt index 42cdd0b540f9..99a8b57b6f85 100644 --- a/public/version_latest.txt +++ b/public/version_latest.txt @@ -1 +1 @@ -5.7.0 +5.9.3 diff --git a/src/_nav.jsx b/src/_nav.jsx index 902a8390b79f..874b6e28eb42 100644 --- a/src/_nav.jsx +++ b/src/_nav.jsx @@ -45,6 +45,11 @@ const _nav = [ name: 'Users', to: '/identity/administration/users', }, + { + component: CNavItem, + name: 'Risky Users', + to: '/identity/administration/risky-users', + }, { component: CNavItem, name: 'Groups', @@ -75,6 +80,11 @@ const _nav = [ name: 'Roles', to: '/identity/administration/roles', }, + { + component: CNavItem, + name: 'JIT Admin', + to: '/identity/administration/jit-admin', + }, { component: CNavItem, name: 'Offboarding Wizard', @@ -109,6 +119,11 @@ const _nav = [ name: 'AAD Connect Report', to: '/identity/reports/azure-ad-connect-report', }, + { + component: CNavItem, + name: 'Risk Detections', + to: '/identity/reports/risk-detections', + }, ], }, { @@ -771,6 +786,11 @@ const _nav = [ name: 'Application Settings', to: '/cipp/settings', }, + { + component: CNavItem, + name: 'Extensions Settings', + to: '/cipp/extensions', + }, { component: CNavItem, name: 'User Settings', diff --git a/src/components/buttons/TableModalButton.jsx b/src/components/buttons/TableModalButton.jsx index 67d2b22193da..f18dee1afe9f 100644 --- a/src/components/buttons/TableModalButton.jsx +++ b/src/components/buttons/TableModalButton.jsx @@ -50,4 +50,5 @@ TableModalButton.propTypes = { title: PropTypes.string, className: PropTypes.string, countOnly: PropTypes.bool, + icon: PropTypes.string, } diff --git a/src/components/forms/RFFComponents.jsx b/src/components/forms/RFFComponents.jsx index 737333c7dfc9..14163a033d62 100644 --- a/src/components/forms/RFFComponents.jsx +++ b/src/components/forms/RFFComponents.jsx @@ -253,6 +253,54 @@ RFFCFormInputArray.propTypes = { ...sharedPropTypes, } +export const RFFCFormInputList = ({ name, label, className = 'mb-3' }) => { + return ( + <> + + {({ fields }) => ( +
+
+ {label && ( + + {label} + + )} + fields.push({ Key: '', Value: '' })} + className="circular-button" + title={'+'} + > + + +
+ {fields.map((name, index) => ( +
+
+ + {({ input, meta }) => { + return + }} + +
+ fields.remove(index)} + className={`circular-button`} + title={'-'} + > + + +
+ ))} +
+ )} +
+ + ) +} +RFFCFormInputList.propTypes = { + ...sharedPropTypes, +} + export const RFFCFormRadio = ({ name, label, @@ -293,7 +341,6 @@ export const RFFCFormRadioList = ({ name, options, className = 'mb-3', - disabled = false, onClick, inline = false, }) => { @@ -312,7 +359,6 @@ export const RFFCFormRadioList = ({ onChange={input.onChange} type="radio" {...option} - disabled={disabled} onClick={onClick} inline={inline} /> @@ -424,19 +470,25 @@ RFFCFormSelect.propTypes = { export function Condition({ when, is, children, like, regex }) { return ( <> - {is && ( + {is !== undefined && ( - {({ input: { value } }) => (value === is ? children : null)} + {({ input: { value } }) => { + return value === is ? children : null + }} )} - {like && ( + {like !== undefined && ( - {({ input: { value } }) => (value.includes(like) ? children : null)} + {({ input: { value } }) => { + return value.includes(like) ? children : null + }} )} - {regex && ( + {regex !== undefined && ( - {({ input: { value } }) => (value.match(regex) ? children : null)} + {({ input: { value } }) => { + return value.match(regex) ? children : null + }} )} @@ -465,10 +517,10 @@ export const RFFSelectSearch = ({ isLoading = false, allowCreate = false, refreshFunction, - props, + ...props }) => { const [inputText, setInputText] = useState('') - const selectSearchvalues = values.map((val) => ({ + const selectSearchValues = values.map((val) => ({ value: val.value, label: val.name, ...val.props, @@ -492,12 +544,33 @@ export const RFFSelectSearch = ({ return ( {({ meta, input }) => { - const handleChange = onChange - ? (e) => { - input.onChange(e) - onChange(e) - } - : input.onChange + const handleChange = (e) => { + if (onChange) { + onChange(e) + } + input.onChange(e) + } + + const selectProps = { + classNamePrefix: 'react-select', + ...input, + name, + id: name, + disabled, + options: selectSearchValues, + placeholder, + isMulti: multi, + inputValue: inputText, + isLoading, + onChange: handleChange, + onInputChange: setOnInputChange, + ...props, + //merge className from props into the default className + className: props.className + ? `${props.className} react-select-container` + : 'react-select-container', + } + return (
@@ -515,81 +588,16 @@ export const RFFSelectSearch = ({ )} - {!allowCreate && onChange && ( - - )} - {allowCreate && onChange && ( - + {allowCreate ? ( + + ) : ( + + + + + ) + }} + /> + {extensionConfigResult?.data?.Results && ( + + {extensionConfigResult?.data?.Results} + + )} + {listExtensionTestResult?.data?.Results && ( + + {listExtensionTestResult?.data?.Results} + {listExtensionTestResult?.data?.Link && ( + + Link + + )} + + )} + + + + + + + + + ))} + + + ) +} diff --git a/src/views/cipp/Scheduler.jsx b/src/views/cipp/Scheduler.jsx index aa07fb3722b6..e0fd1aea974a 100644 --- a/src/views/cipp/Scheduler.jsx +++ b/src/views/cipp/Scheduler.jsx @@ -5,6 +5,7 @@ import { Field, Form, FormSpy } from 'react-final-form' import { RFFCFormInput, RFFCFormInputArray, + RFFCFormInputList, RFFCFormSwitch, RFFSelectSearch, } from 'src/components/forms' @@ -157,11 +158,14 @@ const Scheduler = () => { if (typeof row?.Parameters[key] === 'object') { var nestedParamList = [] Object.keys(row?.Parameters[key]).forEach((nestedKey) => { - console.log(nestedKey) - nestedParamList.push({ - Key: nestedKey, - Value: row?.Parameters[key][nestedKey], - }) + if (nestedKey >= 0) { + nestedParamList.push(row?.Parameters[key][nestedKey]) + } else { + nestedParamList.push({ + Key: nestedKey, + Value: row?.Parameters[key][nestedKey], + }) + } }) parameters[key] = nestedParamList } else { @@ -180,6 +184,10 @@ const Scheduler = () => { }) }) + if (!recurrence) { + recurrence = { name: 'Only once', value: '0' } + } + // Set initial values var formValues = { taskName: row.Name, @@ -414,12 +422,22 @@ const Scheduler = () => { key={idx} /> ) : ( - + <> + {param.Type === 'System.String[]' ? ( + + ) : ( + + )} + )} )} diff --git a/src/views/cipp/app-settings/CIPPSettings.jsx b/src/views/cipp/app-settings/CIPPSettings.jsx index 965e1f98f11f..5a610170e49b 100644 --- a/src/views/cipp/app-settings/CIPPSettings.jsx +++ b/src/views/cipp/app-settings/CIPPSettings.jsx @@ -8,9 +8,7 @@ import { SettingsTenants } from 'src/views/cipp/app-settings/SettingsTenants.jsx import { SettingsBackend } from 'src/views/cipp/app-settings/SettingsBackend.jsx' import { SettingsNotifications } from 'src/views/cipp/app-settings/SettingsNotifications.jsx' import { SettingsLicenses } from 'src/views/cipp/app-settings/SettingsLicenses.jsx' -import { SettingsExtensions } from 'src/views/cipp/app-settings/SettingsExtensions.jsx' import { SettingsMaintenance } from 'src/views/cipp/app-settings/SettingsMaintenance.jsx' -import { SettingsExtensionMappings } from 'src/views/cipp/app-settings/SettingsExtensionMappings.jsx' import { SettingsPartner } from 'src/views/cipp/app-settings/SettingsPartner.jsx' import useQuery from 'src/hooks/useQuery.jsx' import { SettingsSuperAdmin } from './SettingsSuperAdmin.jsx' @@ -58,14 +56,8 @@ export default function CIPPSettings() { setActive(7)} href="#"> Maintenance - setActive(8)} href="#"> - Extensions - - setActive(9)} href="#"> - Extension Mappings - {superAdmin && ( - setActive(10)} href="#"> + setActive(8)} href="#"> SuperAdmin Settings )} @@ -106,16 +98,6 @@ export default function CIPPSettings() { - - - - - - - - - - diff --git a/src/views/cipp/app-settings/SettingsExtensionMappings.jsx b/src/views/cipp/app-settings/SettingsExtensionMappings.jsx index de3246422499..6d7a63d2a3d0 100644 --- a/src/views/cipp/app-settings/SettingsExtensionMappings.jsx +++ b/src/views/cipp/app-settings/SettingsExtensionMappings.jsx @@ -19,13 +19,14 @@ import { CippCallout } from 'src/components/layout/index.js' import CippAccordionItem from 'src/components/contentcards/CippAccordionItem' import { CippTable } from 'src/components/tables' import { CellTip } from 'src/components/tables/CellGenericFormat' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' /** * Retrieves and sets the extension mappings for HaloPSA and NinjaOne. * * @returns {JSX.Element} - JSX component representing the settings extension mappings. */ -export function SettingsExtensionMappings() { +export function SettingsExtensionMappings({ type }) { const [addedAttributes, setAddedAttribute] = React.useState(1) const [mappingArray, setMappingArray] = React.useState('defaultMapping') const [mappingValue, setMappingValue] = React.useState({}) @@ -242,308 +243,326 @@ export function SettingsExtensionMappings() { return ( - {listBackendHaloResult.isUninitialized && - listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' })} - {listBackendNinjaOrgsResult.isUninitialized && - listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' })} - {listBackendNinjaFieldsResult.isUninitialized && - listNinjaFieldsBackend({ path: 'api/ExecExtensionMapping?List=NinjaFields' })} - - - - {extensionHaloConfigResult.isFetching && ( - - )} - Save Mappings - - onHaloAutomap()} className="me-2"> - {extensionNinjaOrgsAutomapResult.isFetching && ( - - )} - Automap HaloPSA Clients - - - } - > - {listBackendHaloResult.isFetching && listBackendHaloResult.isUninitialized ? ( - - ) : ( -
{ - return ( - - - Use the table below to map your client to the correct PSA client. - { - //load all the existing mappings and show them first in a table. - listBackendHaloResult.isSuccess && ( - - ) - } - - - { - return !Object.keys(listBackendHaloResult.data?.Mappings).includes( - tenant.customerId, - ) - }).map((tenant) => ({ - name: tenant.displayName, - value: tenant.customerId, - }))} - onChange={(e) => { - setMappingArray(e.value) + {type === 'HaloPSA' && ( + <> + {listBackendHaloResult.isUninitialized && + listHaloBackend({ path: 'api/ExecExtensionMapping?List=Halo' })} + + + + {extensionHaloConfigResult.isFetching && ( + + )} + Save Mappings + + onHaloAutomap()} className="me-2"> + {extensionNinjaOrgsAutomapResult.isFetching && ( + + )} + Automap HaloPSA Clients + + + } + > + {listBackendHaloResult.isFetching && listBackendHaloResult.isUninitialized ? ( + + ) : ( + { + return ( + + + Use the table below to map your client to the correct PSA client. + { + //load all the existing mappings and show them first in a table. + listBackendHaloResult.isSuccess && ( + + ) + } + + + { + return !Object.keys(listBackendHaloResult.data?.Mappings).includes( + tenant.customerId, + ) + }).map((tenant) => ({ + name: tenant.displayName, + value: tenant.customerId, + }))} + onChange={(e) => { + setMappingArray(e.value) + }} + isLoading={listBackendHaloResult.isFetching} + /> + + + + + + { + return !Object.values(listBackendHaloResult.data?.Mappings) + .map((value) => { + return value.value + }) + .includes(client.value) + }).map((client) => ({ + name: client.name, + value: client.value, + }))} + onChange={(e) => setMappingValue(e)} + placeholder="Select a HaloPSA Client" + isLoading={listBackendHaloResult.isFetching} + /> + + { + if ( + mappingValue.value !== undefined && + mappingValue.value !== '-1' && + Object.values(haloMappingsArray) + .map((item) => item.haloId) + .includes(mappingValue.value) === false + ) { + //set the new mapping in the array + setHaloMappingsArray([ + ...haloMappingsArray, + { + Tenant: listBackendHaloResult.data?.Tenants.find( + (tenant) => tenant.customerId === mappingArray, + ), + haloName: mappingValue.label, + haloId: mappingValue.value, + }, + ]) + } }} - isLoading={listBackendHaloResult.isFetching} - /> - - - - - - { - return !Object.values(listBackendHaloResult.data?.Mappings) - .map((value) => { - return value.value - }) - .includes(client.value) - }).map((client) => ({ - name: client.name, - value: client.value, - }))} - onChange={(e) => setMappingValue(e)} - placeholder="Select a HaloPSA Client" - isLoading={listBackendHaloResult.isFetching} - /> - - { - if ( - mappingValue.value !== undefined && - Object.values(haloMappingsArray) - .map((item) => item.haloId) - .includes(mappingValue.value) === false - ) { - //set the new mapping in the array - setHaloMappingsArray([ - ...haloMappingsArray, - { - Tenant: listBackendHaloResult.data?.Tenants.find( - (tenant) => tenant.customerId === mappingArray, - ), - haloName: mappingValue.label, - haloId: mappingValue.value, - }, - ]) - } - }} - className={`my-4 circular-button`} - title={'+'} - > - - - - - - {HaloAutoMap && ( - - Automapping has been executed. Remember to check the changes and save - them. - - )} - {(extensionHaloConfigResult.isSuccess || extensionHaloConfigResult.isError) && - !extensionHaloConfigResult.isFetching && ( - - {extensionHaloConfigResult.isSuccess - ? extensionHaloConfigResult.data.Results - : 'Error'} - + + + + + + {HaloAutoMap && ( + + Automapping has been executed. Remember to check the changes and save + them. + )} - - - - After editing the mappings you must click Save Mappings for the changes to - take effect. The table will be saved exactly as presented. - - - ) - }} - /> - )} - - - - {extensionNinjaOrgsConfigResult.isFetching && ( - - )} - Set Mappings - - onNinjaOrgsAutomap()} className="me-2"> - {extensionNinjaOrgsAutomapResult.isFetching && ( - - )} - Automap NinjaOne Organizations - - - } - > - {listBackendNinjaOrgsResult.isFetching && listBackendNinjaOrgsResult.isUninitialized ? ( - - ) : ( - { - return ( - - - Use the table below to map your client to the correct NinjaOne Organization. - { - //load all the existing mappings and show them first in a table. - listBackendNinjaOrgsResult.isSuccess && ( - - ) - } - - - { - return !Object.keys( - listBackendNinjaOrgsResult.data?.Mappings, - ).includes(tenant.customerId) - }).map((tenant) => ({ - name: tenant.displayName, - value: tenant.customerId, - }))} - onChange={(e) => { - setMappingArray(e.value) - }} - isLoading={listBackendNinjaOrgsResult.isFetching} - /> - - - - - - { - return !Object.values(listBackendNinjaOrgsResult.data?.Mappings) - .map((value) => { - return value.value - }) - .includes(client.value.toString()) - }).map((client) => ({ - name: client.name, - value: client.value, - }))} - onChange={(e) => setMappingValue(e)} - placeholder="Select a NinjaOne Organization" - isLoading={listBackendNinjaOrgsResult.isFetching} - /> - - { - //set the new mapping in the array - if ( - mappingValue.value !== undefined && - Object.values(ninjaMappingsArray) - .map((item) => item.ninjaId) - .includes(mappingValue.value) === false - ) { - setNinjaMappingsArray([ - ...ninjaMappingsArray, - { - Tenant: listBackendNinjaOrgsResult.data?.Tenants.find( - (tenant) => tenant.customerId === mappingArray, - ), - ninjaName: mappingValue.label, - ninjaId: mappingValue.value, + {(extensionHaloConfigResult.isSuccess || + extensionHaloConfigResult.isError) && + !extensionHaloConfigResult.isFetching && ( + + {extensionHaloConfigResult.isSuccess + ? extensionHaloConfigResult.data.Results + : 'Error'} + + )} + + + + After editing the mappings you must click Save Mappings for the changes to + take effect. The table will be saved exactly as presented. + + + ) + }} + /> + )} + + + )} + {type === 'NinjaOne' && ( + <> + {listBackendNinjaOrgsResult.isUninitialized && + listNinjaOrgsBackend({ path: 'api/ExecExtensionMapping?List=NinjaOrgs' })} + {listBackendNinjaFieldsResult.isUninitialized && + listNinjaFieldsBackend({ path: 'api/ExecExtensionMapping?List=NinjaFields' })} + + + {extensionNinjaOrgsConfigResult.isFetching && ( + + )} + Set Mappings + + onNinjaOrgsAutomap()} className="me-2"> + {extensionNinjaOrgsAutomapResult.isFetching && ( + + )} + Automap NinjaOne Organizations + + + } + > + {listBackendNinjaOrgsResult.isFetching && listBackendNinjaOrgsResult.isUninitialized ? ( + + ) : ( + { + return ( + + + Use the table below to map your client to the correct NinjaOne Organization. + { + //load all the existing mappings and show them first in a table. + listBackendNinjaOrgsResult.isSuccess && ( + + ) + } + + + { + return !Object.keys( + listBackendNinjaOrgsResult.data?.Mappings, + ).includes(tenant.customerId) + }).map((tenant) => ({ + name: tenant.displayName, + value: tenant.customerId, + }))} + onChange={(e) => { + setMappingArray(e.value) + }} + isLoading={listBackendNinjaOrgsResult.isFetching} + /> + + + + + + { + return !Object.values(listBackendNinjaOrgsResult.data?.Mappings) + .map((value) => { + return value.value + }) + .includes(client.value.toString()) }, - ]) - } - }} - className={`my-4 circular-button`} - title={'+'} - > - - - - - - {(extensionNinjaOrgsAutomapResult.isSuccess || - extensionNinjaOrgsAutomapResult.isError) && - !extensionNinjaOrgsAutomapResult.isFetching && ( - - {extensionNinjaOrgsAutomapResult.isSuccess - ? extensionNinjaOrgsAutomapResult.data.Results - : 'Error'} - - )} - {(extensionNinjaOrgsConfigResult.isSuccess || - extensionNinjaOrgsConfigResult.isError) && - !extensionNinjaOrgsConfigResult.isFetching && ( - ({ + name: client.name, + value: client.value, + }))} + onChange={(e) => setMappingValue(e)} + placeholder="Select a NinjaOne Organization" + isLoading={listBackendNinjaOrgsResult.isFetching} + /> + + { + //set the new mapping in the array + if ( + mappingValue.value !== undefined && + mappingValue.value !== '-1' && + Object.values(ninjaMappingsArray) + .map((item) => item.ninjaId) + .includes(mappingValue.value) === false + ) { + setNinjaMappingsArray([ + ...ninjaMappingsArray, + { + Tenant: listBackendNinjaOrgsResult.data?.Tenants.find( + (tenant) => tenant.customerId === mappingArray, + ), + ninjaName: mappingValue.label, + ninjaId: mappingValue.value, + }, + ]) + } + }} + className={`my-4 circular-button`} + title={'+'} > - {extensionNinjaOrgsConfigResult.isSuccess - ? extensionNinjaOrgsConfigResult.data.Results - : 'Error'} - - )} - - - - After editing the mappings you must click Save Mappings for the changes to - take effect. The table will be saved exactly as presented. - - - ) - }} - /> - )} - - + + + + + {(extensionNinjaOrgsAutomapResult.isSuccess || + extensionNinjaOrgsAutomapResult.isError) && + !extensionNinjaOrgsAutomapResult.isFetching && ( + + {extensionNinjaOrgsAutomapResult.isSuccess + ? extensionNinjaOrgsAutomapResult.data.Results + : 'Error'} + + )} + {(extensionNinjaOrgsConfigResult.isSuccess || + extensionNinjaOrgsConfigResult.isError) && + !extensionNinjaOrgsConfigResult.isFetching && ( + + {extensionNinjaOrgsConfigResult.isSuccess + ? extensionNinjaOrgsConfigResult.data.Results + : 'Error'} + + )} + + + + After editing the mappings you must click Save Mappings for the changes to + take effect. The table will be saved exactly as presented. + + + ) + }} + /> + )} + + + )} + {type === 'NinjaOne' && ( + )} - - + + )} ) } diff --git a/src/views/cipp/app-settings/SettingsExtensions.jsx b/src/views/cipp/app-settings/SettingsExtensions.jsx index 20ebbcee9725..ab686d2c1df9 100644 --- a/src/views/cipp/app-settings/SettingsExtensions.jsx +++ b/src/views/cipp/app-settings/SettingsExtensions.jsx @@ -1,20 +1,6 @@ import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js' import React, { useRef } from 'react' -import { - CAccordion, - CAlert, - CButton, - CCallout, - CCard, - CCardBody, - CCardHeader, - CCardText, - CCardTitle, - CCol, - CForm, - CRow, - CSpinner, -} from '@coreui/react' +import { CAccordion, CButton, CCardText, CCol, CForm, CSpinner } from '@coreui/react' import Extensions from 'src/data/Extensions.json' import { Form } from 'react-final-form' import { RFFCFormInput, RFFCFormSwitch } from 'src/components/forms/index.js' diff --git a/src/views/cipp/app-settings/SettingsGeneral.jsx b/src/views/cipp/app-settings/SettingsGeneral.jsx index 04446add8ceb..b5dd40624064 100644 --- a/src/views/cipp/app-settings/SettingsGeneral.jsx +++ b/src/views/cipp/app-settings/SettingsGeneral.jsx @@ -253,37 +253,37 @@ export function SettingsGeneral() { - {permissionsResult.data.Results?.ErrorMessages?.length > 0 || - (permissionsResult.data.Results?.MissingPermissions.length > 0 && ( - + {(permissionsResult.data.Results?.ErrorMessages?.length > 0 || + permissionsResult.data.Results?.MissingPermissions.length > 0) && ( + + <> + {permissionsResult.data.Results?.ErrorMessages?.map((m, idx) => ( +
{m}
+ ))} + + {permissionsResult.data.Results?.MissingPermissions.length > 0 && ( <> - {permissionsResult.data.Results?.ErrorMessages?.map((m, idx) => ( -
{m}
- ))} + Your Secure Application Model is missing the following permissions. See + the documentation on how to add permissions{' '} + + here + + . + + {permissionsResult.data.Results?.MissingPermissions?.map( + (r, index) => ( + {r} + ), + )} + - {permissionsResult.data.Results?.MissingPermissions.length > 0 && ( - <> - Your Secure Application Model is missing the following permissions. - See the documentation on how to add permissions{' '} - - here - - . - - {permissionsResult.data.Results?.MissingPermissions?.map( - (r, index) => ( - {r} - ), - )} - - - )} -
- ))} + )} +
+ )}
diff --git a/src/views/cipp/app-settings/SettingsSuperAdmin.jsx b/src/views/cipp/app-settings/SettingsSuperAdmin.jsx index 7650089b5a23..4e38038fb68c 100644 --- a/src/views/cipp/app-settings/SettingsSuperAdmin.jsx +++ b/src/views/cipp/app-settings/SettingsSuperAdmin.jsx @@ -1,9 +1,11 @@ import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js' -import { CButton, CCol, CForm, CLink, CRow, CSpinner } from '@coreui/react' +import { CAccordion, CButton, CCol, CForm, CLink, CRow, CSpinner } from '@coreui/react' import { Form } from 'react-final-form' import { RFFCFormRadio } from 'src/components/forms/index.js' import React from 'react' import { CippCallout } from 'src/components/layout/index.js' +import CippAccordionItem from 'src/components/contentcards/CippAccordionItem' +import SettingsCustomRoles from 'src/views/cipp/app-settings/components/SettingsCustomRoles' import CippButtonCard from 'src/components/contentcards/CippButtonCard' export function SettingsSuperAdmin() { @@ -38,68 +40,71 @@ export function SettingsSuperAdmin() { ) return ( - - <> + <> + <> - - -

- The configuration settings below should only be modified by a super admin. Super - admins can configure what tenant mode CIPP operates in. See - - our documentation - - for more information on how to configure these modes and what they mean. -

-
-
- - -

Tenant Mode

- ( - <> - {partnerConfig.isFetching && } - - - - - - + <> + + +

+ The configuration settings below should only be modified by a super admin. Super + admins can configure what tenant mode CIPP operates in. See + + our documentation + + for more information on how to configure these modes and what they mean. +

+
+
+ + +

Tenant Mode

+ ( + <> + {partnerConfig.isFetching && } + + + + + + + )} + /> + {webhookCreateResult.isSuccess && ( + + {webhookCreateResult?.data?.results} + )} - /> - {webhookCreateResult.isSuccess && ( - - {webhookCreateResult?.data?.results} - - )} -
-
+
+
+ - -
+
+ + ) } diff --git a/src/views/cipp/app-settings/components/SettingsCustomRoles.jsx b/src/views/cipp/app-settings/components/SettingsCustomRoles.jsx new file mode 100644 index 000000000000..5799c900323f --- /dev/null +++ b/src/views/cipp/app-settings/components/SettingsCustomRoles.jsx @@ -0,0 +1,489 @@ +import React, { useRef, useState } from 'react' +import { + CButton, + CCallout, + CCol, + CForm, + CRow, + CAccordion, + CAccordionHeader, + CAccordionBody, + CAccordionItem, +} from '@coreui/react' +import { Field, Form, FormSpy } from 'react-final-form' +import { RFFCFormRadioList, RFFSelectSearch } from 'src/components/forms' +import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { TenantSelectorMultiple, ModalService, CippOffcanvas } from 'src/components/utilities' +import PropTypes from 'prop-types' +import { OnChange } from 'react-final-form-listeners' +import { useListTenantsQuery } from 'src/store/api/tenants' +import { OffcanvasListSection } from 'src/components/utilities/CippListOffcanvas' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' + +const SettingsCustomRoles = () => { + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const [selectedTenant, setSelectedTenant] = useState([]) + const [blockedTenants, setBlockedTenants] = useState([]) + const tenantSelectorRef = useRef() + const blockedTenantSelectorRef = useRef() + const { data: tenants = [], tenantsFetching } = useListTenantsQuery({ + showAllTenantSelector: true, + }) + const [allTenantSelected, setAllTenantSelected] = useState(false) + const [cippApiRoleSelected, setCippApiRoleSelected] = useState(false) + + const { + data: apiPermissions = [], + isFetching, + isSuccess, + } = useGenericGetRequestQuery({ + path: 'api/ExecAPIPermissionList', + }) + + const { + data: customRoleList = [], + isFetching: customRoleListFetching, + isSuccess: customRoleListSuccess, + refetch: refetchCustomRoleList, + } = useGenericGetRequestQuery({ + path: 'api/ExecCustomRole', + }) + + const handleTenantChange = (e) => { + var alltenant = false + e.map((tenant) => { + if (tenant.value === 'AllTenants') { + alltenant = true + } + }) + if (alltenant && blockedTenants.length === 0) { + setAllTenantSelected(true) + } else { + setAllTenantSelected(false) + } + setSelectedTenant(e) + } + + const handleBlockedTenantChange = (e) => { + setBlockedTenants(e) + if (e.length > 0) { + setAllTenantSelected(false) + } + } + + const handleSubmit = async (values) => { + //filter on only objects that are 'true' + genericPostRequest({ + path: '/api/ExecCustomRole?Action=AddUpdate', + values: { + RoleName: values.RoleName.value, + Permissions: values.Permissions, + AllowedTenants: selectedTenant.map((tenant) => tenant.value), + BlockedTenants: blockedTenants.map((tenant) => tenant.value), + }, + }).then(() => { + refetchCustomRoleList() + }) + } + const handleDelete = async (values) => { + ModalService.confirm({ + title: 'Delete Custom Role', + body: 'Are you sure you want to delete this custom role? Any users with this role will have their permissions reset to the default for their base role.', + onConfirm: () => { + genericPostRequest({ + path: '/api/ExecCustomRole?Action=Delete', + values: { + RoleName: values.RoleName.value, + }, + }).then(() => { + refetchCustomRoleList() + }) + }, + }) + } + + const WhenFieldChanges = ({ field, set }) => ( + + {( + // No subscription. We only use Field to get to the change function + { input: { onChange } }, + ) => ( + + {({ form }) => ( + + {(value) => { + if (field === 'RoleName' && value?.value) { + let customRole = customRoleList.filter(function (obj) { + return obj.RowKey === value.value + }) + if (customRole[0]?.RowKey === 'CIPP-API') { + setCippApiRoleSelected(true) + } else { + setCippApiRoleSelected(false) + } + + if (customRole === undefined || customRole === null || customRole.length === 0) { + return false + } else { + if (set === 'AllowedTenants') { + setSelectedTenant(customRole[0][set]) + var selectedTenantList = [] + tenants.map((tenant) => { + if (customRole[0][set].includes(tenant.customerId)) { + selectedTenantList.push({ + label: tenant.displayName, + value: tenant.customerId, + }) + } + }) + + tenantSelectorRef.current.setValue(selectedTenantList) + } else if (set === 'BlockedTenants') { + setBlockedTenants(customRole[0][set]) + var blockedTenantList = [] + tenants.map((tenant) => { + if (customRole[0][set].includes(tenant.customerId)) { + blockedTenantList.push({ + label: tenant.displayName, + value: tenant.customerId, + }) + } + }) + + blockedTenantSelectorRef.current.setValue(blockedTenantList) + } else { + onChange(customRole[0][set]) + } + } + } + if (field === 'Defaults') { + let newPermissions = {} + Object.keys(apiPermissions).forEach((cat) => { + Object.keys(apiPermissions[cat]).forEach((obj) => { + var newval = '' + if (cat == 'CIPP' && obj == 'Core' && value == 'None') { + newval = 'Read' + } else { + newval = value + } + newPermissions[`${cat}${obj}`] = `${cat}.${obj}.${newval}` + }) + }) + onChange(newPermissions) + } + }} + + )} + + )} + + ) + WhenFieldChanges.propTypes = { + field: PropTypes.node, + set: PropTypes.string, + } + + const ApiPermissionRow = ({ obj, cat }) => { + const [offcanvasVisible, setOffcanvasVisible] = useState(false) + + var items = [] + for (var key in apiPermissions[cat][obj]) + for (var key2 in apiPermissions[cat][obj][key]) { + items.push({ heading: '', content: apiPermissions[cat][obj][key][key2] }) + } + var group = [{ items: items }] + + return ( + <> + +
+
{obj}
+
+
+ + setOffcanvasVisible(true)} variant="ghost" size="sm" color="info"> + + + + + + + setOffcanvasVisible(false)} + title="Permission Info" + placement="end" + size="lg" + > +

{`${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 ( + + ) + }) + return sections + })} +
+ + ) + } + ApiPermissionRow.propTypes = { + obj: PropTypes.node, + cat: PropTypes.node, + } + + return ( + + <> +

+ Custom roles can be used to restrict permissions for users with the 'editor' or 'readonly' + roles in CIPP. They can be limited to a subset of tenants and API permissions. To restrict + direct API access, create a role with the name 'CIPP-API'. +

+

+ This functionality is in + beta and should be treated as such. The custom role must be added to the user in SWA in + conjunction with the base role. (e.g. editor,mycustomrole) +

+ {isSuccess && !isFetching && !tenantsFetching && ( + { + return ( + + + +
+ ({ + name: role.RowKey, + value: role.RowKey, + }))} + isLoading={customRoleListFetching} + refreshFunction={() => refetchCustomRoleList()} + allowCreate={true} + placeholder="Select an existing role or enter a custom role name" + /> + + + + {cippApiRoleSelected && ( + + This role will limit access for the CIPP-API integration. It is not + intended to be used for users. + + )} +
+
+
Allowed Tenants
+ handleTenantChange(e)} + /> + {allTenantSelected && ( + + All tenants selected, no tenant restrictions will be applied. + + )} +
+
+
Blocked Tenants
+ handleBlockedTenantChange(e)} + /> +
+ +
API Permissions
+ + +
+
Set All Permissions
+
+
+ + + + + +
+ + <> + {Object.keys(apiPermissions) + .sort() + .map((cat, catIndex) => ( + + {cat} + + {Object.keys(apiPermissions[cat]) + .sort() + .map((obj, index) => { + return ( + + + + ) + })} + + + ))} + + +
+ + + + {({ values }) => { + return ( + <> + {values['RoleName'] && selectedTenant.length > 0 && ( + <> +
Allowed Tenants
+
    + {selectedTenant.map((tenant, idx) => ( +
  • {tenant.label}
  • + ))} +
+ + )} + {values['RoleName'] && blockedTenants.length > 0 && ( + <> +
Blocked Tenants
+
    + {blockedTenants.map((tenant, idx) => ( +
  • {tenant.label}
  • + ))} +
+ + )} + {values['RoleName'] && values['Permissions'] && ( + <> +
Selected Permissions
+
    + {values['Permissions'] && + Object.keys(values['Permissions']) + ?.sort() + .map((cat, idx) => ( + <> + {!values['Permissions'][cat].includes('None') && ( +
  • {values['Permissions'][cat]}
  • + )} + + ))} +
+ + )} + + ) + }} +
+
+
+ + {postResults.isSuccess && ( + {postResults.data.Results} + )} + + + + + Save + + + {({ values }) => { + return ( + handleDelete(values)} + disabled={!values['RoleName']} + > + + Delete + + ) + }} + + + + +
+ ) + }} + /> + )} + +
+ ) +} + +export default SettingsCustomRoles diff --git a/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx b/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx index 1e98f9ecfc8f..fa85482d11a1 100644 --- a/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx +++ b/src/views/cipp/app-settings/components/SettingsGeneralRow.jsx @@ -6,7 +6,7 @@ import { } from 'src/store/api/app.js' import React, { useRef } from 'react' import useConfirmModal from 'src/hooks/useConfirmModal.jsx' -import { CButton, CCard, CCardBody, CCardHeader, CCol, CRow } from '@coreui/react' +import { CButton, CCard, CCardBody, CCardHeader, CCol, CFormCheck, CRow } from '@coreui/react' import { StatusIcon } from 'src/components/utilities/index.js' import { CippCallout } from 'src/components/layout/index.js' import Skeleton from 'react-loading-skeleton' @@ -15,6 +15,7 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { SettingsPassword } from 'src/views/cipp/app-settings/components/SettingsPassword.jsx' import { SettingsDNSResolver } from 'src/views/cipp/app-settings/components/SettingsDNSResolver.jsx' import CippButtonCard from 'src/components/contentcards/CippButtonCard' +import { RFFCFormCheck } from 'src/components/forms' /** * Fetches and maintains DNS configuration settings for the application. @@ -22,6 +23,7 @@ import CippButtonCard from 'src/components/contentcards/CippButtonCard' * @return {JSX.Element | void} The settings DNS component or nothing if data not ready. */ export function SettingsGeneralRow() { + const [setBackupSchedule, BackupScheduleResult] = useLazyGenericGetRequestQuery() const [runBackup, RunBackupResult] = useLazyGenericGetRequestQuery() const [restoreBackup, restoreBackupResult] = useLazyGenericPostRequestQuery() @@ -49,6 +51,9 @@ export function SettingsGeneralRow() { restoreBackup({ path: '/api/ExecRestoreBackup', values: e.target.result }) } } + const handleBackupSchedule = () => { + setBackupSchedule({ path: `/api/ExecSetCIPPAutoBackup?Enabled=true` }) + } const handleClearCache = useConfirmModal({ body:
Are you sure you want to clear the cache?
, @@ -115,6 +120,17 @@ export function SettingsGeneralRow() { )} Restore backup + handleBackupSchedule()} + disabled={BackupScheduleResult.isFetching} + > + {BackupScheduleResult.isFetching && ( + + )} + Create Automated Backup Task + ) return ( @@ -179,16 +195,24 @@ export function SettingsGeneralRow() { id="contained-button-file" onChange={(e) => handleChange(e)} /> - - Use this button to backup the system configuration for CIPP. This will not include - authentication information or extension configuration. - - + + + Use this button to backup the system configuration for CIPP. This will not include + authentication information or extension configuration. You can also set an automated + daily backup schedule by clicking the button below. This will create a scheduled + task for you. + + {restoreBackupResult.isSuccess && !restoreBackupResult.isFetching && ( {restoreBackupResult.data.Results} )} + {BackupScheduleResult.isSuccess && !BackupScheduleResult.isFetching && ( + + {BackupScheduleResult.data.Results} + + )} {RunBackupResult.isSuccess && !restoreBackupResult.isFetching && ( downloadTxtFile(RunBackupResult.data.backup)}> diff --git a/src/views/email-exchange/administration/QuarantineList.jsx b/src/views/email-exchange/administration/QuarantineList.jsx index 7453af66b1a7..6c5de828afec 100644 --- a/src/views/email-exchange/administration/QuarantineList.jsx +++ b/src/views/email-exchange/administration/QuarantineList.jsx @@ -135,6 +135,20 @@ const QuarantineList = () => { capabilities={{ allTenants: false, helpContext: 'https://google.com' }} title="Quarantine Management" datatable={{ + filterlist: [ + { filterName: 'Status: Not Released', filter: '"ReleaseStatus":"NotReleased"' }, + { filterName: 'Status: Released', filter: '"ReleaseStatus":"Released"' }, + { filterName: 'Status: Denied', filter: '"ReleaseStatus":"Denied"' }, + { + filterName: 'Reason: High Confidence Phishing', + filter: '"QuarantineTypes":"HighConfPhish"', + }, + { filterName: 'Reason: Phishing', filter: '"QuarantineTypes":"Phish"' }, + { filterName: 'Reason: Spam', filter: '"QuarantineTypes":"Spam"' }, + { filterName: 'Reason: Malware', filter: '"QuarantineTypes":"Malware"' }, + { filterName: 'Reason: FileTypeBlock', filter: '"QuarantineTypes":"FileTypeBlock"' }, + { filterName: 'Reason: Bulk', filter: '"QuarantineTypes":"Bulk"' }, + ], keyField: 'id', reportName: `${tenant?.defaultDomainName}-Mailbox-Quarantine`, path: '/api/ListMailQuarantine', diff --git a/src/views/email-exchange/spamfilter/DeploySpamfilter.jsx b/src/views/email-exchange/spamfilter/DeploySpamfilter.jsx index c45b21e3fb0d..c2135ec9c833 100644 --- a/src/views/email-exchange/spamfilter/DeploySpamfilter.jsx +++ b/src/views/email-exchange/spamfilter/DeploySpamfilter.jsx @@ -6,7 +6,7 @@ import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' import { CippWizard } from 'src/components/layout' import { WizardTableField } from 'src/components/tables' import PropTypes from 'prop-types' -import { RFFCFormSelect, RFFCFormTextarea } from 'src/components/forms' +import { RFFCFormSelect, RFFCFormTextarea, RFFCFormInput } from 'src/components/forms' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' import { OnChange } from 'react-final-form-listeners' @@ -151,6 +151,11 @@ const SpamFilterAdd = () => { /> + + + + +
@@ -179,6 +184,8 @@ const SpamFilterAdd = () => {
Rule Settings
{props.values.PowerShellCommand} +
Priority
+ {props.values.Priority} diff --git a/src/views/endpoint/autopilot/AutopilotAddDevice.jsx b/src/views/endpoint/autopilot/AutopilotAddDevice.jsx index 35abc7cc505f..b475e2712388 100644 --- a/src/views/endpoint/autopilot/AutopilotAddDevice.jsx +++ b/src/views/endpoint/autopilot/AutopilotAddDevice.jsx @@ -34,6 +34,28 @@ Error.propTypes = { const AddAPDevice = () => { const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() const [autopilotData, setAutopilotdata] = useState([]) + const completeColumns = [ + { + name: 'Serial Number', + selector: (row) => row['serialNumber'], + sortable: true, + }, + { + name: 'Status', + selector: (row) => row['status'], + sortable: true, + }, + { + name: 'Error Code', + selector: (row) => row['errorCode'], + sortable: true, + }, + { + name: 'Error Description', + selector: (row) => row['errorDescription'], + sortable: true, + }, + ] const tableColumns = [ { name: 'serialNumber', @@ -267,8 +289,18 @@ const AddAPDevice = () => { Loading )} - {postResults.isSuccess && {postResults.data.Results}} - {autopilotData && ( + {postResults.isSuccess && ( + <> + {postResults.data?.Results?.Status} + + + )} + {autopilotData && !postResults.isSuccess && ( ( { - + + ({ + value: tag, + name: language, + }))} + name="languages" + multi={false} + label="Languages" + /> + + + + { - + -

- - - - - - - - - + + + + + + + + + + + + +
diff --git a/src/views/endpoint/intune/MEMListAppProtection.jsx b/src/views/endpoint/intune/MEMListAppProtection.jsx index 569608ec916c..f017ba2f2065 100644 --- a/src/views/endpoint/intune/MEMListAppProtection.jsx +++ b/src/views/endpoint/intune/MEMListAppProtection.jsx @@ -48,7 +48,7 @@ const Actions = (row, rowIndex, formatExtraData) => { color: 'danger', modal: true, icon: , - modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=${row.URLName}`, + modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=managedAppPolicies`, modalMessage: 'Are you sure you want to delete this policy?', }, ]} @@ -122,6 +122,15 @@ const AppProtectionList = () => { expandableRows: true, expandableRowsComponent: ExpandedComponent, expandOnRowClicked: true, + selectableRows: true, + actionsList: [ + { + label: 'Delete Policy', + modal: true, + modalUrl: `api/RemovePolicy?TenantFilter=${tenant?.defaultDomainName}&ID=!id&URLName=managedAppPolicies`, + modalMessage: 'Are you sure you want to convert these users to a shared mailbox?', + }, + ], }, }} /> diff --git a/src/views/endpoint/intune/MEMListCompliance.jsx b/src/views/endpoint/intune/MEMListCompliance.jsx index 6fbe0cdf84ce..328a387566fd 100644 --- a/src/views/endpoint/intune/MEMListCompliance.jsx +++ b/src/views/endpoint/intune/MEMListCompliance.jsx @@ -71,7 +71,7 @@ const Actions = (row, rowIndex, formatExtraData) => { color: 'danger', modal: true, icon: , - modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=${row.URLName}`, + modalUrl: `/api/RemovePolicy?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&URLName=deviceCompliancePolicies`, modalMessage: 'Are you sure you want to delete this policy?', }, ]} @@ -145,6 +145,15 @@ const ComplianceList = () => { expandableRows: true, expandableRowsComponent: ExpandedComponent, expandOnRowClicked: true, + selectableRows: true, + actionsList: [ + { + label: 'Delete Policy', + modal: true, + modalUrl: `api/RemovePolicy?TenantFilter=${tenant?.defaultDomainName}&ID=!id&URLName=deviceCompliancePolicies`, + modalMessage: 'Are you sure you want to delete these policies?', + }, + ], }, }} /> diff --git a/src/views/endpoint/intune/MEMListPolicies.jsx b/src/views/endpoint/intune/MEMListPolicies.jsx index 0ea3ed1a26f4..f9f82026cd52 100644 --- a/src/views/endpoint/intune/MEMListPolicies.jsx +++ b/src/views/endpoint/intune/MEMListPolicies.jsx @@ -140,6 +140,15 @@ const IntuneList = () => { expandableRows: true, expandableRowsComponent: ExpandedComponent, expandOnRowClicked: true, + selectableRows: true, + actionsList: [ + { + label: 'Delete Policy', + modal: true, + modalUrl: `api/RemovePolicy?TenantFilter=${tenant?.defaultDomainName}&ID=!id&URLName=!URLName`, + modalMessage: 'Are you sure you want to convert these users to a shared mailbox?', + }, + ], }, }} /> diff --git a/src/views/home/Home.jsx b/src/views/home/Home.jsx index 432bd10e0870..7111f9e41bb2 100644 --- a/src/views/home/Home.jsx +++ b/src/views/home/Home.jsx @@ -30,8 +30,7 @@ import CippCopyToClipboard from 'src/components/utilities/CippCopyToClipboard' import { CChart } from '@coreui/react-chartjs' import { getStyle } from '@coreui/utils' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Link } from 'react-router-dom' -import { useNavigate } from 'react-router-dom' +import { useNavigate, Link } from 'react-router-dom' import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' import { ModalService } from 'src/components/utilities' diff --git a/src/views/identity/administration/DeployJITAdmin.jsx b/src/views/identity/administration/DeployJITAdmin.jsx new file mode 100644 index 000000000000..9d16549f9867 --- /dev/null +++ b/src/views/identity/administration/DeployJITAdmin.jsx @@ -0,0 +1,340 @@ +import React, { useState } from 'react' +import { CButton, CCallout, CCol, CForm, CRow, CSpinner, CTooltip } from '@coreui/react' +import { useSelector } from 'react-redux' +import { Field, Form, FormSpy } from 'react-final-form' +import { + Condition, + RFFCFormInput, + RFFCFormRadioList, + RFFCFormSwitch, + RFFSelectSearch, +} from 'src/components/forms' +import { + useGenericGetRequestQuery, + useLazyGenericGetRequestQuery, + useLazyGenericPostRequestQuery, +} from 'src/store/api/app' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleNotch, faEdit, faEye } from '@fortawesome/free-solid-svg-icons' +import { CippContentCard, CippPage, CippPageList } from 'src/components/layout' +import { CellTip, cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import 'react-datepicker/dist/react-datepicker.css' +import { TenantSelector } from 'src/components/utilities' +import arrayMutators from 'final-form-arrays' +import DatePicker from 'react-datepicker' +import 'react-datepicker/dist/react-datepicker.css' +import { useListUsersQuery } from 'src/store/api/users' +import GDAPRoles from 'src/data/GDAPRoles' +import { CippDatatable, cellDateFormatter } from 'src/components/tables' + +const DeployJITAdmin = () => { + const [ExecuteGetRequest, getResults] = useLazyGenericGetRequestQuery() + const currentDate = new Date() + const [startDate, setStartDate] = useState(currentDate) + const [endDate, setEndDate] = useState(currentDate) + + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const [refreshState, setRefreshState] = useState(false) + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const onSubmit = (values) => { + const startTime = Math.floor(startDate.getTime() / 1000) + const endTime = Math.floor(endDate.getTime() / 1000) + const shippedValues = { + TenantFilter: tenantDomain, + UserId: values.UserId?.value, + UserPrincipalName: values.UserPrincipalName, + FirstName: values.FirstName, + LastName: values.LastName, + useraction: values.useraction, + AdminRoles: values.AdminRoles?.map((role) => role.value), + StartDate: startTime, + UseTAP: values.useTap, + EndDate: endTime, + ExpireAction: values.expireAction.value, + PostExecution: { + Webhook: values.webhook, + Email: values.email, + PSA: values.psa, + }, + } + genericPostRequest({ path: '/api/ExecJITAdmin', values: shippedValues }).then((res) => { + setRefreshState(res.requestId) + }) + } + + const { + data: users = [], + isFetching: usersIsFetching, + error: usersError, + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenantDomain, + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName,accountEnabled', + $count: true, + $top: 999, + $orderby: 'displayName', + }, + }) + + return ( + + <> + + + + { + return ( + +

+ JIT Admin creates an account that is usable for a specific period of time. + Enter a username, select admin roles, date range and expiration action. +

+ + + + + {(props) => } + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + ({ + value: user.id, + name: `${user.displayName} <${user.userPrincipalName}>`, + }))} + placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} + name="UserId" + isLoading={usersIsFetching} + /> + + {({ values }) => { + return users?.Results?.map((user, key) => { + if ( + user.id === values?.UserId?.value && + user.accountEnabled === false + ) { + return ( + + This user is currently disabled, they will automatically be + enabled when JIT is executed. + + ) + } + }) + }} + + + + +
+ + + ({ + value: role.ObjectId, + name: role.Name, + }))} + multi={true} + placeholder="Select Roles" + name="AdminRoles" + /> + + + + + + setStartDate(date)} + /> + + + + setEndDate(date)} + /> + + + + + + + + + + +
+ +
+
+
+
+ + + + + + + + + + + + Add JIT Admin + {postResults.isFetching && ( + + )} + + + + {postResults.isSuccess && ( + + {postResults.data?.Results.map((result, idx) => ( +
  • {result}
  • + ))} +
    + )} + {getResults.isFetching && ( + + Loading + + )} + {getResults.isSuccess && ( + {getResults.data?.Results} + )} + {getResults.isError && ( + + Could not connect to API: {getResults.error.message} + + )} +
    + ) + }} + /> +
    +
    + + + row['userPrincipalName'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'userPrincipalName', + }, + { + name: 'Account Enabled', + selector: (row) => row['accountEnabled'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'accountEnabled', + }, + { + name: 'JIT Enabled', + selector: (row) => row['jitAdminEnabled'], + sortable: true, + cell: cellGenericFormatter(), + exportSelector: 'jitAdminEnabled', + }, + { + name: 'JIT Expires', + selector: (row) => row['jitAdminExpiration'], + sortable: true, + cell: cellDateFormatter({ format: 'short' }), + exportSelector: 'jitAdminExpiration', + }, + { + name: 'Admin Roles', + selector: (row) => row['memberOf'], + sortable: false, + cell: cellGenericFormatter(), + exportSelector: 'memberOf', + }, + ]} + /> + + +
    + +
    + ) +} + +export default DeployJITAdmin diff --git a/src/views/identity/administration/EditGroup.jsx b/src/views/identity/administration/EditGroup.jsx index 928962af5b83..7e81fc66306e 100644 --- a/src/views/identity/administration/EditGroup.jsx +++ b/src/views/identity/administration/EditGroup.jsx @@ -111,7 +111,7 @@ const EditGroup = () => { allowExternal: values.allowExternal, sendCopies: values.sendCopies, mail: group[0].mail, - groupName: group[0].DisplayName, + groupName: group[0].displayName, } //window.alert(JSON.stringify(shippedValues)) genericPostRequest({ path: '/api/EditGroup', values: shippedValues }).then((res) => { diff --git a/src/views/identity/administration/OffboardingWizard.jsx b/src/views/identity/administration/OffboardingWizard.jsx index b5b344969c7a..4d6c496951f8 100644 --- a/src/views/identity/administration/OffboardingWizard.jsx +++ b/src/views/identity/administration/OffboardingWizard.jsx @@ -47,7 +47,18 @@ const OffboardingWizard = () => { data: users = [], isFetching: usersIsFetching, error: usersError, - } = useListUsersQuery({ tenantDomain }) + } = useGenericGetRequestQuery({ + path: `/api/ListGraphRequest`, + params: { + TenantFilter: tenantDomain, + Endpoint: 'users', + $select: + 'id,displayName,givenName,mail,mailNickname,proxyAddresses,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled', + $count: true, + $orderby: 'displayName', + $top: 999, + }, + }) const { data: recipients = [], @@ -121,7 +132,7 @@ const OffboardingWizard = () => { ({ + values={users?.Results?.map((user) => ({ value: user.userPrincipalName, name: `${user.displayName} <${user.userPrincipalName}>`, }))} @@ -177,36 +188,30 @@ const OffboardingWizard = () => { x.mail) - .map((user) => ({ - value: user.mail, - name: `${user.displayName} <${user.mail}>`, - }))} + values={users.Results?.filter((x) => x.mail).map((user) => ({ + value: user.mail, + name: `${user.displayName} <${user.mail}>`, + }))} placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} name="AccessNoAutomap" /> x.mail) - .map((user) => ({ - value: user.mail, - name: `${user.displayName} <${user.mail}>`, - }))} + values={users.Results?.filter((x) => x.mail).map((user) => ({ + value: user.mail, + name: `${user.displayName} <${user.mail}>`, + }))} placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} name="AccessAutomap" /> x.mail) - .map((user) => ({ - value: user.mail, - name: `${user.displayName} <${user.mail}>`, - }))} + values={users.Results?.filter((x) => x.mail).map((user) => ({ + value: user.mail, + name: `${user.displayName} <${user.mail}>`, + }))} placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} name="OnedriveAccess" /> @@ -297,7 +302,7 @@ const OffboardingWizard = () => { >
    Selected User:
    - {users.find((x) => x.userPrincipalName === user.value) + {users.Results?.find((x) => x.userPrincipalName === user.value) .onPremisesSyncEnabled === true ? ( row['riskLastUpdatedDateTime'], + sortable: true, + exportSelector: 'riskLastUpdatedDateTime', + }, + { + name: 'User Principal Name', + selector: (row) => row['userPrincipalName'], + sortable: true, + exportSelector: 'userPrincipalName', + }, + { + name: 'Risk Level', + selector: (row) => row['riskLevel'], + sortable: true, + exportSelector: 'riskLevel', + }, + { + name: 'Risk State', + selector: (row) => row['riskState'], + sortable: true, + exportSelector: 'riskState', + }, + { + name: 'Risk Detail', + selector: (row) => row['riskDetail'], + sortable: true, + exportSelector: 'riskDetail', + }, + { + name: 'isProcessing', + selector: (row) => row['isProcessing'], + sortable: true, + exportSelector: 'isProcessing', + }, + { + name: 'isDeleted', + selector: (row) => row['isDeleted'], + sortable: true, + exportSelector: 'isDeleted', + }, +] + +const RiskyUsers = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + return ( + <> + + + ) +} + +export default RiskyUsers diff --git a/src/views/identity/administration/Roles.jsx b/src/views/identity/administration/Roles.jsx index 79383c4f007d..7e103a130920 100644 --- a/src/views/identity/administration/Roles.jsx +++ b/src/views/identity/administration/Roles.jsx @@ -23,7 +23,12 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { >
    Role Group Name:
    {row.DisplayName}



    -
    Member Names:
    {row.Members ?

    {row.Members}

    :

    Role has no members.

    } +
    Member Names:
    {' '} + {row.Members ? ( + row.Members.split(',').map((member, index) =>

    {member}

    ) + ) : ( +

    Role has no members.

    + )} ) @@ -53,6 +58,26 @@ const columns = [ exportSelector: 'Members', omit: true, }, + { + selector: (row) => row['Members'], + name: 'Assignments', + sortable: false, + cell: (row) => { + if (row.Members === 'none') { + return null + } + const memberCount = row.Members ? row.Members.split(',').length : 0 + const memberText = + row.Members && row.Members !== 'none' ? `Member${memberCount === 1 ? '' : 's'}` : null + return ( + <> + {memberCount} {memberText} + + ) + }, + exportSelector: 'Members', + maxWidth: '150px', + }, { selector: (row) => 'View Members', name: 'Members', diff --git a/src/views/identity/administration/Users.jsx b/src/views/identity/administration/Users.jsx index 2d0a4aa9ac49..0b0ea525497e 100644 --- a/src/views/identity/administration/Users.jsx +++ b/src/views/identity/administration/Users.jsx @@ -111,6 +111,26 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { modalUrl: `/api/ExecSendPush?TenantFilter=${tenant.defaultDomainName}&UserEmail=${row.userPrincipalName}`, modalMessage: 'Are you sure you want to send a MFA request?', }, + { + label: 'Set Per-User MFA', + color: 'info', + modal: true, + modalUrl: `/api/ExecPerUserMFA`, + modalType: 'POST', + modalBody: { + TenantFilter: tenant.defaultDomainName, + userId: `${row.userPrincipalName}`, + }, + modalMessage: 'Are you sure you want to set per-user MFA for these users?', + modalDropdown: { + url: '/MFAStates.json', + labelField: 'label', + valueField: 'value', + addedField: { + State: 'value', + }, + }, + }, { label: 'Convert to Shared Mailbox', color: 'info', @@ -273,7 +293,7 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { label: 'Revoke all user sessions', color: 'danger', modal: true, - modalUrl: `/api/ExecRevokeSessions?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}`, + modalUrl: `/api/ExecRevokeSessions?TenantFilter=${tenant.defaultDomainName}&ID=${row.id}&Username=${row.userPrincipalName}`, modalMessage: 'Are you sure you want to revoke this users sessions?', }, { @@ -447,11 +467,12 @@ const Users = (row) => { filterlist: [ { filterName: 'Enabled users', filter: '"accountEnabled":true' }, { filterName: 'Disabled users', filter: '"accountEnabled":false' }, - { filterName: 'AAD users', filter: '"onPremisesSyncEnabled":false' }, + { filterName: 'AAD users', filter: 'Complex: onPremisesSyncEnabled ne True' }, { filterName: 'Synced users', filter: '"onPremisesSyncEnabled":true', }, + { filterName: 'Non-guest users', filter: 'Complex: usertype ne Guest' }, { filterName: 'Guest users', filter: '"usertype":"guest"' }, { filterName: 'Users with a license', @@ -499,6 +520,26 @@ const Users = (row) => { modalUrl: `/api/ExecResetMFA?TenantFilter=!Tenant&ID=!id`, modalMessage: 'Are you sure you want to enable MFA for these users?', }, + { + label: 'Set Per-User MFA', + color: 'info', + modal: true, + modalUrl: `/api/ExecPerUserMFA`, + modalType: 'POST', + modalBody: { + TenantFilter: tenant.defaultDomainName, + userId: '!userPrincipalName', + }, + modalMessage: 'Are you sure you want to set per-user MFA for these users?', + modalDropdown: { + url: '/MFAStates.json', + labelField: 'label', + valueField: 'value', + addedField: { + State: 'value', + }, + }, + }, { label: 'Enable Online Archive', color: 'info', diff --git a/src/views/identity/reports/MFAReport.jsx b/src/views/identity/reports/MFAReport.jsx index 4f1d80936fa2..27f09aecfa3b 100644 --- a/src/views/identity/reports/MFAReport.jsx +++ b/src/views/identity/reports/MFAReport.jsx @@ -2,6 +2,7 @@ import React from 'react' import { useSelector } from 'react-redux' import { cellBooleanFormatter, CellTip } from 'src/components/tables' import { CippPageList } from 'src/components/layout' +import { Row } from 'react-bootstrap' const columns = [ { @@ -9,7 +10,8 @@ const columns = [ name: 'User Principal Name', sortable: true, exportSelector: 'UPN', - grow: 2, + cell: (row) => CellTip(row['UPN']), + maxWidth: '400px', }, { selector: (row) => row['AccountEnabled'], @@ -17,6 +19,7 @@ const columns = [ sortable: true, cell: cellBooleanFormatter({ colourless: true }), exportSelector: 'AccountEnabled', + maxWidth: '200px', }, { selector: (row) => row['isLicensed'], @@ -24,6 +27,7 @@ const columns = [ sortable: true, cell: cellBooleanFormatter({ colourless: true }), exportSelector: 'isLicensed', + maxWidth: '200px', }, { selector: (row) => row['MFARegistration'], @@ -31,13 +35,23 @@ const columns = [ sortable: true, cell: cellBooleanFormatter(), exportSelector: 'MFARegistration', + maxWidth: '200px', + }, + { + selector: (row) => row['PerUser'], + name: 'Per user MFA Status', + sortable: true, + cell: cellBooleanFormatter(), + exportSelector: 'PerUser', + maxWidth: '200px', }, { selector: (row) => row['CoveredBySD'], name: 'Enforced via Security Defaults', sortable: true, - cell: cellBooleanFormatter({ colourless: true }), + cell: cellBooleanFormatter(), exportSelector: 'CoveredBySD', + maxWidth: '200px', }, { selector: (row) => row['CoveredByCA'], @@ -46,12 +60,6 @@ const columns = [ cell: (row) => CellTip(row['CoveredByCA']), exportSelector: 'CoveredByCA', }, - { - selector: (row) => row['PerUser'], - name: 'Per user MFA Status', - sortable: true, - exportSelector: 'PerUser', - }, ] const Altcolumns = [ diff --git a/src/views/identity/reports/RiskDetections.jsx b/src/views/identity/reports/RiskDetections.jsx new file mode 100644 index 000000000000..ed1604acda6e --- /dev/null +++ b/src/views/identity/reports/RiskDetections.jsx @@ -0,0 +1,125 @@ +import { useSelector } from 'react-redux' +import { CippPageList } from 'src/components/layout' +import { CellTip } from 'src/components/tables' + +const columns = [ + { + name: 'Detected Date', + selector: (row) => row['detectedDateTime'], + sortable: true, + exportSelector: 'detectedDateTime', + }, + { + name: 'User Principal Name', + selector: (row) => row['userPrincipalName'], + sortable: true, + exportSelector: 'userPrincipalName', + }, + { + name: 'Location', + selector: (row) => `${row.location?.city} - ${row.location?.countryOrRegion}`, + sortable: true, + exportSelector: 'Location', + cell: (row) => CellTip(`${row.location?.city} - ${row.location?.countryOrRegion}`), + }, + { + name: 'IP Address', + selector: (row) => row['ipAddress'], + sortable: true, + exportSelector: 'ipAddress', + }, + { + name: 'Risk State', + selector: (row) => row['riskState'], + sortable: true, + exportSelector: 'riskState', + }, + { + name: 'Risk Detail', + selector: (row) => row['riskDetail'], + sortable: true, + exportSelector: 'riskDetail', + }, + { + name: 'Risk Level', + selector: (row) => row['riskLevel'], + sortable: true, + exportSelector: 'riskLevel', + }, + { + name: 'Risk Type', + selector: (row) => row['riskType'], + sortable: true, + exportSelector: 'riskType', + }, + { + name: 'Risk Event Type', + selector: (row) => row['riskEventType'], + sortable: true, + exportSelector: 'riskEventType', + }, + { + name: 'Detection Type', + selector: (row) => row['detectionTimingType'], + sortable: true, + exportSelector: 'detectionTimingType', + }, + { + name: 'Activity', + selector: (row) => row['activity'], + sortable: true, + exportSelector: 'activity', + }, +] + +const RiskDetections = () => { + const tenant = useSelector((state) => state.app.currentTenant) + + return ( + <> + + + ) +} + +export default RiskDetections diff --git a/src/views/identity/reports/SignIns.jsx b/src/views/identity/reports/SignIns.jsx index bcd14acc0c07..45934e5de02b 100644 --- a/src/views/identity/reports/SignIns.jsx +++ b/src/views/identity/reports/SignIns.jsx @@ -16,7 +16,7 @@ import React, { useState } from 'react' import { Form } from 'react-final-form' import { useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' -import { RFFCFormCheck, RFFCFormInput } from 'src/components/forms' +import { Condition, RFFCFormCheck, RFFCFormInput } from 'src/components/forms' import { CippPageList } from 'src/components/layout' import { CellTip } from 'src/components/tables' import useQuery from 'src/hooks/useQuery' @@ -95,11 +95,10 @@ const columns = [ const SignInsReport = () => { const tenant = useSelector((state) => state.app.currentTenant) - let navigate = useNavigate() let query = useQuery() const filter = query.get('filter') const DateFilter = query.get('DateFilter') - const searchparams = query.toString() + const [searchParams, setSearchParams] = useState({}) const [visibleA, setVisibleA] = useState(true) const handleSubmit = async (values) => { @@ -113,11 +112,7 @@ const SignInsReport = () => { SearchNow: true, ...values, } - var queryString = Object.keys(shippedValues) - .map((key) => key + '=' + shippedValues[key]) - .join('&') - - navigate(`?${queryString}`) + setSearchParams(shippedValues) } return ( @@ -152,6 +147,11 @@ const SignInsReport = () => { render={({ handleSubmit, submitting, values }) => { return ( + + + + + { + + + + + + + @@ -189,10 +201,16 @@ const SignInsReport = () => { title="Sign Ins Report" capabilities={{ allTenants: false, helpContext: 'https://google.com' }} datatable={{ + filterlist: [ + { + filterName: 'Risky sign-ins', + filter: 'Complex: riskState ne none', + }, + ], columns: columns, - path: `/api/ListSignIns?${searchparams}`, + path: `/api/ListSignIns`, reportName: `${tenant?.defaultDomainName}-SignIns-Report`, - params: { TenantFilter: tenant?.defaultDomainName }, + params: { TenantFilter: tenant?.defaultDomainName, ...searchParams }, }} /> diff --git a/src/views/teams-share/onedrive/OneDriveList.jsx b/src/views/teams-share/onedrive/OneDriveList.jsx index cacd7d39d0fc..f8bb4936e8d6 100644 --- a/src/views/teams-share/onedrive/OneDriveList.jsx +++ b/src/views/teams-share/onedrive/OneDriveList.jsx @@ -37,7 +37,7 @@ const OneDriveList = () => { TenantFilter: tenant.defaultDomainName, RemovePermission: false, }, - modalUrl: `/api/ExecSharePointOwner`, + modalUrl: `/api/ExecSharePointPerms`, modalDropdown: { url: `/api/listUsers?TenantFilter=${tenant.defaultDomainName}`, labelField: 'displayName', @@ -55,7 +55,7 @@ const OneDriveList = () => { TenantFilter: tenant.defaultDomainName, RemovePermission: true, }, - modalUrl: `/api/ExecSharePointOwner`, + modalUrl: `/api/ExecSharePointPerms`, modalDropdown: { url: `/api/listUsers?TenantFilter=${tenant.defaultDomainName}`, labelField: 'displayName', diff --git a/src/views/teams-share/sharepoint/SharepointList.jsx b/src/views/teams-share/sharepoint/SharepointList.jsx index e4ba2d74db6f..a93bb7c01250 100644 --- a/src/views/teams-share/sharepoint/SharepointList.jsx +++ b/src/views/teams-share/sharepoint/SharepointList.jsx @@ -79,7 +79,7 @@ const SharepointList = () => { RemovePermission: false, URL: row.URL, }, - modalUrl: `/api/ExecSharePointOwner`, + modalUrl: `/api/ExecSharePointPerms`, modalDropdown: { url: `/api/listUsers?TenantFilter=${tenant.defaultDomainName}`, labelField: 'displayName', @@ -98,7 +98,7 @@ const SharepointList = () => { RemovePermission: true, URL: row.URL, }, - modalUrl: `/api/ExecSharePointOwner`, + modalUrl: `/api/ExecSharePointPerms`, modalDropdown: { url: `/api/listUsers?TenantFilter=${tenant.defaultDomainName}`, labelField: 'displayName', diff --git a/src/views/tenant/administration/AlertWizard.jsx b/src/views/tenant/administration/AlertWizard.jsx index 2c3bf20bf0cf..549a316beb1f 100644 --- a/src/views/tenant/administration/AlertWizard.jsx +++ b/src/views/tenant/administration/AlertWizard.jsx @@ -96,7 +96,6 @@ const AlertWizard = () => { const getRecurrenceOptions = () => { const values = currentFormState?.values if (values) { - //console.log(currentFormState) const updatedRecurrenceOptions = recurrenceOptions.map((opt) => ({ ...opt, name: opt.name.replace(' (Recommended)', ''), @@ -317,7 +316,7 @@ const AlertWizard = () => { multi={true} name={`actions`} placeholder={ - 'Select one action or multple actions from the list' + 'Select one action or multiple actions from the list' } label="Then perform the following action(s)" /> diff --git a/src/views/tenant/administration/GDAPInviteWizard.jsx b/src/views/tenant/administration/GDAPInviteWizard.jsx index 2377ae2e0239..956b8a374f00 100644 --- a/src/views/tenant/administration/GDAPInviteWizard.jsx +++ b/src/views/tenant/administration/GDAPInviteWizard.jsx @@ -19,6 +19,7 @@ import { TitleButton } from 'src/components/buttons' import PropTypes from 'prop-types' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { Condition, RFFCFormSwitch } from 'src/components/forms' const Error = ({ name }) => ( (value && value.length !== 0 ? undefined : 'Required') const GDAPInviteWizard = () => { + const defaultRolesArray = [ + { + Name: 'User Administrator', + ObjectId: 'fe930be7-5e62-47db-91af-98c3a49a38b1', + }, + { + Name: 'Teams Administrator', + ObjectId: '69091246-20e8-4a56-aa4d-066075b2a7a8', + }, + { + Name: 'SharePoint Administrator', + ObjectId: 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c', + }, + { + Name: 'Security Administrator', + ObjectId: '194ae4cb-b126-40b2-bd5b-6091b380977d', + }, + { + Name: 'Privileged Role Administrator', + ObjectId: 'e8611ab8-c189-46e8-94e1-60213ab1f814', + }, + { + Name: 'Privileged Authentication Administrator', + ObjectId: '7be44c8a-adaf-4e2a-84d6-ab2649e08a13', + }, + { + Name: 'Intune Administrator', + ObjectId: '3a2c62db-5318-420d-8d74-23affee5d9d5', + }, + { + Name: 'Exchange Administrator', + ObjectId: '29232cdf-9323-42fd-ade2-1d097af3e4de', + }, + { + Name: 'Cloud Device Administrator', + ObjectId: '7698a772-787b-4ac8-901f-60d6b08affd2', + }, + { + Name: 'Cloud App Security Administrator', + ObjectId: '892c5842-a9a6-463a-8041-72aa08ca3cf6', + }, + { + Name: 'Authentication Policy Administrator', + ObjectId: '0526716b-113d-4c15-b2c8-68e3c22b9f80', + }, + { + Name: 'Application Administrator', + ObjectId: '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3', + }, + ] const [inviteCount, setInviteCount] = useState(1) const [loopRunning, setLoopRunning] = React.useState(false) const [massResults, setMassResults] = React.useState([]) const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() const [genericGetRequest, getResults] = useLazyGenericGetRequestQuery() + const [easyModeDone, setEasyMode] = useState(false) + const [easyModeProgress, setEasyModeProgress] = useState(null) const handleSubmit = async (values) => { - const resultsarr = [] - setLoopRunning(true) - for (var x = 0; x < inviteCount; x++) { - const results = await genericPostRequest({ path: '/api/ExecGDAPInvite', values: values }) - resultsarr.push(results.data) - setMassResults(resultsarr) + if (values.easyMode === true) { + if (easyModeDone === false) { + const defaultRoles = { + gdapRoles: defaultRolesArray, + } + const easyModeValues = { ...defaultRoles } + try { + await genericPostRequest({ path: '/api/ExecAddGDAPRole', values: easyModeValues }) + const results = await genericGetRequest({ path: '/api/ListGDAPRoles' }) + const filteredResults = results.data.filter((role) => + defaultRolesArray.some((defaultRole) => defaultRole.ObjectId === role.roleDefinitionId), + ) + setEasyMode(true) + const resultsarr = [] + setLoopRunning(true) + for (var x = 0; x < inviteCount; x++) { + const results = await genericPostRequest({ + path: '/api/ExecGDAPInvite', + values: { ...values, gdapRoles: filteredResults }, + }) + resultsarr.push(results.data) + setMassResults(resultsarr) + } + setLoopRunning(false) + } catch (error) { + setEasyModeProgress(`Failed to create GDAP roles or invite users ${error}`) + setLoopRunning(false) + } + } + } else { + // Normal mode execution + const resultsarr = [] + setLoopRunning(true) + for (var y = 0; y < inviteCount; y++) { + const results = await genericPostRequest({ path: '/api/ExecGDAPInvite', values: values }) + resultsarr.push(results.data) + setMassResults(resultsarr) + } + setLoopRunning(false) } - setLoopRunning(false) } - const formValues = {} + const formValues = { easyMode: true } const inviteColumns = [ { @@ -99,10 +184,7 @@ const GDAPInviteWizard = () => { onSubmit={handleSubmit} wizardTitle="GDAP Invite Wizard" > - +

    Step 1

    @@ -111,39 +193,66 @@ const GDAPInviteWizard = () => {

    - - CIPP will create a single relationship with all roles you've selected for the maximum - duration of 730 days using a GUID as a random name for the relationship. -
    It is recommend to put CIPP user in the correct GDAP Role Groups to manage your - environment secure after deployment of GDAP. -
    -
    - -
    + + + +

    + CIPP will create 12 new groups in your Azure AD environment if they do not exist, + and add the CIPP user to these 12 groups. The CIPP user will be added to the + following groups: +

    +
      +
    • M365 GDAP Application Administrator
    • +
    • M365 GDAP Authentication Policy Administrator
    • +
    • M365 GDAP Cloud App Security Administrator
    • +
    • M365 GDAP Cloud Device Administrator
    • +
    • M365 GDAP Exchange Administrator
    • +
    • M365 GDAP Intune Administrator
    • +
    • M365 GDAP Privileged Authentication Administrator
    • +
    • M365 GDAP Privileged Role Administrator
    • +
    • M365 GDAP Security Administrator
    • +
    • M365 GDAP SharePoint Administrator
    • +
    • M365 GDAP Teams Administrator
    • +
    • M365 GDAP User Administrator
    • +
    + Any other user that needs to gain access to your Microsoft CSP Tenants will need to be + manually added to these groups. +
    +
    + + + CIPP will create a single relationship with all roles you've selected for the maximum + duration of 730 days using a GUID as a random name for the relationship. + + +
    + +
    - - {(props) => ( - row['RoleName'], - sortable: true, - exportselector: 'Name', - }, - { - name: 'Group', - selector: (row) => row['GroupName'], - sortable: true, - }, - ]} - fieldProps={props} - /> - )} - + + {(props) => ( + row['RoleName'], + sortable: true, + exportselector: 'Name', + }, + { + name: 'Group', + selector: (row) => row['GroupName'], + sortable: true, + }, + ]} + fieldProps={props} + /> + )} + +

    @@ -190,25 +299,57 @@ const GDAPInviteWizard = () => { -
    Roles and group names
    - {props.values.gdapRoles.map((role, idx) => ( + {props.values.easyMode === false && ( + <> +
    Roles and group names
    + {props.values.gdapRoles.map((role, idx) => ( + + {role.RoleName === 'Company Administrator' && ( + + WARNING: The Company Administrator role will prevent GDAP + relationships from automatically extending. We recommend against + using this in any GDAP relationship. + + )} + + ))} + +
      + {props.values.gdapRoles.map((role, idx) => ( +
    • + {role.RoleName} - {role.GroupName} +
    • + ))} +
    +
    + + )} + {props.values.easyMode === true && ( <> - {role.RoleName === 'Company Administrator' && ( - - WARNING: The Company Administrator role will prevent GDAP - relationships from automatically extending. We recommend against using - this in any GDAP relationship. - - )} + +

    + You have selected CIPP to manage your roles and groups. Invites will + contain the following roles and groups +

    +
      +
    • M365 GDAP Application Administrator
    • +
    • M365 GDAP Authentication Policy Administrator
    • +
    • M365 GDAP Cloud App Security Administrator
    • +
    • M365 GDAP Cloud Device Administrator
    • +
    • M365 GDAP Exchange Administrator
    • +
    • M365 GDAP Intune Administrator
    • +
    • M365 GDAP Privileged Authentication Administrator
    • +
    • M365 GDAP Privileged Role Administrator
    • +
    • M365 GDAP Security Administrator
    • +
    • M365 GDAP SharePoint Administrator
    • +
    • M365 GDAP Teams Administrator
    • +
    • M365 GDAP User Administrator
    • +
    +
    - ))} - - {props.values.gdapRoles.map((role, idx) => ( -
  • - {role.RoleName} - {role.GroupName} -
  • - ))} -
    + )} + {easyModeProgress && {easyModeProgress}} + {getResults.isFetching && }
    @@ -218,6 +359,14 @@ const GDAPInviteWizard = () => { )} {(massResults.length >= 1 || loopRunning) && ( <> + +

    + The invites have been generated. You can view the results below. The + invite link is to be used by a Global Administrator + of your clients Tenant. Theonboardinglink is to be + used by a CIPP administrator to finish the process inside of CIPP. +

    +
    {loopRunning ? ( diff --git a/src/views/tenant/administration/GraphExplorer.jsx b/src/views/tenant/administration/GraphExplorer.jsx index 3d6950c598d4..ebc3abd8b39d 100644 --- a/src/views/tenant/administration/GraphExplorer.jsx +++ b/src/views/tenant/administration/GraphExplorer.jsx @@ -32,6 +32,7 @@ import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' import PropTypes from 'prop-types' import { CippCodeOffCanvas, ModalService } from 'src/components/utilities' import { debounce } from 'lodash-es' +import CippScheduleOffcanvas from 'src/components/utilities/CippScheduleOffcanvas' const GraphExplorer = () => { const tenant = useSelector((state) => state.app.currentTenant) @@ -57,6 +58,8 @@ const GraphExplorer = () => { error: presetsError, } = useGenericGetRequestQuery({ path: '/api/ListGraphExplorerPresets', params: { random2 } }) const QueryColumns = { set: false, data: [] } + const [scheduleVisible, setScheduleVisible] = useState(false) + const [scheduleValues, setScheduleValues] = useState({}) const debounceEndpointChange = useMemo(() => { function endpointChange(value) { @@ -148,6 +151,36 @@ const GraphExplorer = () => { }) } + function handleSchedule(values) { + var graphParameters = [] + const paramNames = ['$filter', '$format', '$search', '$select', '$top'] + paramNames.map((param) => { + if (values[param]) { + if (Array.isArray(values[param])) { + graphParameters.push({ Key: param, Value: values[param].map((p) => p.value).join(',') }) + } else { + graphParameters.push({ Key: param, Value: values[param] }) + } + } + }) + + const reportName = values.name ?? 'Graph Explorer' + const shippedValues = { + taskName: reportName + ' - ' + tenant.displayName, + command: { label: 'Get-GraphRequestList', value: 'Get-GraphRequestList' }, + parameters: { + Parameters: graphParameters, + NoPagination: values.NoPagination, + ReverseTenantLookup: values.ReverseTenantLookup, + ReverseTenantLookupProperty: values.ReverseTenantLookupProperty, + Endpoint: values.endpoint, + SkipCache: true, + }, + } + setScheduleValues(shippedValues) + setScheduleVisible(true) + } + const presets = [ { name: 'All users with email addresses', @@ -617,6 +650,19 @@ const GraphExplorer = () => { Query + + {(props) => { + return ( + handleSchedule(props.values)} + className="ms-2" + > + + Schedule Report + + ) + }} + @@ -628,6 +674,13 @@ const GraphExplorer = () => { + setScheduleVisible(false)} + initialValues={scheduleValues} + />
    {!searchNow && Execute a search to get started.} diff --git a/src/views/tenant/administration/ListGDAPRelationships.jsx b/src/views/tenant/administration/ListGDAPRelationships.jsx index 11ae56e89e33..25b332ffdcaf 100644 --- a/src/views/tenant/administration/ListGDAPRelationships.jsx +++ b/src/views/tenant/administration/ListGDAPRelationships.jsx @@ -251,7 +251,7 @@ const GDAPRelationships = () => { columns, reportName: `GDAP-Relationships`, path: '/api/ListGraphRequest', - params: { Endpoint: 'tenantRelationships/delegatedAdminRelationships' }, + params: { Endpoint: 'tenantRelationships/delegatedAdminRelationships', $top: 300 }, }} />
    diff --git a/src/views/tenant/conditional/DeployVacation.jsx b/src/views/tenant/conditional/DeployVacation.jsx index 572aee8bb83d..6f43e2ddda30 100644 --- a/src/views/tenant/conditional/DeployVacation.jsx +++ b/src/views/tenant/conditional/DeployVacation.jsx @@ -50,7 +50,17 @@ const ListClassicAlerts = () => { data: users = [], isFetching: usersIsFetching, error: usersError, - } = useListUsersQuery({ tenantDomain }) + } = useGenericGetRequestQuery({ + path: '/api/ListGraphRequest', + params: { + TenantFilter: tenantDomain, + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName,accountEnabled', + $count: true, + $top: 999, + $orderby: 'displayName', + }, + }) const { data: caPolicies = [], @@ -90,12 +100,13 @@ const ListClassicAlerts = () => { ({ + values={users?.Results?.map((user) => ({ value: user.id, name: `${user.displayName} <${user.userPrincipalName}>`, }))} placeholder={!usersIsFetching ? 'Select user' : 'Loading...'} name="UserId" + isLoading={usersIsFetching} />
    @@ -109,6 +120,7 @@ const ListClassicAlerts = () => { }))} placeholder={!caIsFetching ? 'Select policy' : 'Loading...'} name="PolicyId" + isLoading={caIsFetching} /> diff --git a/src/views/tenant/standards/BPAReportBuilder.jsx b/src/views/tenant/standards/BPAReportBuilder.jsx index 1a247f7bff68..eabe4d1bdc63 100644 --- a/src/views/tenant/standards/BPAReportBuilder.jsx +++ b/src/views/tenant/standards/BPAReportBuilder.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react' -import { CippPage, CippContentCard } from 'src/components/layout' +import { CippPage, CippContentCard, CippCallout } from 'src/components/layout' import BPAReportSchema from 'src/data/BPAReport.schema.v1' import BPAReportUISchema from 'src/data/BPAReport.uischema.v1' import validator from '@rjsf/validator-ajv8' @@ -24,7 +24,7 @@ import { CFormLabel, CTooltip, } from '@coreui/react' -import { useGenericGetRequestQuery } from 'src/store/api/app' +import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import CopyToClipboard from 'react-copy-to-clipboard' import PropTypes from 'prop-types' @@ -109,6 +109,8 @@ const BPAReportBuilder = () => { let query = useQuery() const [refreshValue, setRefreshValue] = useState('') const Report = query.get('Report') + const [newBPATemplate, newTemplateResult] = useLazyGenericPostRequestQuery() + const [filename, setFilename] = useState() const [visibleA, setVisibleA] = useState(true) const { data: templates = [], isLoading: templatesfetch } = useGenericGetRequestQuery({ @@ -151,13 +153,14 @@ const BPAReportBuilder = () => { const handlePublish = async (event) => { event.preventDefault() - const data = new FormData(event.target) - const ghuser = data.get('GitHubUser') - const reportfilename = data.get('ReportFilename') - const report = JSON.stringify(formData, null, 2) - const url = - 'https://github.com/' + ghuser + '/CIPP-API/new/master/Config?filename=' + reportfilename - window.open(url, '_blank') + const data = formData + newBPATemplate({ path: '/api/AddBPATemplate', values: data }) + } + + const handleDelete = async (event) => { + event.preventDefault() + const data = formData.name + newBPATemplate({ path: `/api/RemoveBPATemplate?TemplateName=${data}` }) } const options = { @@ -224,30 +227,38 @@ const BPAReportBuilder = () => {
    - + - GitHub Username/Org Name - - Report Filename - + Store and Publish Report in CIPP - - - - - - - Publish - - - - + + + + + + Publish + + + handleDelete(e)}> + + Delete + + + {newTemplateResult.isFetching && } + {newTemplateResult.isSuccess && ( + + {newTemplateResult?.data?.Results} + + )} diff --git a/src/views/tenant/standards/BestPracticeAnalyser.jsx b/src/views/tenant/standards/BestPracticeAnalyser.jsx index 10e7c480c29f..a0e1cb00b101 100644 --- a/src/views/tenant/standards/BestPracticeAnalyser.jsx +++ b/src/views/tenant/standards/BestPracticeAnalyser.jsx @@ -179,9 +179,12 @@ const BestPracticeAnalyser = () => { if (graphrequest.data.length === 0) { graphrequest.data = [{ data: 'No Data Found' }] } - const flatObj = graphrequest.data.Columns ? graphrequest.data.Columns : [] + const flatObj = graphrequest.data.Columns.length >= 0 ? graphrequest.data.Columns : [] flatObj.map((col) => { + if (col === null) { + return + } // Determine the cell selector based on the 'formatter' property let cellSelector if (col.formatter) { @@ -346,99 +349,115 @@ const BestPracticeAnalyser = () => { refreshFunction={setRefreshValue} />
    - {graphrequest.data.Columns.map((info, idx) => ( - - - - {info.name} - - - - {info.formatter === 'bool' && ( - - - {graphrequest.data.Data[0][info.value] ? 'Yes' : 'No'} - - )} - {info.formatter === 'reverseBool' && ( - - - {graphrequest.data.Data[0][info.value] ? 'No' : 'Yes'} - - )} - {info.formatter === 'warnBool' && ( - - - {graphrequest.data.Data[0][info.value] ? 'Yes' : 'No'} - - )} + {graphrequest.data?.Data[0] && + Object.keys(graphrequest.data.Data[0]).length === 0 ? ( + + + Best Practice Report + + + + No Data Found for this tenant. Please refresh the tenant data. + + + + ) : ( + graphrequest.data.Columns.map((info, idx) => ( + + + + {info.name} + + + + {info.formatter === 'bool' && ( + + + {graphrequest.data.Data[0][info.value] ? 'Yes' : 'No'} + + )} + {info.formatter === 'reverseBool' && ( + + + {graphrequest.data.Data[0][info.value] ? 'No' : 'Yes'} + + )} + {info.formatter === 'warnBool' && ( + + + {graphrequest.data.Data[0][info.value] ? 'Yes' : 'No'} + + )} - {info.formatter === 'table' && ( - <> - - - )} + {info.formatter === 'table' && ( + <> + + + )} - {info.formatter === 'number' && ( -

    - {getNestedValue(graphrequest.data.Data[0], info.value)} -

    - )} -
    - - {info.desc} - -
    -
    -
    - ))} + {info.formatter === 'number' && ( +

    + {getNestedValue(graphrequest.data.Data[0], info.value)} +

    + )} +
    + + {info.desc} + +
    +
    +
    + )) + )} )} diff --git a/src/views/tenant/standards/ListAppliedStandards.jsx b/src/views/tenant/standards/ListAppliedStandards.jsx index 484ec3862a36..f60370975ee6 100644 --- a/src/views/tenant/standards/ListAppliedStandards.jsx +++ b/src/views/tenant/standards/ListAppliedStandards.jsx @@ -42,6 +42,7 @@ import GDAPRoles from 'src/data/GDAPRoles' import timezoneList from 'src/data/timezoneList' import Select from 'react-select' import { cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import langaugeList from 'src/data/languageList' const DeleteAction = () => { const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) @@ -547,7 +548,7 @@ const ApplyNewStandard = () => { /> -
    Optional Input
    +
    Settings
    @@ -634,26 +635,19 @@ const ApplyNewStandard = () => {
    Remediate
    -
    Optional Input
    +
    Settings
    {obj.addedComponent && obj.addedComponent.map((component) => ( <> @@ -774,7 +768,7 @@ const ApplyNewStandard = () => {
    -
    Optional Input
    +
    Settings
    {template.templates.isSuccess && ( {
    -
    Optional Input
    +
    Settings
    { />

    + + + ({ + value: tag, + name: language, + }))} + name="standards.APConfig.languages" + multi={false} + label="Languages" + /> + +
    {
    -
    Optional Input
    +
    Settings