Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'KelvinTegelaar:main' into main
Browse files Browse the repository at this point in the history
oitjack authored Jul 3, 2024
2 parents 8b4633e + 6224b50 commit b43f20d
Showing 61 changed files with 3,391 additions and 870 deletions.
7 changes: 7 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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": [
4 changes: 4 additions & 0 deletions Tools/Start-CippDevEmulators.ps1
Original file line number Diff line number Diff line change
@@ -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

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
14 changes: 14 additions & 0 deletions public/MFAStates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"value": "disabled",
"label": "Disabled"
},
{
"value": "enabled",
"label": "Enabled"
},
{
"value": "enforced",
"label": "Enforced"
}
]
2 changes: 1 addition & 1 deletion public/version_latest.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.7.0
5.9.3
20 changes: 20 additions & 0 deletions src/_nav.jsx
Original file line number Diff line number Diff line change
@@ -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',
1 change: 1 addition & 0 deletions src/components/buttons/TableModalButton.jsx
Original file line number Diff line number Diff line change
@@ -50,4 +50,5 @@ TableModalButton.propTypes = {
title: PropTypes.string,
className: PropTypes.string,
countOnly: PropTypes.bool,
icon: PropTypes.string,
}
186 changes: 97 additions & 89 deletions src/components/forms/RFFComponents.jsx
Original file line number Diff line number Diff line change
@@ -253,6 +253,54 @@ RFFCFormInputArray.propTypes = {
...sharedPropTypes,
}

export const RFFCFormInputList = ({ name, label, className = 'mb-3' }) => {
return (
<>
<FieldArray name={name}>
{({ fields }) => (
<div>
<div className="mb-2">
{label && (
<CFormLabel className="me-2" htmlFor={name}>
{label}
</CFormLabel>
)}
<CButton
onClick={() => fields.push({ Key: '', Value: '' })}
className="circular-button"
title={'+'}
>
<FontAwesomeIcon icon={'plus'} />
</CButton>
</div>
{fields.map((name, index) => (
<div key={name} className={className}>
<div>
<Field name={`${name}`} component="input">
{({ input, meta }) => {
return <CFormInput placeholder="Value" {...input} className="mb-2" />
}}
</Field>
</div>
<CButton
onClick={() => fields.remove(index)}
className={`circular-button`}
title={'-'}
>
<FontAwesomeIcon icon={'minus'} />
</CButton>
</div>
))}
</div>
)}
</FieldArray>
</>
)
}
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 && (
<Field name={when} subscription={{ value: true }}>
{({ input: { value } }) => (value === is ? children : null)}
{({ input: { value } }) => {
return value === is ? children : null
}}
</Field>
)}
{like && (
{like !== undefined && (
<Field name={when} subscription={{ value: true }}>
{({ input: { value } }) => (value.includes(like) ? children : null)}
{({ input: { value } }) => {
return value.includes(like) ? children : null
}}
</Field>
)}
{regex && (
{regex !== undefined && (
<Field name={when} subscription={{ value: true }}>
{({ input: { value } }) => (value.match(regex) ? children : null)}
{({ input: { value } }) => {
return value.match(regex) ? children : null
}}
</Field>
)}
</>
@@ -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 (
<Field name={name} validate={validate}>
{({ 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 (
<div>
<CFormLabel htmlFor={name}>
@@ -515,81 +588,16 @@ export const RFFSelectSearch = ({
</CTooltip>
)}
</CFormLabel>
{!allowCreate && onChange && (
<Select
className="react-select-container"
classNamePrefix="react-select"
{...input}
isClearable={false}
name={name}
id={name}
disabled={disabled}
options={selectSearchvalues}
placeholder={placeholder}
isMulti={multi}
onChange={handleChange}
onInputChange={debounceOnInputChange}
inputValue={inputText}
isLoading={isLoading}
{...props}
/>
)}
{!allowCreate && !onChange && (
<Select
className="react-select-container"
classNamePrefix="react-select"
{...input}
isClearable={true}
name={name}
id={name}
disabled={disabled}
options={selectSearchvalues}
placeholder={placeholder}
onInputChange={setOnInputChange}
isMulti={multi}
inputValue={inputText}
isLoading={isLoading}
{...props}
/>
)}
{allowCreate && onChange && (
<Creatable
className="react-select-container"
classNamePrefix="react-select"
{...input}
isClearable={false}
name={name}
id={name}
disabled={disabled}
options={selectSearchvalues}
placeholder={placeholder}
isMulti={multi}
onChange={handleChange}
onInputChange={debounceOnInputChange}
inputValue={inputText}
isLoading={isLoading}
{...props}
/>
{allowCreate ? (
<Creatable {...selectProps} isClearable={true} />
) : (
<Select {...selectProps} isClearable={!onChange} />
)}
{allowCreate && !onChange && (
<Creatable
className="react-select-container"
classNamePrefix="react-select"
{...input}
isClearable={true}
name={name}
id={name}
disabled={disabled}
options={selectSearchvalues}
placeholder={placeholder}
onInputChange={setOnInputChange}
isMulti={multi}
inputValue={inputText}
isLoading={isLoading}
{...props}
/>
{meta.error && meta.touched && (
<span className="text-danger">
{typeof meta.error === 'object' ? Object.values(meta.error).join('') : meta.error}
</span>
)}
{meta.error && meta.touched && <span className="text-danger">{meta.error}</span>}
</div>
)
}}
2 changes: 2 additions & 0 deletions src/components/forms/index.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import {
RFFCFormSelect,
RFFSelectSearch,
RFFCFormInputArray,
RFFCFormInputList,
} from 'src/components/forms/RFFComponents'

export {
@@ -24,4 +25,5 @@ export {
RFFCFormSelect,
RFFSelectSearch,
RFFCFormInputArray,
RFFCFormInputList,
}
26 changes: 25 additions & 1 deletion src/components/layout/AppHeader.jsx
Original file line number Diff line number Diff line change
@@ -15,7 +15,12 @@ import { AppHeaderSearch } from 'src/components/header'
import { CippActionsOffcanvas, TenantSelector } from '../utilities'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBars } from '@fortawesome/free-solid-svg-icons'
import { setCurrentTheme, setUserSettings, toggleSidebarShow } from 'src/store/features/app'
import {
setCurrentTheme,
setSetupCompleted,
setUserSettings,
toggleSidebarShow,
} from 'src/store/features/app'
import { useMediaPredicate } from 'react-media-hook'
import {
useGenericGetRequestQuery,
@@ -92,6 +97,25 @@ const AppHeader = () => {
}
}, [delay, state])
}
//useEffect to check if any of the dashboard alerts contained the key "setupCompleted" and if so,
//check if the value of this key is false. If so, set the setupCompleted state to false
//if none is found, set the setupCompleted state to true
useEffect(() => {
if (dashboard && Array.isArray(dashboard) && dashboard.length >= 1) {
console.log('Finding if setup is completed.')
const setupCompleted = dashboard.find((alert) => alert && alert.setupCompleted === false)
if (setupCompleted) {
console.log("Setup isn't completed yet, we found a match with false.")
dispatch(setSetupCompleted({ setupCompleted: false }))
} else {
console.log('Setup is completed.')
dispatch(setSetupCompleted({ setupCompleted: true }))
}
} else {
console.log('Setup is completed.')
dispatch(setSetupCompleted({ setupCompleted: true }))
}
}, [dashboard, dispatch])

useEffect(() => {
if (cippQueueList.isUninitialized && (cippQueueList.isFetching || cippQueueList.isLoading)) {
1 change: 1 addition & 0 deletions src/components/tables/CellBoolean.jsx
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@ export default function CellBoolean({
if (
cell.toLowerCase() === 'success' ||
cell.toLowerCase() === 'enabled' ||
cell.toLowerCase() === 'enforced' ||
cell.toLowerCase() === 'pass' ||
cell.toLowerCase() === 'true' ||
cell.toLowerCase() === 'compliant'
47 changes: 28 additions & 19 deletions src/components/tables/CippTable.jsx
Original file line number Diff line number Diff line change
@@ -414,6 +414,7 @@ export default function CippTable({
(modalMessage, modalUrl, modalType = 'GET', modalBody, modalInput, modalDropdown) => {
if (modalType === 'GET') {
ModalService.confirm({
getData: () => inputRef.current?.value,
body: (
<div style={{ overflow: 'visible' }}>
<div>{modalMessage}</div>
@@ -466,6 +467,18 @@ export default function CippTable({
title: 'Confirm',
onConfirm: async () => {
const resultsarr = []
const selectedValue = inputRef.current.value
let additionalFields = {}
if (inputRef.current.nodeName === 'SELECT') {
const selectedItem = dropDownInfo.data.find(
(item) => item[modalDropdown.valueField] === selectedValue,
)
if (selectedItem && modalDropdown.addedField) {
Object.keys(modalDropdown.addedField).forEach((key) => {
additionalFields[key] = selectedItem[modalDropdown.addedField[key]]
})
}
}
for (const row of selectedRows) {
setLoopRunning(true)
const urlParams = new URLSearchParams(modalUrl.split('?')[1])
@@ -492,26 +505,13 @@ export default function CippTable({
}
}
const NewModalUrl = `${modalUrl.split('?')[0]}?${urlParams.toString()}`
const selectedValue = inputRef.current.value
let additionalFields = {}
if (inputRef.current.nodeName === 'SELECT') {
const selectedItem = dropDownInfo.data.find(
(item) => item[modalDropdown.valueField] === selectedValue,
)
if (selectedItem && modalDropdown.addedField) {
Object.keys(modalDropdown.addedField).forEach((key) => {
additionalFields[key] = selectedItem[modalDropdown.addedField[key]]
})
}
}

const results = await genericPostRequest({
path: NewModalUrl,
values: {
...modalBody,
...newModalBody,
...additionalFields,
...{ input: inputRef.current.value },
...{ input: selectedValue },
},
})
resultsarr.push(results)
@@ -637,14 +637,25 @@ export default function CippTable({

// Define the flatten function
const flatten = (obj, prefix = '') => {
if (obj === null) return {}
return Object.keys(obj).reduce((output, key) => {
const newKey = prefix ? `${prefix}.${key}` : key
const value = obj[key] === null ? '' : obj[key]

if (typeof value === 'object' && !Array.isArray(value)) {
Object.assign(output, flatten(value, newKey))
} else {
output[newKey] = value
if (Array.isArray(value)) {
if (typeof value[0] === 'object') {
value.map((item, idx) => {
Object.assign(output, flatten(item, `${newKey}[${idx}]`))
})
} else {
output[newKey] = value
}
} else {
output[newKey] = value
}
}
return output
}, {})
@@ -677,8 +688,7 @@ export default function CippTable({
})
return Array.isArray(exportData) && exportData.length > 0
? exportData.map((obj) => {
const flattenedObj = flatten(obj)
return applyFormatter(flattenedObj)
return flatten(applyFormatter(obj))
})
: []
}
@@ -689,8 +699,7 @@ export default function CippTable({
// Adjusted dataFlat processing to include formatting
let dataFlat = Array.isArray(data)
? data.map((item) => {
const flattenedItem = flatten(item)
return applyFormatter(flattenedItem)
return flatten(applyFormatter(item))
})
: []
if (!disablePDFExport) {
2 changes: 1 addition & 1 deletion src/components/utilities/CippActionsOffcanvas.jsx
Original file line number Diff line number Diff line change
@@ -345,7 +345,7 @@ export default function CippActionsOffcanvas(props) {
}
let actionsSelectorsContent
try {
actionsSelectorsContent = props.actionsSelect.map((action, index) => (
actionsSelectorsContent = props?.actionsSelect?.map((action, index) => (
<CListGroupItem className="" component="label" color={action.color} key={index}>
{action.label}
<CListGroupItem
6 changes: 3 additions & 3 deletions src/components/utilities/CippFuzzySearch.jsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import Fuse from 'fuse.js'
function CippfuzzySearch(options) {
const fuse = new Fuse(options, {
keys: ['name', 'groupName', 'items.name'],
threshold: 0.5,
threshold: 0.3,
location: 0,
ignoreLocation: true,
useExtendedSearch: true,
@@ -15,8 +15,8 @@ function CippfuzzySearch(options) {
if (!value.length) {
return options
}

return fuse.search(value).map((_ref) => {
const search = fuse.search(value)
return search.map((_ref) => {
let { item } = _ref
return item
})
9 changes: 7 additions & 2 deletions src/components/utilities/CippListOffcanvas.jsx
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ CippListOffcanvas.propTypes = {
hideFunction: PropTypes.func.isRequired,
}

export function OffcanvasListSection({ title, items }) {
export function OffcanvasListSection({ title, items, showCardTitle = true }) {
//console.log(items)
const mappedItems = items.map((item, key) => ({ value: item.content, label: item.heading }))
return (
@@ -48,7 +48,11 @@ export function OffcanvasListSection({ title, items }) {
<CCard className="content-card">
<CCardHeader className="d-flex justify-content-between align-items-center">
<CCardTitle>
<FontAwesomeIcon icon={faGlobe} className="mx-2" /> Extended Information
{showCardTitle && (
<>
<FontAwesomeIcon icon={faGlobe} className="mx-2" /> Extended Information
</>
)}
</CCardTitle>
</CCardHeader>
<CCardBody>
@@ -62,4 +66,5 @@ export function OffcanvasListSection({ title, items }) {
OffcanvasListSection.propTypes = {
title: PropTypes.string,
items: PropTypes.array,
showCardTitle: PropTypes.bool,
}
276 changes: 276 additions & 0 deletions src/components/utilities/CippScheduleOffcanvas.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import {
CButton,
CCallout,
CCard,
CCardBody,
CCardHeader,
CCol,
CForm,
CRow,
CSpinner,
CTooltip,
} from '@coreui/react'
import { CippOffcanvas, TenantSelector } from '.'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Field, Form, FormSpy } from 'react-final-form'
import arrayMutators from 'final-form-arrays'
import {
RFFCFormInput,
RFFCFormInputArray,
RFFCFormSwitch,
RFFSelectSearch,
} from 'src/components/forms'
import { useSelector } from 'react-redux'
import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app'
import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'

export default function CippScheduleOffcanvas({
state: visible,
hideFunction,
title,
placement,
...props
}) {
const currentDate = new Date()
const [startDate, setStartDate] = useState(currentDate)
const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName)
const [refreshState, setRefreshState] = useState(false)
const taskName = `Scheduled Task ${currentDate.toLocaleString()}`
const { data: availableCommands = [], isLoading: isLoadingcmd } = useGenericGetRequestQuery({
path: 'api/ListFunctionParameters?Module=CIPPCore',
})

const recurrenceOptions = [
{ value: '0', name: 'Only once' },
{ value: '1', name: 'Every 1 day' },
{ value: '7', name: 'Every 7 days' },
{ value: '30', name: 'Every 30 days' },
{ value: '365', name: 'Every 365 days' },
]

const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery()
const onSubmit = (values) => {
const unixTime = Math.floor(startDate.getTime() / 1000)
const shippedValues = {
TenantFilter: tenantDomain,
Name: values.taskName,
Command: values.command,
Parameters: values.parameters,
ScheduledTime: unixTime,
Recurrence: values.Recurrence,
AdditionalProperties: values.additional,
PostExecution: {
Webhook: values.webhook,
Email: values.email,
PSA: values.psa,
},
}
genericPostRequest({ path: '/api/AddScheduledItem', values: shippedValues }).then((res) => {
setRefreshState(res.requestId)
if (props.submitFunction) {
props.submitFunction()
}
})
}

return (
<CippOffcanvas
placement={placement}
title={title}
visible={visible}
hideFunction={hideFunction}
>
<CCard>
<CCardHeader></CCardHeader>
<CCardBody>
<Form
onSubmit={onSubmit}
mutators={{
...arrayMutators,
}}
initialValues={{ ...props.initialValues }}
render={({ handleSubmit, submitting, values }) => {
return (
<CForm onSubmit={handleSubmit}>
<CRow className="mb-3">
<CCol>
<label>Tenant</label>
<Field name="tenantFilter">{(props) => <TenantSelector />}</Field>
</CCol>
</CRow>
<CRow>
<CCol>
<RFFCFormInput
type="text"
name="taskName"
label="Task Name"
firstValue={`Task ${currentDate.toLocaleString()}`}
/>
</CCol>
</CRow>
<CRow>
<CCol>
<label>Scheduled Date</label>
<DatePicker
className="form-control mb-3"
selected={startDate}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="Pp"
onChange={(date) => setStartDate(date)}
/>
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<RFFSelectSearch
values={recurrenceOptions}
name="Recurrence"
placeholder="Select a recurrence"
label="Recurrence"
/>
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<RFFSelectSearch
values={availableCommands.map((cmd) => ({
value: cmd.Function,
name: cmd.Function,
}))}
name="command"
placeholder={
isLoadingcmd ? (
<CSpinner size="sm" />
) : (
'Select a command or report to execute.'
)
}
label="Command to execute"
/>
</CCol>
</CRow>
<FormSpy>
{/* eslint-disable react/prop-types */}
{(props) => {
const selectedCommand = availableCommands.find(
(cmd) => cmd.Function === props.values.command?.value,
)
return (
<CRow className="mb-3">
<CCol>{selectedCommand?.Synopsis}</CCol>
</CRow>
)
}}
</FormSpy>
<CRow>
<FormSpy>
{/* eslint-disable react/prop-types */}
{(props) => {
const selectedCommand = availableCommands.find(
(cmd) => cmd.Function === props.values.command?.value,
)
let paramblock = null
if (selectedCommand) {
//if the command parameter type is boolean we use <RFFCFormCheck /> else <RFFCFormInput />.
const parameters = selectedCommand.Parameters
if (parameters.length > 0) {
paramblock = parameters.map((param, idx) => (
<CRow key={idx} className="mb-3">
<CTooltip
content={
param?.Description !== null
? param.Description
: 'No Description'
}
placement="left"
>
<CCol>
{param.Type === 'System.Boolean' ||
param.Type ===
'System.Management.Automation.SwitchParameter' ? (
<>
<label>{param.Name}</label>
<RFFCFormSwitch
initialValue={false}
name={`parameters.${param.Name}`}
label={`True`}
/>
</>
) : (
<>
{param.Type === 'System.Collections.Hashtable' ? (
<RFFCFormInputArray
name={`parameters.${param.Name}`}
label={`${param.Name}`}
key={idx}
/>
) : (
<RFFCFormInput
type="text"
key={idx}
name={`parameters.${param.Name}`}
label={`${param.Name}`}
/>
)}
</>
)}
</CCol>
</CTooltip>
</CRow>
))
}
}
return paramblock
}}
</FormSpy>
</CRow>
<CRow className="mb-3">
<CCol>
<RFFCFormInputArray name={`additional`} label="Additional Properties" />
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<label>Send results to</label>
<RFFCFormSwitch name="webhook" label="Webhook" />
<RFFCFormSwitch name="email" label="E-mail" />
<RFFCFormSwitch name="psa" label="PSA" />
</CCol>
</CRow>
<CRow className="mb-3">
<CCol md={6}>
<CButton type="submit" disabled={submitting}>
Add Schedule
{postResults.isFetching && (
<FontAwesomeIcon icon="circle-notch" spin className="ms-2" size="1x" />
)}
</CButton>
</CCol>
</CRow>
{postResults.isSuccess && (
<CCallout color="success">
<li>{postResults.data.Results}</li>
</CCallout>
)}
</CForm>
)
}}
/>
</CCardBody>
</CCard>
</CippOffcanvas>
)
}

CippScheduleOffcanvas.propTypes = {
groups: PropTypes.array,
placement: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
state: PropTypes.bool,
hideFunction: PropTypes.func.isRequired,
}
2 changes: 1 addition & 1 deletion src/components/utilities/TenantSelector.jsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import { useListTenantsQuery } from 'src/store/api/tenants'
import { setCurrentTenant } from 'src/store/features/app'
import { CButton, CDropdown, CDropdownMenu, CDropdownToggle } from '@coreui/react'
import { CButton } from '@coreui/react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { queryString } from 'src/helpers'
import { faBuilding } from '@fortawesome/free-solid-svg-icons'
72 changes: 68 additions & 4 deletions src/data/Extensions.json
Original file line number Diff line number Diff line change
@@ -16,7 +16,8 @@
"name": "cippapi.Enabled",
"label": "Enable Integration"
}
]
],
"mappingRequired": false
},
{
"name": "Gradient Integration",
@@ -49,7 +50,8 @@
"name": "Gradient.Enabled",
"label": "Enable Integration"
}
]
],
"mappingRequired": false
},
{
"name": "Halo PSA Ticketing Integration",
@@ -105,7 +107,8 @@
"name": "HaloPSA.Enabled",
"label": "Enable Integration"
}
]
],
"mappingRequired": true
},
{
"name": "NinjaOne Integration",
@@ -155,6 +158,67 @@
"name": "NinjaOne.Enabled",
"label": "Enable Integration"
}
]
],
"mappingRequired": true
},
{
"name": "PasswordPusher",
"type": "PWPush",
"cat": "Passwords",
"forceSyncButton": false,
"helpText": "This integration allows you to generate password links instead of plain text passwords. Visit https://pwpush.com/ or https://github.com/pglombardo/PasswordPusher for more information.",
"SettingOptions": [
{
"type": "checkbox",
"name": "PWPush.Enabled",
"label": "Replace generated passwords with PWPush links"
},
{
"type": "input",
"fieldtype": "text",
"name": "PWPush.BaseUrl",
"label": "PWPush URL",
"placeholder": "Enter your PWPush URL. (default: https://pwpush.com)"
},
{
"type": "input",
"fieldtype": "text",
"name": "PWPush.EmailAddress",
"label": "PWPush email address",
"placeholder": "Enter your email address for PWPush. (optional)"
},
{
"type": "input",
"fieldtype": "password",
"name": "PWPush.APIKey",
"label": "PWPush API Key",
"placeholder": "Enter your PWPush API Key. (optional)"
},
{
"type": "checkbox",
"name": "PWPush.RetrievalStep",
"label": "Click to retrieve password (recommended)"
},
{
"type": "input",
"fieldtype": "number",
"name": "PWPush.ExpireAfterDays",
"label": "Expiration in Days",
"placeholder": "Expiration time in days. (optional)"
},
{
"type": "input",
"fieldtype": "number",
"name": "PWPush.ExpireAfterViews",
"label": "Expiration after views",
"placeholder": "Expiration after views. (optional)"
},
{
"type": "checkbox",
"name": "PWPush.DeletableByViewer",
"label": "Allow deletion of passwords"
}
],
"mappingRequired": false
}
]
358 changes: 338 additions & 20 deletions src/data/standards.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/importsMap.jsx
Original file line number Diff line number Diff line change
@@ -12,8 +12,10 @@ import React from 'react'
"/identity/administration/users/edit": React.lazy(() => import('./views/identity/administration/EditUser')),
"/identity/administration/users/view": React.lazy(() => import('./views/identity/administration/ViewUser')),
"/identity/administration/users/InviteGuest": React.lazy(() => import('./views/identity/administration/InviteGuest')),
"/identity/administration/jit-admin": React.lazy(() => import('./views/identity/administration/DeployJITAdmin')),
"/identity/administration/ViewBec": React.lazy(() => import('./views/identity/administration/ViewBEC')),
"/identity/administration/users": React.lazy(() => import('./views/identity/administration/Users')),
"/identity/administration/risky-users": React.lazy(() => import('./views/identity/administration/RiskyUsers')),
"/identity/administration/devices": React.lazy(() => import('./views/identity/administration/Devices')),
"/identity/administration/groups/add": React.lazy(() => import('./views/identity/administration/AddGroup')),
"/identity/administration/group-templates": React.lazy(() => import('./views/identity/administration/GroupTemplates')),
@@ -31,6 +33,7 @@ import React from 'react'
"/identity/reports/inactive-users-report": React.lazy(() => import('./views/identity/reports/InactiveUsers')),
"/identity/reports/Signin-report": React.lazy(() => import('./views/identity/reports/SignIns')),
"/identity/reports/azure-ad-connect-report": React.lazy(() => import('./views/identity/reports/AzureADConnectReport')),
"/identity/reports/risk-detections": React.lazy(() => import('./views/identity/reports/RiskDetections')),
"/tenant/administration/tenants": React.lazy(() => import('./views/tenant/administration/Tenants')),
"/tenant/administration/tenants/edit": React.lazy(() => import('./views/tenant/administration/EditTenant')),
"/tenant/administration/partner-relationships": React.lazy(() => import('./views/tenant/administration/PartnerRelationships')),
@@ -129,6 +132,7 @@ import React from 'react'
"/security/reports/list-device-compliance": React.lazy(() => import('./views/security/reports/ListDeviceComplianceReport')),
"/license": React.lazy(() => import('./views/pages/license/License')),
"/cipp/settings": React.lazy(() => import('./views/cipp/app-settings/CIPPSettings')),
"/cipp/extensions": React.lazy(() => import('./views/cipp/Extensions')),
"/cipp/setup": React.lazy(() => import('./views/cipp/Setup')),
"/tenant/administration/securescore": React.lazy(() => import('./views/tenant/administration/SecureScore')),
"/tenant/administration/gdap": React.lazy(() => import('./views/tenant/administration/GDAPWizard')),
24 changes: 24 additions & 0 deletions src/routes.json
Original file line number Diff line number Diff line change
@@ -76,6 +76,12 @@
"component": "views/identity/administration/InviteGuest",
"allowedRoles": ["admin", "editor", "readonly"]
},
{
"path": "/identity/administration/jit-admin",
"name": "JIT Admin",
"component": "views/identity/administration/DeployJITAdmin",
"allowedRoles": ["admin", "editor", "readonly"]
},
{
"path": "/identity/administration/ViewBec",
"name": "View BEC",
@@ -93,6 +99,12 @@
"component": "views/identity/administration/Users",
"allowedRoles": ["admin", "editor", "readonly"]
},
{
"path": "/identity/administration/risky-users",
"name": "Risky Users",
"component": "views/identity/administration/RiskyUsers",
"allowedRoles": ["admin", "editor", "readonly"]
},
{
"path": "/identity/administration/devices",
"name": "Devices",
@@ -200,6 +212,12 @@
"component": "views/identity/reports/AzureADConnectReport",
"allowedRoles": ["admin", "editor", "readonly"]
},
{
"path": "/identity/reports/risk-detections",
"name": "Risk Detections",
"component": "views/identity/reports/RiskDetections",
"allowedRoles": ["admin", "editor", "readonly"]
},
{
"path": "/tenant",
"name": "Tenant",
@@ -888,6 +906,12 @@
"component": "views/cipp/app-settings/CIPPSettings",
"allowedRoles": ["admin"]
},
{
"path": "/cipp/extensions",
"name": "Extensions Settings",
"component": "views/cipp/Extensions",
"allowedRoles": ["admin"]
},
{
"path": "/cipp/setup",
"name": "Setup",
5 changes: 5 additions & 0 deletions src/store/features/app.js
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ const initialState = {
defaultColumns: {},
newUserDefaults: {},
recentPages: [],
setupCompleted: false,
}

export const appSlice = createSlice({
@@ -62,6 +63,9 @@ export const appSlice = createSlice({
setRecentPages: (state, action) => {
state.recentPages = action.payload?.recentPages
},
setSetupCompleted: (state, action) => {
state.setupCompleted = action.payload?.setupCompleted
},
},
})

@@ -80,6 +84,7 @@ export const {
setDefaultColumns,
setNewUserDefaults,
setRecentPages,
setSetupCompleted,
} = appSlice.actions

export default persistReducer(
12 changes: 10 additions & 2 deletions src/store/middleware/errorMiddleware.js
Original file line number Diff line number Diff line change
@@ -2,17 +2,25 @@
// set action.hideToastError to `true` to ignore this middleware
import { showToast } from 'src/store/features/toasts'
import { isRejectedWithValue } from '@reduxjs/toolkit'
import { store } from '../store'

export const errorMiddleware =
({ dispatch }) =>
(next) =>
(action) => {
const { getState } = store
const state = getState()
const setupCompleted = state.app?.setupCompleted
let SamWizardError = false
if (action?.meta?.arg?.originalArgs?.path === '/api/ExecSamSetup') {
SamWizardError = true
}
if (
isRejectedWithValue(action) &&
!action.error?.hideToastError &&
action.payload.message !== 'canceled'
action.payload.message !== 'canceled' &&
(setupCompleted || SamWizardError)
) {
console.error(action)
if (action.payload.data === 'Backend call failure') {
action.payload.data =
'The Azure Function has taken too long to respond. Try selecting a different report or a single tenant instead'
193 changes: 193 additions & 0 deletions src/views/cipp/Extensions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import React, { useRef, useState } from 'react'
import {
CButton,
CCardText,
CCol,
CForm,
CNav,
CNavItem,
CRow,
CTabContent,
CTabPane,
} from '@coreui/react'
import { CippCallout, CippPage } from 'src/components/layout'
import { CippLazy } from 'src/components/utilities'
import { useNavigate } from 'react-router-dom'
import useQuery from 'src/hooks/useQuery.jsx'
import Extensions from 'src/data/Extensions.json'
import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
import CippButtonCard from 'src/components/contentcards/CippButtonCard.jsx'
import { RFFCFormInput, RFFCFormSwitch } from 'src/components/forms/RFFComponents.jsx'
import { Form } from 'react-final-form'
import { SettingsExtensionMappings } from './app-settings/SettingsExtensionMappings'

export default function CIPPExtensions() {
const [listBackend, listBackendResult] = useLazyGenericGetRequestQuery()
const inputRef = useRef(null)
const [setExtensionconfig, extensionConfigResult] = useLazyGenericPostRequestQuery()
const [execTestExtension, listExtensionTestResult] = useLazyGenericGetRequestQuery()
const [execSyncExtension, listSyncExtensionResult] = useLazyGenericGetRequestQuery()

const onSubmitTest = (integrationName) => {
execTestExtension({
path: 'api/ExecExtensionTest?extensionName=' + integrationName,
})
}
const onSubmit = (values) => {
setExtensionconfig({
path: 'api/ExecExtensionsConfig',
values: values,
})
}

const ButtonGenerate = (integrationType, forceSync) => (
<>
<CButton className="me-2" form={integrationType} type="submit">
{extensionConfigResult.isFetching && (
<FontAwesomeIcon icon={faCircleNotch} spin className="me-2" size="1x" />
)}
Set Extension Settings
</CButton>
<CButton onClick={() => onSubmitTest(integrationType)} className="me-2">
{listExtensionTestResult.isFetching && (
<FontAwesomeIcon icon={faCircleNotch} spin className="me-2" size="1x" />
)}
Test Extension
</CButton>
{forceSync && (
<CButton
onClick={() =>
execSyncExtension({
path: 'api/ExecExtensionSync?Extension=' + integrationType,
})
}
className="me-2"
>
{listSyncExtensionResult.isFetching && (
<FontAwesomeIcon icon={faCircleNotch} spin className="me-2" size="1x" />
)}
Force Sync
</CButton>
)}
</>
)
const queryString = useQuery()
const navigate = useNavigate()

const tab = queryString.get('tab')
const [active, setActiveTab] = useState(tab ? parseInt(tab) : 0)
const setActive = (tab) => {
setActiveTab(tab)
queryString.set('tab', tab.toString())
navigate(`${location.pathname}?${queryString}`)
}

return (
<CippPage title="Settings" tenantSelector={false}>
{listBackendResult.isUninitialized && listBackend({ path: 'api/ListExtensionsConfig' })}
<CNav variant="tabs" role="tablist">
{Extensions.map((integration, idx) => (
<CNavItem
key={`tab-${idx}`}
active={active === idx}
onClick={() => setActive(idx)}
href="#"
>
{integration.name}
</CNavItem>
))}
</CNav>
<CTabContent>
{Extensions.map((integration, idx) => (
<CTabPane key={`pane-${idx}`} visible={active === idx} className="mt-3">
<CippLazy visible={active === idx}>
<CRow className="mb-3">
<CCol sm={12} md={integration.mappingRequired ? 4 : 12} className="mb-3">
<CippButtonCard
title={integration.name}
titleType="big"
isFetching={listBackendResult.isFetching}
CardButton={ButtonGenerate(integration.type, integration.forceSync)}
key={idx}
>
<p>{integration.helpText}</p>
<Form
onSubmit={onSubmit}
initialValues={listBackendResult.data}
render={({ handleSubmit, submitting, values }) => {
return (
<CForm id={integration.type} onSubmit={handleSubmit}>
<CCardText>
<CCol className="mb-3">
{integration.SettingOptions.map(
(integrationOptions, idx) =>
integrationOptions.type === 'input' && (
<CCol key={`${idx}-${integrationOptions.name}`}>
<RFFCFormInput
type={integrationOptions.fieldtype}
name={integrationOptions.name}
label={integrationOptions.label}
placeholder={integrationOptions.placeholder}
/>
</CCol>
),
)}
{integration.SettingOptions.map(
(integrationOptions, idx) =>
integrationOptions.type === 'checkbox' && (
<CCol key={`${integrationOptions.name}-${idx}`}>
<RFFCFormSwitch
name={integrationOptions.name}
label={integrationOptions.label}
value={false}
/>
</CCol>
),
)}
<input
ref={inputRef}
type="hidden"
name="type"
value={integration.type}
/>
</CCol>
</CCardText>
</CForm>
)
}}
/>
{extensionConfigResult?.data?.Results && (
<CippCallout color={extensionConfigResult.isSuccess ? 'success' : 'danger'}>
{extensionConfigResult?.data?.Results}
</CippCallout>
)}
{listExtensionTestResult?.data?.Results && (
<CippCallout color={listExtensionTestResult.isSuccess ? 'success' : 'danger'}>
{listExtensionTestResult?.data?.Results}
{listExtensionTestResult?.data?.Link && (
<a
href={listExtensionTestResult?.data?.Link}
target="_blank"
rel="noreferrer"
className="ms-2"
>
Link
</a>
)}
</CippCallout>
)}
</CippButtonCard>
</CCol>
<CCol sm={12} md={8}>
<SettingsExtensionMappings type={integration.type} />
</CCol>
</CRow>
</CippLazy>
</CTabPane>
))}
</CTabContent>
</CippPage>
)
}
40 changes: 29 additions & 11 deletions src/views/cipp/Scheduler.jsx
Original file line number Diff line number Diff line change
@@ -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}
/>
) : (
<RFFCFormInput
type="text"
key={idx}
name={`parameters.${param.Name}`}
label={`${param.Name}`}
/>
<>
{param.Type === 'System.String[]' ? (
<RFFCFormInputList
name={`parameters.${param.Name}[]`}
label={`${param.Name}`}
key={idx}
/>
) : (
<RFFCFormInput
type="text"
key={idx}
name={`parameters.${param.Name}`}
label={`${param.Name}`}
/>
)}
</>
)}
</>
)}
20 changes: 1 addition & 19 deletions src/views/cipp/app-settings/CIPPSettings.jsx
Original file line number Diff line number Diff line change
@@ -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() {
<CNavItem active={active === 7} onClick={() => setActive(7)} href="#">
Maintenance
</CNavItem>
<CNavItem active={active === 8} onClick={() => setActive(8)} href="#">
Extensions
</CNavItem>
<CNavItem active={active === 9} onClick={() => setActive(9)} href="#">
Extension Mappings
</CNavItem>
{superAdmin && (
<CNavItem active={active === 10} onClick={() => setActive(10)} href="#">
<CNavItem active={active === 8} onClick={() => setActive(8)} href="#">
SuperAdmin Settings
</CNavItem>
)}
@@ -106,16 +98,6 @@ export default function CIPPSettings() {
</CTabPane>
<CTabPane visible={active === 8} className="mt-3">
<CippLazy visible={active === 8}>
<SettingsExtensions />
</CippLazy>
</CTabPane>
<CTabPane visible={active === 9} className="mt-3">
<CippLazy visible={active === 9}>
<SettingsExtensionMappings />
</CippLazy>
</CTabPane>
<CTabPane visible={active === 10} className="mt-3">
<CippLazy visible={active === 10}>
<SettingsSuperAdmin />
</CippLazy>
</CTabPane>
619 changes: 319 additions & 300 deletions src/views/cipp/app-settings/SettingsExtensionMappings.jsx

Large diffs are not rendered by default.

16 changes: 1 addition & 15 deletions src/views/cipp/app-settings/SettingsExtensions.jsx
Original file line number Diff line number Diff line change
@@ -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'
58 changes: 29 additions & 29 deletions src/views/cipp/app-settings/SettingsGeneral.jsx
Original file line number Diff line number Diff line change
@@ -253,37 +253,37 @@ export function SettingsGeneral() {
</CCallout>
</CCol>
<CCol>
{permissionsResult.data.Results?.ErrorMessages?.length > 0 ||
(permissionsResult.data.Results?.MissingPermissions.length > 0 && (
<CCallout color="danger">
{(permissionsResult.data.Results?.ErrorMessages?.length > 0 ||
permissionsResult.data.Results?.MissingPermissions.length > 0) && (
<CCallout color="danger">
<>
{permissionsResult.data.Results?.ErrorMessages?.map((m, idx) => (
<div key={idx}>{m}</div>
))}
</>
{permissionsResult.data.Results?.MissingPermissions.length > 0 && (
<>
{permissionsResult.data.Results?.ErrorMessages?.map((m, idx) => (
<div key={idx}>{m}</div>
))}
Your Secure Application Model is missing the following permissions. See
the documentation on how to add permissions{' '}
<a
target="_blank"
rel="noreferrer"
href="https://docs.cipp.app/setup/installation/permissions#manual-permissions"
>
here
</a>
.
<CListGroup flush>
{permissionsResult.data.Results?.MissingPermissions?.map(
(r, index) => (
<CListGroupItem key={index}>{r}</CListGroupItem>
),
)}
</CListGroup>
</>
{permissionsResult.data.Results?.MissingPermissions.length > 0 && (
<>
Your Secure Application Model is missing the following permissions.
See the documentation on how to add permissions{' '}
<a
target="_blank"
rel="noreferrer"
href="https://docs.cipp.app/setup/installation/permissions#manual-permissions"
>
here
</a>
.
<CListGroup flush>
{permissionsResult.data.Results?.MissingPermissions?.map(
(r, index) => (
<CListGroupItem key={index}>{r}</CListGroupItem>
),
)}
</CListGroup>
</>
)}
</CCallout>
))}
)}
</CCallout>
)}
</CCol>
</CRow>
</>
127 changes: 66 additions & 61 deletions src/views/cipp/app-settings/SettingsSuperAdmin.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<CippButtonCard
title="Super Admin Configuration"
titleType="big"
isFetching={partnerConfig.isFetching}
CardButton={buttonCard}
>
<>
<>
<CippButtonCard
title="Tenant Mode"
titleType="big"
isFetching={partnerConfig.isFetching}
CardButton={buttonCard}
>
<>
<CRow>
<CCol sm={12} md={6} lg={8} className="mb-3">
<p className="me-1">
The configuration settings below should only be modified by a super admin. Super
admins can configure what tenant mode CIPP operates in. See
<CLink
href="https://docs.cipp.app/setup/installation/owntenant"
target="_blank"
className="m-1"
>
our documentation
</CLink>
for more information on how to configure these modes and what they mean.
</p>
</CCol>
</CRow>
<CRow>
<CCol sm={12} md={12} className="mb-3">
<p className="fw-lighter">Tenant Mode</p>
<Form
onSubmit={onSubmit}
initialValues={partnerConfig.data}
render={({ handleSubmit }) => (
<>
{partnerConfig.isFetching && <CSpinner size="sm" className="me-2" />}
<CForm id="submitForm" onSubmit={handleSubmit}>
<RFFCFormRadio
name="TenantMode"
label="Multi Tenant - GDAP Mode"
value="default"
/>
<RFFCFormRadio
name="TenantMode"
label="Multi Tenant - Add Partner Tenant"
value="PartnerTenantAvailable"
/>
<RFFCFormRadio
name="TenantMode"
label="Single Tenant - Own Tenant Mode"
value="owntenant"
/>
</CForm>
</>
<>
<CRow>
<CCol sm={12} md={6} lg={8} className="mb-3">
<p className="me-1">
The configuration settings below should only be modified by a super admin. Super
admins can configure what tenant mode CIPP operates in. See
<CLink
href="https://docs.cipp.app/setup/installation/owntenant"
target="_blank"
className="m-1"
>
our documentation
</CLink>
for more information on how to configure these modes and what they mean.
</p>
</CCol>
</CRow>
<CRow>
<CCol sm={12} md={12} className="mb-3">
<p className="fw-lighter">Tenant Mode</p>
<Form
onSubmit={onSubmit}
initialValues={partnerConfig.data}
render={({ handleSubmit }) => (
<>
{partnerConfig.isFetching && <CSpinner size="sm" className="me-2" />}
<CForm id="submitForm" onSubmit={handleSubmit}>
<RFFCFormRadio
name="TenantMode"
label="Multi Tenant - GDAP Mode"
value="default"
/>
<RFFCFormRadio
name="TenantMode"
label="Multi Tenant - Add Partner Tenant"
value="PartnerTenantAvailable"
/>
<RFFCFormRadio
name="TenantMode"
label="Single Tenant - Own Tenant Mode"
value="owntenant"
/>
</CForm>
</>
)}
/>
{webhookCreateResult.isSuccess && (
<CippCallout color="info" dismissible>
{webhookCreateResult?.data?.results}
</CippCallout>
)}
/>
{webhookCreateResult.isSuccess && (
<CippCallout color="info" dismissible>
{webhookCreateResult?.data?.results}
</CippCallout>
)}
</CCol>
</CRow>
</CCol>
</CRow>
</>
</>
</>
</CippButtonCard>
</CippButtonCard>
<SettingsCustomRoles />
</>
)
}
489 changes: 489 additions & 0 deletions src/views/cipp/app-settings/components/SettingsCustomRoles.jsx

Large diffs are not rendered by default.

36 changes: 30 additions & 6 deletions src/views/cipp/app-settings/components/SettingsGeneralRow.jsx
Original file line number Diff line number Diff line change
@@ -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,13 +15,15 @@ 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.
*
* @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: <div>Are you sure you want to clear the cache?</div>,
@@ -115,6 +120,17 @@ export function SettingsGeneralRow() {
)}
Restore backup
</CButton>
<CButton
className="me-2"
name="file"
onClick={() => handleBackupSchedule()}
disabled={BackupScheduleResult.isFetching}
>
{BackupScheduleResult.isFetching && (
<FontAwesomeIcon icon={faCircleNotch} spin className="me-2" size="1x" />
)}
Create Automated Backup Task
</CButton>
</>
)
return (
@@ -179,16 +195,24 @@ export function SettingsGeneralRow() {
id="contained-button-file"
onChange={(e) => handleChange(e)}
/>
<small>
Use this button to backup the system configuration for CIPP. This will not include
authentication information or extension configuration.
</small>

<CRow className="mb-3">
<small>
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.
</small>
</CRow>
{restoreBackupResult.isSuccess && !restoreBackupResult.isFetching && (
<CippCallout color="success" dismissible>
{restoreBackupResult.data.Results}
</CippCallout>
)}
{BackupScheduleResult.isSuccess && !BackupScheduleResult.isFetching && (
<CippCallout color="success" dismissible>
{BackupScheduleResult.data.Results}
</CippCallout>
)}
{RunBackupResult.isSuccess && !restoreBackupResult.isFetching && (
<CippCallout color="success" dismissible>
<CButton onClick={() => downloadTxtFile(RunBackupResult.data.backup)}>
14 changes: 14 additions & 0 deletions src/views/email-exchange/administration/QuarantineList.jsx
Original file line number Diff line number Diff line change
@@ -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',
9 changes: 8 additions & 1 deletion src/views/email-exchange/spamfilter/DeploySpamfilter.jsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
/>
</CCol>
</CRow>
<CRow>
<CCol>
<RFFCFormInput name="Priority" label="SpamFilter priority" placeholder={'0'} />
</CCol>
</CRow>
<hr className="my-4" />
<WhenFieldChanges field="TemplateList" set="PowerShellCommand" />
</CippWizard.Page>
@@ -179,6 +184,8 @@ const SpamFilterAdd = () => {
</CCallout>
<h5 className="mb-0">Rule Settings</h5>
<CCallout color="info">{props.values.PowerShellCommand}</CCallout>
<h5 className="mb-0">Priority</h5>
<CCallout color="info">{props.values.Priority}</CCallout>
</CCol>
</CRow>
</>
36 changes: 34 additions & 2 deletions src/views/endpoint/autopilot/AutopilotAddDevice.jsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
<CSpinner>Loading</CSpinner>
</CCallout>
)}
{postResults.isSuccess && <CCallout color="success">{postResults.data.Results}</CCallout>}
{autopilotData && (
{postResults.isSuccess && (
<>
<CCallout color="success">{postResults.data?.Results?.Status}</CCallout>
<CippTable
reportName="none"
tableProps={{ subheader: false }}
data={postResults.data?.Results?.Devices}
columns={completeColumns}
/>
</>
)}
{autopilotData && !postResults.isSuccess && (
<CippTable
reportName="none"
tableProps={{ subheader: false }}
75 changes: 46 additions & 29 deletions src/views/endpoint/autopilot/AutopilotAddProfile.jsx
Original file line number Diff line number Diff line change
@@ -6,8 +6,9 @@ import { faCheck, faExclamationTriangle, faTimes } from '@fortawesome/free-solid
import { CippWizard } from 'src/components/layout'
import { WizardTableField } from 'src/components/tables'
import PropTypes from 'prop-types'
import { RFFCFormInput, RFFCFormSwitch } from 'src/components/forms'
import { RFFCFormInput, RFFCFormSwitch, RFFSelectSearch } from 'src/components/forms'
import { useLazyGenericPostRequestQuery } from 'src/store/api/app'
import langaugeList from 'src/data/languageList'

const Error = ({ name }) => (
<Field
@@ -112,7 +113,20 @@ const ApplyStandard = () => {
</CCol>
</CRow>
<CRow>
<CCol md={12}>
<CCol md={12} className="mb-2">
<RFFSelectSearch
values={langaugeList.map(({ language, tag }) => ({
value: tag,
name: language,
}))}
name="languages"
multi={false}
label="Languages"
/>
</CCol>
</CRow>
<CRow>
<CCol md={12} className="mb-2">
<RFFCFormInput
type="text"
name="Description"
@@ -122,41 +136,44 @@ const ApplyStandard = () => {
</CCol>
</CRow>
<CRow>
<CCol md={12}>
<CCol md={12} className="mb-2">
<RFFCFormInput
type="text"
name="DeviceNameTemplate"
label="Unique name template"
placeholder="leave blank for none"
/>
<br></br>
</CCol>
</CRow>
<RFFCFormSwitch
value={true}
name="CollectHash"
label="Convert all targeted devices to Autopilot"
/>
<RFFCFormSwitch value={true} name="Assignto" label="Assign to all devices" />
<RFFCFormSwitch value={true} name="DeploymentMode" label="Self-deploying mode" />
<RFFCFormSwitch value={true} name="HideTerms" label="Hide Terms and conditions" />
<RFFCFormSwitch value={true} name="HidePrivacy" label="Hide Privacy Settings" />
<RFFCFormSwitch
value={true}
name="HideChangeAccount"
label="Hide Change Account Options"
/>
<RFFCFormSwitch
value={true}
name="NotLocalAdmin"
label="Setup user as standard user (Leave unchecked to setup user as local admin)"
/>
<RFFCFormSwitch value={true} name="allowWhiteglove" label="Allow White Glove OOBE" />
<RFFCFormSwitch
value={true}
name="Autokeyboard"
label="Automatically configure keyboard"
/>
<CRow>
<CCol md={12} className="mb-2">
<RFFCFormSwitch
value={true}
name="CollectHash"
label="Convert all targeted devices to Autopilot"
/>
<RFFCFormSwitch value={true} name="Assignto" label="Assign to all devices" />
<RFFCFormSwitch value={true} name="DeploymentMode" label="Self-deploying mode" />
<RFFCFormSwitch value={true} name="HideTerms" label="Hide Terms and conditions" />
<RFFCFormSwitch value={true} name="HidePrivacy" label="Hide Privacy Settings" />
<RFFCFormSwitch
value={true}
name="HideChangeAccount"
label="Hide Change Account Options"
/>
<RFFCFormSwitch
value={true}
name="NotLocalAdmin"
label="Setup user as standard user (Leave unchecked to setup user as local admin)"
/>
<RFFCFormSwitch value={true} name="allowWhiteglove" label="Allow White Glove OOBE" />
<RFFCFormSwitch
value={true}
name="Autokeyboard"
label="Automatically configure keyboard"
/>
</CCol>
</CRow>
</CForm>
<hr className="my-4" />
</CippWizard.Page>
11 changes: 10 additions & 1 deletion src/views/endpoint/intune/MEMListAppProtection.jsx
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ const Actions = (row, rowIndex, formatExtraData) => {
color: 'danger',
modal: true,
icon: <FontAwesomeIcon icon={faTrashAlt} className="me-2" />,
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?',
},
],
},
}}
/>
11 changes: 10 additions & 1 deletion src/views/endpoint/intune/MEMListCompliance.jsx
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ const Actions = (row, rowIndex, formatExtraData) => {
color: 'danger',
modal: true,
icon: <FontAwesomeIcon icon={faTrashAlt} className="me-2" />,
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?',
},
],
},
}}
/>
9 changes: 9 additions & 0 deletions src/views/endpoint/intune/MEMListPolicies.jsx
Original file line number Diff line number Diff line change
@@ -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?',
},
],
},
}}
/>
3 changes: 1 addition & 2 deletions src/views/home/Home.jsx
Original file line number Diff line number Diff line change
@@ -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'

340 changes: 340 additions & 0 deletions src/views/identity/administration/DeployJITAdmin.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<CippPage title={`Add JIT Admin`} tenantSelector={false}>
<>
<CRow className="mb-3">
<CCol lg={4} md={12}>
<CippContentCard title="Add JIT Admin" icon={faEdit}>
<Form
onSubmit={onSubmit}
mutators={{
...arrayMutators,
}}
render={({ handleSubmit, submitting, values }) => {
return (
<CForm onSubmit={handleSubmit}>
<p>
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.
</p>
<CRow className="mb-3">
<CCol>
<label className="mb-2">Tenant</label>
<Field name="tenantFilter">
{(props) => <TenantSelector showAllTenantSelector={false} />}
</Field>
</CCol>
</CRow>
<CRow>
<hr />
</CRow>
<CRow className="mb-3">
<CCol>
<RFFCFormRadioList
name="useraction"
options={[
{ value: 'create', label: 'New User' },
{ value: 'select', label: 'Existing User' },
]}
validate={false}
inline={true}
className=""
/>
</CCol>
</CRow>
<Condition when="useraction" is="create">
<CRow>
<CCol>
<RFFCFormInput label="First Name" name="FirstName" />
</CCol>
</CRow>
<CRow>
<CCol>
<RFFCFormInput label="Last Name" name="LastName" />
</CCol>
</CRow>
<CRow>
<CCol>
<RFFCFormInput label="User Principal Name" name="UserPrincipalName" />
</CCol>
</CRow>
</Condition>
<Condition when="useraction" is="select">
<CRow className="mb-3">
<CCol>
<RFFSelectSearch
label={'Users in ' + tenantDomain}
values={users?.Results?.map((user) => ({
value: user.id,
name: `${user.displayName} <${user.userPrincipalName}>`,
}))}
placeholder={!usersIsFetching ? 'Select user' : 'Loading...'}
name="UserId"
isLoading={usersIsFetching}
/>
<FormSpy subscription={{ values: true }}>
{({ values }) => {
return users?.Results?.map((user, key) => {
if (
user.id === values?.UserId?.value &&
user.accountEnabled === false
) {
return (
<CCallout color="warning" key={key} className="mt-3">
This user is currently disabled, they will automatically be
enabled when JIT is executed.
</CCallout>
)
}
})
}}
</FormSpy>
</CCol>
</CRow>
</Condition>
<hr />
<CRow className="mb-3">
<CCol>
<RFFSelectSearch
label="Administrative Roles"
values={GDAPRoles?.map((role) => ({
value: role.ObjectId,
name: role.Name,
}))}
multi={true}
placeholder="Select Roles"
name="AdminRoles"
/>
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<label>Scheduled Start Date</label>
<DatePicker
className="form-control"
selected={startDate}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="Pp"
onChange={(date) => setStartDate(date)}
/>
</CCol>
<CCol>
<label>Scheduled End Date</label>
<DatePicker
className="form-control"
selected={endDate}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="Pp"
onChange={(date) => setEndDate(date)}
/>
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<RFFSelectSearch
label="Expiration Action"
values={[
{ value: 'RemoveRoles', name: 'Remove Admin Roles' },
{ value: 'DisableUser', name: 'Disable User' },
{ value: 'DeleteUser', name: 'Delete User' },
]}
placeholder="Select action for when JIT expires"
name="expireAction"
/>
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<CTooltip content="Generate a Temporary Access Password for the JIT Admin account if enabled for the tenant. This applies to both New and Existing users. The start time coincides with the scheduled time.">
<div>
<RFFCFormSwitch name="useTap" label="Generate TAP" />
</div>
</CTooltip>
</CCol>
</CRow>
<CRow className="mb-3">
<CCol>
<label>Send results to</label>
<RFFCFormSwitch name="webhook" label="Webhook" />
<RFFCFormSwitch name="email" label="E-mail" />
<RFFCFormSwitch name="psa" label="PSA" />
</CCol>
</CRow>
<CRow className="mb-3">
<CCol md={6}>
<CButton type="submit" disabled={submitting}>
Add JIT Admin
{postResults.isFetching && (
<FontAwesomeIcon
icon={faCircleNotch}
spin
className="ms-2"
size="1x"
/>
)}
</CButton>
</CCol>
</CRow>
{postResults.isSuccess && (
<CCallout color="success">
{postResults.data?.Results.map((result, idx) => (
<li key={idx}>{result}</li>
))}
</CCallout>
)}
{getResults.isFetching && (
<CCallout color="info">
<CSpinner>Loading</CSpinner>
</CCallout>
)}
{getResults.isSuccess && (
<CCallout color="info">{getResults.data?.Results}</CCallout>
)}
{getResults.isError && (
<CCallout color="danger">
Could not connect to API: {getResults.error.message}
</CCallout>
)}
</CForm>
)
}}
/>
</CippContentCard>
</CCol>
<CCol lg={8} md={12}>
<CippContentCard title="JIT Admins" icon="user-shield">
<CippDatatable
title="JIT Admins"
path="/api/ExecJITAdmin?Action=List"
params={{ TenantFilter: tenantDomain, refreshState }}
columns={[
{
name: 'User',
selector: (row) => 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',
},
]}
/>
</CippContentCard>
</CCol>
</CRow>
</>
</CippPage>
)
}

export default DeployJITAdmin
2 changes: 1 addition & 1 deletion src/views/identity/administration/EditGroup.jsx
Original file line number Diff line number Diff line change
@@ -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) => {
47 changes: 26 additions & 21 deletions src/views/identity/administration/OffboardingWizard.jsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
<RFFSelectSearch
multi
label={'Users in ' + tenantDomain}
values={users?.map((user) => ({
values={users?.Results?.map((user) => ({
value: user.userPrincipalName,
name: `${user.displayName} <${user.userPrincipalName}>`,
}))}
@@ -177,36 +188,30 @@ const OffboardingWizard = () => {
<RFFSelectSearch
label="Give other user full access on mailbox without automapping"
multi
values={users
?.filter((x) => 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"
/>
<RFFSelectSearch
label="Give other user full access on mailbox with automapping"
multi
values={users
?.filter((x) => 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"
/>
<RFFSelectSearch
label="Give other user full access on Onedrive"
multi
values={users
?.filter((x) => 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 = () => {
>
<h5 className="mb-0">Selected User:</h5>
<span>
{users.find((x) => x.userPrincipalName === user.value)
{users.Results?.find((x) => x.userPrincipalName === user.value)
.onPremisesSyncEnabled === true ? (
<CTooltip content="This user is AD sync enabled, offboarding will fail for some steps">
<FontAwesomeIcon
103 changes: 103 additions & 0 deletions src/views/identity/administration/RiskyUsers.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useSelector } from 'react-redux'
import { CippPageList } from 'src/components/layout'

const columns = [
{
name: 'Risk Last Updated Date',
selector: (row) => 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 (
<>
<CippPageList
title="Risky Users"
capabilities={{ allTenants: true, helpContext: 'https://google.com' }}
datatable={{
filterlist: [
{
filterName: 'State: none',
filter: 'Complex: riskState eq none',
},
{
filterName: 'State: atRisk',
filter: 'Complex: riskState eq atRisk',
},
{
filterName: 'State: confirmedCompromised',
filter: 'Complex: riskState eq confirmedCompromised',
},
{
filterName: 'State: confirmedSafe',
filter: 'Complex: riskState eq confirmedSafe',
},
{
filterName: 'State: dismissed',
filter: 'Complex: riskState eq dismissed',
},
{
filterName: 'State: remediated',
filter: 'Complex: riskState eq remediated',
},
{
filterName: 'State: unknownFutureValue',
filter: 'Complex: riskState eq unknownFutureValue',
},
],
columns: columns,
path: `api/ListGraphRequest`,
reportName: `${tenant?.defaultDomainName}-ListRiskyUsers`,
params: {
TenantFilter: tenant?.defaultDomainName,
Endpoint: `identityProtection/riskyUsers`,
$count: true,
$orderby: 'riskLastUpdatedDateTime',
},
}}
/>
</>
)
}

export default RiskyUsers
27 changes: 26 additions & 1 deletion src/views/identity/administration/Roles.jsx
Original file line number Diff line number Diff line change
@@ -23,7 +23,12 @@ const Offcanvas = (row, rowIndex, formatExtraData) => {
>
<h5>Role Group Name:</h5> {row.DisplayName}
<br></br> <br></br>
<h5>Member Names:</h5> {row.Members ? <p>{row.Members}</p> : <p>Role has no members.</p>}
<h5>Member Names:</h5>{' '}
{row.Members ? (
row.Members.split(',').map((member, index) => <p key={index}>{member}</p>)
) : (
<p>Role has no members.</p>
)}
</CippOffcanvas>
</>
)
@@ -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',
45 changes: 43 additions & 2 deletions src/views/identity/administration/Users.jsx
Original file line number Diff line number Diff line change
@@ -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',
24 changes: 16 additions & 8 deletions src/views/identity/reports/MFAReport.jsx
Original file line number Diff line number Diff line change
@@ -2,42 +2,56 @@ 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 = [
{
selector: (row) => row['UPN'],
name: 'User Principal Name',
sortable: true,
exportSelector: 'UPN',
grow: 2,
cell: (row) => CellTip(row['UPN']),
maxWidth: '400px',
},
{
selector: (row) => row['AccountEnabled'],
name: 'Account Enabled',
sortable: true,
cell: cellBooleanFormatter({ colourless: true }),
exportSelector: 'AccountEnabled',
maxWidth: '200px',
},
{
selector: (row) => row['isLicensed'],
name: 'Account Licensed',
sortable: true,
cell: cellBooleanFormatter({ colourless: true }),
exportSelector: 'isLicensed',
maxWidth: '200px',
},
{
selector: (row) => row['MFARegistration'],
name: 'Registered for Conditional MFA',
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 = [
125 changes: 125 additions & 0 deletions src/views/identity/reports/RiskDetections.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<CippPageList
title="Risk Detection Report"
capabilities={{ allTenants: true, helpContext: 'https://google.com' }}
datatable={{
filterlist: [
{
filterName: 'State: atRisk',
filter: 'Complex: riskState eq atRisk',
},
{
filterName: 'State: confirmedCompromised',
filter: 'Complex: riskState eq confirmedCompromised',
},
{
filterName: 'State: confirmedSafe',
filter: 'Complex: riskState eq confirmedSafe',
},
{
filterName: 'State: dismissed',
filter: 'Complex: riskState eq dismissed',
},
{
filterName: 'State: remediated',
filter: 'Complex: riskState eq remediated',
},
{
filterName: 'State: unknownFutureValue',
filter: 'Complex: riskState eq unknownFutureValue',
},
],
columns: columns,
path: `api/ListGraphRequest`,
reportName: `${tenant?.defaultDomainName}-RiskDetections-Report`,
params: {
TenantFilter: tenant?.defaultDomainName,
Endpoint: `identityProtection/riskDetections`,
$count: true,
$orderby: 'detectedDateTime',
},
}}
/>
</>
)
}

export default RiskDetections
Loading

0 comments on commit b43f20d

Please sign in to comment.