From a7c8a4c7d1d1671c3e582733c1887805789e85a5 Mon Sep 17 00:00:00 2001 From: k-grube Date: Fri, 12 Nov 2021 23:15:25 -0800 Subject: [PATCH 1/5] Administration/Tenants display --- src/store/modules/tenants.js | 7 +- src/views/tenant/administration/Tenants.js | 109 ++++++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/store/modules/tenants.js b/src/store/modules/tenants.js index dd8b04f5393b..2b86cbfdb23e 100644 --- a/src/store/modules/tenants.js +++ b/src/store/modules/tenants.js @@ -2,6 +2,7 @@ const initialState = { tenants: [], selectedTenant: {}, loading: false, + loaded: false, } const LOADING = 'tenants/LOADING' @@ -13,11 +14,11 @@ const SET_TENANT = 'tenants/SET_TENANT' export default function reducer(state = initialState, action = {}) { switch (action.type) { case LOADING: - return { ...state, loading: true } + return { ...state, loading: true, loaded: false } case LOADING_SUCCESS: - return { ...state, tenants: action.result, loading: false } + return { ...state, tenants: action.result, loading: false, loaded: true } case LOADING_FAILURE: - return { ...state, loading: false, tenants: [] } + return { ...state, loading: false, loaded: false, tenants: [] } case SET_TENANT: return { ...state, selectedTenant: action.tenant } default: diff --git a/src/views/tenant/administration/Tenants.js b/src/views/tenant/administration/Tenants.js index da7486924fd7..da6ccb11c42a 100644 --- a/src/views/tenant/administration/Tenants.js +++ b/src/views/tenant/administration/Tenants.js @@ -1,7 +1,112 @@ -import React from 'react' +import React, { useEffect } from 'react' +import ToolkitProvider, { CSVExport, Search } from 'react-bootstrap-table2-toolkit' +import paginationFactory from 'react-bootstrap-table2-paginator' +import CellBadge from '../../../components/cipp/CellBadge' +import { useDispatch, useSelector } from 'react-redux' +import { listTenants } from '../../../store/modules/tenants' +import BootstrapTable from 'react-bootstrap-table-next' +import { CButton, CSpinner } from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { cilCog } from '@coreui/icons' + +const { SearchBar } = Search +const pagination = paginationFactory() + +// eslint-disable-next-line react/display-name +const linkCog = (url) => (cell) => + ( + + + + ) + +const columns = [ + { + text: 'Name', + dataField: 'displayName', + sort: true, + }, + { + text: 'Default Domain', + dataField: 'defaultDomainName', + }, + { + text: 'M365 Portal', + dataField: 'customerId', + formatter: linkCog( + (cell) => + `https://portal.office.com/Partner/BeginClientSession.aspx?CTID=${cell}&CSDEST=o365admincenter`, + ), + }, + { + text: 'Exchange Portal', + dataField: 'defaultDomainName', + formatter: linkCog( + (cell) => `https://outlook.office365.com/ecp/?rfr=Admin_o365&exsvurl=1&delegatedOrg=${cell}`, + ), + }, + { + text: 'AAD Portal', + dataField: 'defaultDomainName', + formatter: linkCog((cell) => `https://aad.portal.azure.com/${cell}`), + }, + { + text: 'Teams Portal', + dataField: 'defaultDomainName', + formatter: linkCog((cell) => `https://admin.teams.microsoft.com/?delegatedOrg=${cell}`), + }, + { + text: 'Azure Portal', + dataField: 'defaultDomainName', + formatter: linkCog((cell) => `https://portal.azure.com/${cell}`), + }, + { + text: 'MEM (Intune) Portal', + dataField: 'defaultDomainName', + formatter: linkCog((cell) => `https://endpoint.microsoft.com/${cell}`), + }, + // @todo not used at the moment? + // { + // text: 'Domains', + // dataField: 'defaultDomainName', + // }, +] const Tenants = () => { - return
+ const dispatch = useDispatch() + const { tenants, loading, loaded } = useSelector((state) => state.tenants) + + useEffect(() => { + dispatch(listTenants()) + }, []) + + return ( +
+
+
+

Tenants

+ {!loaded && loading && } + {loaded && !loading && ( + + {(props) => ( +
+ {/* eslint-disable-next-line react/prop-types */} + +
+ {/*eslint-disable */} + + {/*eslint-enable */} +
+ )} +
+ )} +
+
+ ) } export default Tenants From 7a5f8711df157023b62c0c220d2aece479344940 Mon Sep 17 00:00:00 2001 From: k-grube Date: Fri, 12 Nov 2021 23:38:11 -0800 Subject: [PATCH 2/5] administration/ConditionalAccess --- src/store/modules/tenants.js | 44 ++++++ .../administration/ConditionalAccess.js | 148 ++++++++++++++++++ .../administration/Conditionalaccess.js | 7 - 3 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 src/views/tenant/administration/ConditionalAccess.js delete mode 100644 src/views/tenant/administration/Conditionalaccess.js diff --git a/src/store/modules/tenants.js b/src/store/modules/tenants.js index 2b86cbfdb23e..47ee56184802 100644 --- a/src/store/modules/tenants.js +++ b/src/store/modules/tenants.js @@ -3,6 +3,11 @@ const initialState = { selectedTenant: {}, loading: false, loaded: false, + cap: { + loading: false, + loaded: false, + policies: [], + }, } const LOADING = 'tenants/LOADING' @@ -11,6 +16,10 @@ const LOADING_FAILURE = 'tenants/LOADING_FAILURE' const SET_TENANT = 'tenants/SET_TENANT' +const LOAD_CONDITIONAL_ACCESS = 'tenants/LOAD_CONDITIONAL_ACCESS' +const LOAD_CONDITIONAL_ACCESS_SUCCESS = 'tenants/LOAD_CONDITIONAL_ACCESS_SUCCESS' +const LOAD_CONDITIONAL_ACCESS_FAIL = 'tenants/LOAD_CONDITIONAL_ACCESS_FAIL' + export default function reducer(state = initialState, action = {}) { switch (action.type) { case LOADING: @@ -21,6 +30,31 @@ export default function reducer(state = initialState, action = {}) { return { ...state, loading: false, loaded: false, tenants: [] } case SET_TENANT: return { ...state, selectedTenant: action.tenant } + case LOAD_CONDITIONAL_ACCESS: + return { + ...state, + cap: { + ...state.cap, + loading: true, + loaded: false, + policies: [], + }, + } + case LOAD_CONDITIONAL_ACCESS_SUCCESS: + return { + ...state, + cap: { + ...state.cap, + loading: false, + loaded: true, + policies: action.result, + }, + } + case LOAD_CONDITIONAL_ACCESS_FAIL: + return { + ...state, + cap: initialState.cap, + } default: return state } @@ -39,3 +73,13 @@ export function setTenant({ tenant }) { tenant, } } + +export function loadConditionalAccessPolicies({ domain }) { + return { + types: [LOAD_CONDITIONAL_ACCESS, LOAD_CONDITIONAL_ACCESS_SUCCESS, LOAD_CONDITIONAL_ACCESS_FAIL], + promise: (client) => + client + .get('/api/ListConditionalAccessPolicies', { params: { TenantFilter: domain } }) + .then((result) => result.data), + } +} diff --git a/src/views/tenant/administration/ConditionalAccess.js b/src/views/tenant/administration/ConditionalAccess.js new file mode 100644 index 000000000000..238e9b192b9c --- /dev/null +++ b/src/views/tenant/administration/ConditionalAccess.js @@ -0,0 +1,148 @@ +import React, { useEffect } from 'react' +import ToolkitProvider, { CSVExport, Search } from 'react-bootstrap-table2-toolkit' +import paginationFactory from 'react-bootstrap-table2-paginator' +import CellBadge from '../../../components/cipp/CellBadge' +import { useDispatch, useSelector } from 'react-redux' +import { listTenants, loadConditionalAccessPolicies } from '../../../store/modules/tenants' +import BootstrapTable from 'react-bootstrap-table-next' +import { CButton, CSpinner } from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { cilCog } from '@coreui/icons' +import TenantSelector from '../../../components/cipp/TenantSelector' + +const { SearchBar } = Search +const pagination = paginationFactory() + +const columns = [ + { + text: 'Name', + dataField: 'displayName', + sort: true, + }, + { + text: 'State', + dataField: 'state', + sort: true, + }, + { + text: 'Last Modified', + dataField: 'modifiedDateTime', + sort: true, + }, + { + text: 'Client App Types', + dataField: 'clientAppTypes', + sort: true, + }, + { + text: 'Platform Inc', + dataField: 'includePlatforms', + sort: true, + }, + { + text: 'Platform Exc', + dataField: 'excludePlatforms', + sort: true, + }, + { + text: 'Include Locations', + dataField: 'includeLocations', + sort: true, + }, + { + text: 'Exclude Locations', + dataField: 'excludeLocations', + sort: true, + }, + { + text: 'Include Users', + dataField: 'includeUsers', + sort: true, + }, + { + text: 'Exclude Users', + dataField: 'excludeUsers', + sort: true, + }, + { + text: 'Include Groups', + dataField: 'includeGroups', + sort: true, + }, + { + text: 'Exclude Groups', + dataField: 'excludeGroups', + sort: true, + }, + { + text: 'Include Applications', + dataField: 'includeApplications', + sort: true, + }, + { + text: 'Exclude Applications', + dataField: 'excludeApplications', + sort: true, + }, + { + text: 'Control Operator', + dataField: 'grantControlsOperator', + sort: true, + }, + { + text: 'Built-in Controls', + dataField: 'builtInControls', + sort: true, + }, +] + +const ConditionalAccess = () => { + const dispatch = useDispatch() + const { + tenant, + cap: { policies, loading, loaded }, + } = useSelector((state) => state.tenants) + + const tenantSelected = tenant && tenant.defaultDomainName + + useEffect(() => { + if (tenantSelected) { + dispatch(loadConditionalAccessPolicies({ domain: tenantSelected.defaultDomainName })) + } + }, []) + + const action = (selected) => + dispatch(loadConditionalAccessPolicies({ domain: selected.defaultDomainName })) + + return ( +
+ +
+
+

Conditional Access Policies

+ {!loaded && loading && } + {loaded && !loading && ( + + {(props) => ( +
+ {/* eslint-disable-next-line react/prop-types */} + +
+ {/*eslint-disable */} + + {/*eslint-enable */} +
+ )} +
+ )} +
+
+ ) +} + +export default ConditionalAccess diff --git a/src/views/tenant/administration/Conditionalaccess.js b/src/views/tenant/administration/Conditionalaccess.js deleted file mode 100644 index 8347fdb4cc09..000000000000 --- a/src/views/tenant/administration/Conditionalaccess.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' - -const Conditionalaccess = () => { - return
-} - -export default Conditionalaccess From e1ffea58cfcce13205cb15cbb7ecbd08954685af Mon Sep 17 00:00:00 2001 From: k-grube Date: Sat, 13 Nov 2021 01:28:44 -0800 Subject: [PATCH 3/5] Add modal size support --- src/components/SharedModal.js | 4 ++-- src/store/modules/modal.js | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/SharedModal.js b/src/components/SharedModal.js index 2d3c01e6f7fd..d3211433e215 100644 --- a/src/components/SharedModal.js +++ b/src/components/SharedModal.js @@ -4,13 +4,13 @@ import { CButton, CModal, CModalBody, CModalFooter, CModalHeader, CModalTitle } import { hideModal } from '../store/modules/modal' export default function SharedModal() { - const { body, title, visible } = useSelector((store) => store.modal) + const { body, title, visible, size } = useSelector((store) => store.modal) const dispatch = useDispatch() const hideAction = () => dispatch(hideModal()) return ( - + {title} diff --git a/src/store/modules/modal.js b/src/store/modules/modal.js index 77f4cca3291f..655147f87379 100644 --- a/src/store/modules/modal.js +++ b/src/store/modules/modal.js @@ -2,6 +2,7 @@ const initialState = { visible: false, body: undefined, title: undefined, + size: undefined, } const SET_VISIBLE = 'modal/SET_VISIBLE' @@ -20,6 +21,7 @@ export default function reducer(state = initialState, action = {}) { ...state, body: action.body, title: action.title, + size: action.size, } case RESET_MODAL: return initialState @@ -46,10 +48,18 @@ export function resetModal() { } } -export function setModalContent({ body, title }) { +/** + * + * @param {Element} body + * @param {String} title + * @param {String} [size] ['sm', 'lg', 'xl'] defaults to None + * @returns {{size, type: string, body, title}} + */ +export function setModalContent({ body, title, size }) { return { type: SET_CONTENT, body: body, title, + size, } } From 6652a8251cfc1cf654c8fa13aa80a8a976bca61b Mon Sep 17 00:00:00 2001 From: k-grube Date: Sat, 13 Nov 2021 01:28:58 -0800 Subject: [PATCH 4/5] stripe table --- src/views/tenant/administration/Tenants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/tenant/administration/Tenants.js b/src/views/tenant/administration/Tenants.js index da6ccb11c42a..5e854376d776 100644 --- a/src/views/tenant/administration/Tenants.js +++ b/src/views/tenant/administration/Tenants.js @@ -97,6 +97,7 @@ const Tenants = () => { {/*eslint-enable */} From 49a22ab18cab48bad6389488cd054a0fbe9bdb16 Mon Sep 17 00:00:00 2001 From: k-grube Date: Sat, 13 Nov 2021 01:30:14 -0800 Subject: [PATCH 5/5] Update BPA, format cells properly --- .../tenant/standards/BestPracticeAnalyser.js | 349 +++++++++++++----- 1 file changed, 255 insertions(+), 94 deletions(-) diff --git a/src/views/tenant/standards/BestPracticeAnalyser.js b/src/views/tenant/standards/BestPracticeAnalyser.js index e0ee9d6ff157..0d3716dacc8a 100644 --- a/src/views/tenant/standards/BestPracticeAnalyser.js +++ b/src/views/tenant/standards/BestPracticeAnalyser.js @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { CButton } from '@coreui/react' +import { CButton, CSpinner } from '@coreui/react' import BootstrapTable from 'react-bootstrap-table-next' import ToolkitProvider, { Search, CSVExport } from 'react-bootstrap-table2-toolkit' import paginationFactory from 'react-bootstrap-table2-paginator' @@ -13,101 +13,83 @@ import { loadBestPracticeReport, forceRefreshBestPracticeReport, } from '../../../store/modules/standards' +import CellProgressBar from '../../../components/cipp/CellProgressBar' +import CellBadge from '../../../components/cipp/CellBadge' +import CellBoolean from '../../../components/cipp/CellBoolean' +import { setModalContent, showModal } from '../../../store/modules/modal' +import PropTypes from 'prop-types' const { SearchBar } = Search const { ExportCSVButton } = CSVExport -const CIconWarning = () => -const CIconXCircle = () => -const CIconCheckCircle = () => - -const rowFormatter = (type) => { - const FailIcon = type === 'circle' ? CIconXCircle : CIconWarning - - const CellFormatter = (cell) => { - console.log('cell', cell) - if (cell === '') { - return
No Data
- } else if (cell === true) { - return - } else { - return +const IconWarning = () => +const IconError = () => +const IconSuccess = () => + +const pagination = paginationFactory() + +const rowFormatter = + (type = 'error') => + // eslint-disable-next-line react/display-name + (cell) => { + if (cell) { + return + } else if (cell === '') { + return } + return type === 'error' ? : } - return CellFormatter +const SharedMailboxesCard = ({ row }) => { + return ( + <> + {/* @todo why */} + {row.DisabledSharedMailboxLogins.split('
').map((el) => ( + <> + {el} +
+ + ))} + + ) +} + +SharedMailboxesCard.propTypes = { + row: PropTypes.object, } -const pagination = paginationFactory() +const UnusedLicensesCard = ({ row }) => { + const tabularized = row.UnusedLicenseList.split('
') + .map((line) => + line + .split(', ') + .map((sku) => sku.split(': ').reduce((key, val) => ({ [key]: val }))) + .reduce((pv, cv) => ({ ...pv, ...cv })), + ) + .sort((a, b) => b.SKU.toLocaleLowerCase().localeCompare(a.SKU.toLocaleLowerCase())) + + const columns = [ + { + text: 'SKU', + dataField: 'SKU', + }, + { + text: 'Purchased', + dataField: 'Purchased', + }, + { + text: 'Consumed', + dataField: 'Consumed', + }, + ] + + return +} +UnusedLicensesCard.propTypes = { + row: PropTypes.object, +} -const columns = [ - { - text: 'Tenant', - dataField: 'Tenant', - sort: true, - }, - { - text: 'Last Refresh', - dataField: 'LastRefresh', - formatter: (cell) =>
{moment.utc(cell).format('MMM D YYYY')}
, - sort: true, - }, - { - text: 'Unified Audit Log Enabled', - dataField: 'UnifiedAuditLog', - formatter: rowFormatter('circle'), - }, - { - text: 'Security Defaults Enabled', - dataField: 'SecureDefaultState', - formatter: rowFormatter('circle'), - }, - { - text: 'Message Cop for Send As', - dataField: 'MessageCopyForSend', - formatter: rowFormatter('warn'), - }, - { - text: 'User Cannot Consernt to Apps', - dataField: 'AdminConsentForApplications', - formatter: rowFormatter('warn'), - }, - { - text: 'Passwords Do Not Expire', - dataField: 'DoNotExpirePasswords', - formatter: rowFormatter('warn'), - }, - { - text: 'Privacy in Reports Enabled', - dataField: 'PrivacyEnabled', - formatter: rowFormatter('warn'), - }, - { - text: 'Self Service Password Reset Enabled', - dataField: 'SelfServicePasswordReset', - formatter: rowFormatter('warn'), - }, - { - text: 'Modern Auth Enabled', - dataField: 'EnableModernAuth', - formatter: rowFormatter('circle'), - }, - { - text: 'Shared Mailboxes Logins Disabled', - dataField: 'DisabledSharedMailboxLoginsCount', - }, - { - text: 'Unused Licenses', - dataField: 'UnusedLicensesResult', - formatter: rowFormatter('circle'), - }, - { - text: 'Secure Score', - dataField: 'SecureScoreCurrent', - }, -] - -const BestPracticeAnalyzer = () => { +const BestPracticeAnalyser = () => { const dispatch = useDispatch() const bpa = useSelector((state) => state.standards.bpa) @@ -119,17 +101,195 @@ const BestPracticeAnalyzer = () => { load() }, []) + const handleSharedMailboxes = ({ row }) => { + dispatch( + setModalContent({ + body: , + title: `Shared Mailboxes with Enabled User Accounts`, + }), + ) + dispatch(showModal()) + } + + const handleUnusedLicense = ({ row }) => { + dispatch( + setModalContent({ + body: , + title: `SKUs with Unassigned Licenses`, + size: 'lg', + }), + ) + dispatch(showModal()) + } + + const handleMessageCopy = ({ row }) => { + dispatch( + setModalContent({ + body: ( + <> + {row.MessageCopyForSendList.split('
').map((el) => ( + <> + {el} +
+ + ))} + + ), + title: 'Message Copy for Send As', + }), + ) + dispatch(showModal()) + } + + const columns = [ + { + text: 'Tenant', + dataField: 'Tenant', + sort: true, + }, + { + text: 'Last Refresh', + dataField: 'LastRefresh', + formatter: (cell) =>
{moment.utc(cell).format('MMM D YYYY')}
, + sort: true, + }, + { + text: 'Unified Audit Log Enabled', + dataField: 'UnifiedAuditLog', + formatter: rowFormatter('error'), + }, + { + text: 'Security Defaults Enabled', + dataField: 'SecureDefaultState', + formatter: (cell) => { + if (cell) { + return + } else if (cell === '') { + return + } + return + }, + }, + { + text: 'Message Copy for Send As', + dataField: 'MessageCopyForSend', + formatter: (cell, row) => { + if (cell === 'PASS') { + return + } else if (cell === 'FAIL') { + return ( + handleMessageCopy({ row })} + >{`${row.MessageCopyForSendAsCount} Users`} + ) + } + return + }, + }, + { + text: 'User Cannot Consent to Apps', + dataField: 'AdminConsentForApplications', + formatter: (cell, row) => { + if (cell) { + return + } else if (cell === '') { + return + } + return + }, + }, + { + text: 'Passwords Do Not Expire', + dataField: 'DoNotExpirePasswords', + formatter: rowFormatter('error'), + }, + { + text: 'Privacy in Reports Enabled', + dataField: 'PrivacyEnabled', + formatter: (cell, row) => { + if (cell) { + return + } else if (cell === '') { + return + } + return + }, + }, + { + text: 'Self Service Password Reset Enabled', + dataField: 'SelfServicePasswordReset', + formatter: (cell) => { + if (cell === 'Off') { + return + } else if (cell === 'On') { + return + } else if (cell === 'Specific Users') { + return + } + return + }, + }, + { + text: 'Modern Auth Enabled', + dataField: 'EnableModernAuth', + formatter: rowFormatter('error'), + }, + { + text: 'Shared Mailboxes Logins Disabled', + dataField: 'DisabledSharedMailboxLoginsCount', + formatter: (cell, row) => { + if (cell > 0) { + return ( + handleSharedMailboxes({ row })} + > + {cell} User{cell > 1 ? 's' : ''} + + ) + } else if (cell === 0) { + return + } + return + }, + }, + { + text: 'Unused Licenses', + dataField: 'UnusedLicensesResult', + formatter: (cell, row) => { + if (cell === 'FAIL') { + return ( + handleUnusedLicense({ row })}> + {row.UnusedLicensesCount} SKU{row.UnusedLicensesCount > 1 ? 's' : ''} + + ) + } else if (cell === 'PASS') { + return + } + return + }, + }, + { + text: 'Secure Score', + dataField: 'SecureScorePercentage', + formatter: (cell, row) => { + if (!cell) { + return + } + return CellProgressBar({ value: row.SecureScorePercentage }) + }, + }, + ] + return (
-

Best Practice Analyzer Report

- {!bpa.loaded && bpa.loading && ( -
-
-
- )} +

Best Practice Analyser Report

+ {!bpa.loaded && bpa.loading && } {!bpa.loading && bpa.loaded && ( - + {(props) => (
{/* eslint-disable-next-line react/prop-types */} @@ -148,6 +308,7 @@ const BestPracticeAnalyzer = () => { {...props.baseProps} pagination={pagination} wrapperClasses="table-responsive" + striped /> {/*eslint-enable */}
@@ -159,4 +320,4 @@ const BestPracticeAnalyzer = () => { ) } -export default BestPracticeAnalyzer +export default BestPracticeAnalyser