diff --git a/src/_nav.jsx b/src/_nav.jsx index e6567546bf11..631d6eaf1169 100644 --- a/src/_nav.jsx +++ b/src/_nav.jsx @@ -19,6 +19,7 @@ import { faUserShield, faEnvelope, faToolbox, + faDownload, } from '@fortawesome/free-solid-svg-icons' const _nav = [ @@ -184,6 +185,25 @@ const _nav = [ }, ], }, + { + component: CNavGroup, + name: 'Configuration Backup', + section: 'Tenant Administration', + to: '/cipp/gdap', + icon: , + items: [ + { + component: CNavItem, + name: 'Backup Wizard', + to: '/tenant/backup/backup-wizard', + }, + { + component: CNavItem, + name: 'Restore Wizard', + to: '/tenant/backup/restore-wizard', + }, + ], + }, { component: CNavGroup, name: 'Tools', diff --git a/src/importsMap.jsx b/src/importsMap.jsx index 63ae50439b0d..9666488abfc9 100644 --- a/src/importsMap.jsx +++ b/src/importsMap.jsx @@ -34,6 +34,8 @@ import React from 'react' "/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/backup/backup-wizard": React.lazy(() => import('./views/tenant/backup/CreateBackup')), + "/tenant/backup/restore-wizard": React.lazy(() => import('./views/tenant/backup/RestoreBackup')), "/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')), @@ -46,6 +48,7 @@ import React from 'react' "/tenant/administration/enterprise-apps": React.lazy(() => import('./views/tenant/administration/ListEnterpriseApps')), "/tenant/administration/app-consent-requests": React.lazy(() => import('./views/tenant/administration/ListAppConsentRequests')), "/tenant/conditional/list-policies": React.lazy(() => import('./views/tenant/conditional/ConditionalAccess')), + "/tenant/administration/authentication-methods": React.lazy(() => import('./views/tenant/administration/AuthMethods')), "/tenant/conditional/deploy-vacation": React.lazy(() => import('./views/tenant/conditional/DeployVacation')), "/tenant/conditional/test-policy": React.lazy(() => import('./views/tenant/conditional/TestCAPolicy')), "/tenant/conditional/list-named-locations": React.lazy(() => import('./views/tenant/conditional/NamedLocations')), @@ -53,7 +56,6 @@ import React from 'react' "/tenant/conditional/deploy-named-location": React.lazy(() => import('./views/tenant/conditional/DeployNamedLocation')), "/tenant/conditional/list-template": React.lazy(() => import('./views/tenant/conditional/ListCATemplates')), "/tenant/conditional/add-template": React.lazy(() => import('./views/tenant/conditional/AddCATemplate')), - "/tenant/administration/authentication-methods": React.lazy(() => import('./views/tenant/administration/AuthMethods')), "/tenant/administration/list-licenses": React.lazy(() => import('./views/tenant/administration/ListLicences')), "/tenant/administration/application-consent": React.lazy(() => import('./views/tenant/administration/ListOauthApps')), "/tenant/standards/list-applied-standards": React.lazy(() => import('./views/tenant/standards/ListAppliedStandards')), @@ -117,8 +119,8 @@ import React from 'react' "/email/administration/edit-calendar-permissions": React.lazy(() => import('./views/email-exchange/administration/EditCalendarPermissions')), "/email/administration/view-mobile-devices": React.lazy(() => import('./views/email-exchange/administration/ViewMobileDevices')), "/email/administration/edit-contact": React.lazy(() => import('./views/email-exchange/administration/EditContact')), - "/email/administration/mailboxes": React.lazy(() => import('./views/email-exchange/administration/MailboxesList')), - "/email/administration/deleted-mailboxes": React.lazy(() => import('./views/email-exchange/administration/DeletedMailboxes')), + "/email/administration/mailboxes": React.lazy(() => import('./views/email-exchange/administration/MailboxesList')), + "/email/administration/deleted-mailboxes": React.lazy(() => import('./views/email-exchange/administration/DeletedMailboxes')), "/email/administration/mailbox-rules": React.lazy(() => import('./views/email-exchange/administration/MailboxRuleList')), "/email/administration/Quarantine": React.lazy(() => import('./views/email-exchange/administration/QuarantineList')), "/email/administration/tenant-allow-block-lists": React.lazy(() => import('./views/email-exchange/administration/ListTenantAllowBlockList')), diff --git a/src/routes.json b/src/routes.json index 669c385e6d3d..96584165d77c 100644 --- a/src/routes.json +++ b/src/routes.json @@ -228,6 +228,20 @@ "name": "Administration", "allowedRoles": ["admin", "editor", "readonly"] }, + { + "path": "/tenant/backup/backup-wizard", + "name": "Backup", + "component": "views/tenant/backup/CreateBackup", + + "allowedRoles": ["admin", "editor", "readonly"] + }, + { + "path": "/tenant/backup/restore-wizard", + "name": "Restore Backup", + "component": "views/tenant/backup/RestoreBackup", + + "allowedRoles": ["admin", "editor", "readonly"] + }, { "path": "/tenant/administration/tenants", "name": "Tenants", diff --git a/src/views/tenant/backup/CreateBackup.jsx b/src/views/tenant/backup/CreateBackup.jsx new file mode 100644 index 000000000000..b694fc225efd --- /dev/null +++ b/src/views/tenant/backup/CreateBackup.jsx @@ -0,0 +1,267 @@ +import React, { useState } from 'react' +import { CButton, CCallout, CCol, CForm, CRow, CSpinner, CTooltip } from '@coreui/react' +import { useSelector } from 'react-redux' +import { Field, Form } from 'react-final-form' +import { RFFCFormSwitch } 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 { CippPage, CippPageList } from 'src/components/layout' +import 'react-datepicker/dist/react-datepicker.css' +import { ModalService, TenantSelector } from 'src/components/utilities' +import arrayMutators from 'final-form-arrays' +import { useListConditionalAccessPoliciesQuery } from 'src/store/api/tenants' +import CippButtonCard from 'src/components/contentcards/CippButtonCard' +import { CellTip, cellGenericFormatter } from 'src/components/tables/CellGenericFormat' +import { cellBadgeFormatter, cellDateFormatter } from 'src/components/tables' + +const CreateBackup = () => { + 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 startDate = new Date() + startDate.setHours(0, 0, 0, 0) + //decrease by 45 seconds to ensure the task runs after the current time + const unixTime = Math.floor(startDate.getTime() / 1000) - 45 + const shippedValues = { + TenantFilter: tenantDomain, + Name: `CIPP Backup ${tenantDomain}`, + Command: { value: `New-CIPPBackup` }, + Parameters: { ...values }, + ScheduledTime: unixTime, + Recurrence: '1d', + } + genericPostRequest({ path: '/api/AddScheduledItem?hidden=true', values: shippedValues }).then( + (res) => { + setRefreshState(res.requestId) + }, + ) + } + const Offcanvas = (row, rowIndex, formatExtraData) => { + const handleDeleteSchedule = (apiurl, message) => { + ModalService.confirm({ + title: 'Confirm', + body:
{message}
, + onConfirm: () => + ExecuteGetRequest({ path: apiurl }).then((res) => { + setRefreshState(res.requestId) + }), + confirmLabel: 'Continue', + cancelLabel: 'Cancel', + }) + } + let jsonResults + try { + jsonResults = JSON.parse(row.Results) + } catch (error) { + jsonResults = row.Results + } + + return ( + <> + + + handleDeleteSchedule( + `/api/RemoveScheduledItem?&ID=${row.RowKey}`, + 'Do you want to delete this job?', + ) + } + size="sm" + variant="ghost" + color="danger" + > + + + + + ) + } + const columns = [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + cell: (row) => CellTip(row['Tenant']), + exportSelector: 'Tenant', + }, + { + name: 'Task State', + selector: (row) => row['TaskState'], + sortable: true, + cell: cellBadgeFormatter(), + exportSelector: 'TaskState', + }, + { + name: 'Last executed time', + selector: (row) => row['ExecutedTime'], + sortable: true, + cell: cellDateFormatter({ format: 'relative' }), + exportSelector: 'ExecutedTime', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '100px', + }, + ] + + 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', + }, + }) + + const { + data: caPolicies = [], + isFetching: caIsFetching, + error: caError, + } = useListConditionalAccessPoliciesQuery({ domain: tenantDomain }) + + return ( + + <> + + + + Create Backup Schedule + {postResults.isFetching && ( + + )} + + } + title="Add backup Schedule" + icon={faEdit} + > +
{ + return ( + +

+ Backups are stored in CIPPs storage and can be restored using the CIPP + Restore Backup Wizard. Backups run daily or on demand by clicking the backup + now button. +

+ + + + {(props) => } + + + +
+
+ + +

Identity

+ + +

Conditional Access

+ + + +

Intune

+ + + +

CIPP

+ + +
+
+ {postResults.isSuccess && ( + +
  • {postResults.data.Results}
  • +
    + )} + {getResults.isFetching && ( + + Loading + + )} + {getResults.isSuccess && ( + {getResults.data?.Results} + )} + {getResults.isError && ( + + Could not connect to API: {getResults.error.message} + + )} +
    + ) + }} + /> + + + + + + + + + + ) +} + +export default CreateBackup diff --git a/src/views/tenant/backup/RestoreBackup.jsx b/src/views/tenant/backup/RestoreBackup.jsx new file mode 100644 index 000000000000..161ddc2da6a3 --- /dev/null +++ b/src/views/tenant/backup/RestoreBackup.jsx @@ -0,0 +1,333 @@ +import React, { useState } from 'react' +import { CCallout, CCol, CListGroup, CListGroupItem, CRow, CSpinner, CTooltip } from '@coreui/react' +import { Field, FormSpy } from 'react-final-form' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExclamationTriangle, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons' +import { useSelector } from 'react-redux' +import { CippCallout, CippWizard } from 'src/components/layout' +import PropTypes from 'prop-types' +import { Condition, RFFCFormSwitch, RFFSelectSearch } from 'src/components/forms' +import { TenantSelector } from 'src/components/utilities' +import { useGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' +import 'react-datepicker/dist/react-datepicker.css' + +const Error = ({ name }) => ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +const OffboardingWizard = () => { + const tenantDomain = useSelector((state) => state.app.currentTenant.defaultDomainName) + const { + data: currentBackups = [], + isFetching: currentBackupsIsFetching, + error: currentBackupsError, + } = useGenericGetRequestQuery({ + path: `/api/ExecListBackup?TenantFilter=${tenantDomain}&Type=Scheduled`, + }) + + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + + const handleSubmit = async (values) => { + const shippedValues = { + TenantFilter: tenantDomain, + OOO: values.OOO ? values.OOO : '', + forward: values.forward ? values.forward.value : '', + OnedriveAccess: values.OnedriveAccess ? values.OnedriveAccess : '', + AccessNoAutomap: values.AccessNoAutomap ? values.AccessNoAutomap : '', + AccessAutomap: values.AccessAutomap ? values.AccessAutomap : '', + ConvertToShared: values.ConvertToShared, + HideFromGAL: values.HideFromGAL, + DisableSignIn: values.DisableSignIn, + RemoveGroups: values.RemoveGroups, + RemoveLicenses: values.RemoveLicenses, + ResetPass: values.ResetPass, + RevokeSessions: values.RevokeSessions, + user: values.User, + deleteuser: values.DeleteUser, + removeRules: values.RemoveRules, + removeMobile: values.RemoveMobile, + keepCopy: values.keepCopy, + removePermissions: values.removePermissions, + PostExecution: values.Scheduled?.enabled + ? { webhook: values.webhook, psa: values.psa, email: values.email } + : '', + } + + //alert(JSON.stringify(values, null, 2)) + genericPostRequest({ path: '/api/ExecOffboardUser', values: shippedValues }) + } + + return ( + + +
    +

    Step 1

    +
    Choose a tenant
    +
    +
    + {(props) => } +
    +
    + +
    +

    Step 2

    +
    Select the backup to restore
    +
    +
    +
    + ({ + value: backup.RowKey, + name: `${backup.BackupDate}`, + }))} + placeholder={!currentBackupsIsFetching ? 'Select a backup' : 'Loading...'} + name="User" + /> + {currentBackupsError && Failed to load list of Current Backups} +
    +
    +
    + +
    +

    Step 3

    +
    Choose restore options
    +
    +
    +
    + + +

    Identity

    + + +

    Conditional Access

    + + + +
    + +

    Intune

    + + + +

    CIPP

    + + +
    +
    +
    + + + + + + + +
    Warning
    +

    + Overwriting existing entries will remove the current settings and replace them with + the backup settings. If you have selected to restore users, all properties will be + overwritten with the backup settings. +

    + +

    + To prevent and skip already existing entries, deselect the setting from the list + above, or disable overwrite. +

    +
    +
    +
    +
    +
    + +
    +

    Step 4

    +
    Confirm and apply
    +
    +
    +
    + {postResults.isFetching && ( + + Loading + + )} + {postResults.isSuccess && ( + + {postResults.data.Results.map((message, idx) => { + return
  • {message}
  • + })} +
    + )} + {!postResults.isSuccess && ( + + {/* eslint-disable react/prop-types */} + {(props) => ( + <> + + + + +
    Selected Tenant:
    + {tenantDomain} +
    +
    +
    +
    +
    + + + + + Revoke Sessions + + + + Remove all mobile devices + + + + Remove all mailbox rules + + + + Remove all mailbox permissions + + + + Remove Licenses + + + + Convert to Shared + + + + Disable Sign-in + + + + Reset Password + + + + Remove from all groups + + + + Hide from Global Address List + + + + Set Out of Office + + + + Give another user access to the mailbox with automap + + + + Give another user access to the mailbox without automap + + + + Give another user access to OneDrive + + + + Forward all e-mail to another user + + + + + + + )} +
    + )} +
    +
    +
    +
    + ) +} + +export default OffboardingWizard