diff --git a/.vscode/launch.json b/.vscode/launch.json index fcd6fc8d6c56..e8f4c409c8fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,13 @@ { "version": "0.2.0", "configurations": [ + { + "name": "PowerShell: Execute current file", + "type": "PowerShell", + "request": "launch", + "script": "${file}", + "cwd": "${file}" + }, { "command": "npm run start-api", "name": "Run emulator", diff --git a/README.md b/README.md index 9e42be1b45e1..677617c1ae29 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,10 @@ ![CyberDrain Light](github_assets/img/CIPP.png#gh-dark-mode-only) ![CyberDrain Dark](github_assets/img/CIPP-Light.png#gh-light-mode-only) -
- -[![GitHub Latest Release](https://img.shields.io/github/v/release/KelvinTegelaar/CIPP?label=Latest%20Release&style=for-the-badge)](https://github.com/KelvinTegelaar/CIPP/releases) -![CodeQL Security Analysis Status](https://img.shields.io/github/workflow/status/KelvinTegelaar/CIPP/CodeQL?label=CodeQL%20Security&style=for-the-badge) -[![GitHub Enhancement Requests](https://img.shields.io/github/issues/KelvinTegelaar/CIPP/enhancement?label=Enhancement%20Requests&style=for-the-badge)](https://github.com/KelvinTegelaar/CIPP/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) -[![GitHub Bugs](https://img.shields.io/github/issues/KelvinTegelaar/CIPP/bug?label=Bugs&style=for-the-badge)](https://github.com/KelvinTegelaar/CIPP/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Abug) -[![Discord](https://img.shields.io/discord/905453405936447518?label=Discord&style=for-the-badge)](https://discord.com/invite/cyberdrain) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/KelvinTegelaar?label=Public%20Sponsors&style=for-the-badge)](https://github.com/sponsors/KelvinTegelaar) - -
-

Sponsored by

![OIT](github_assets/img/oitpsonsor_light.png)     -![Genuine Technology Services](github_assets/img/Genuine-logo-vertical-light.png)     ![Immybot](github_assets/img/Immybot.png)     ![NinjaOne](github_assets/img/NinjaOne-Light.png#gh-dark-mode-only) ![NinjaOne](github_assets/img/NinjaOne-Dark.png#gh-light-mode-only)     @@ -31,143 +19,4 @@ The CyberDrain Improved Partner Portal is a portal to help manage administration This project is a way to help you with administration, with user management, and deploying your own preferred standards. It's not a replacement for security tools, or a way to cut costs on specific subscriptions. The tool should assist you in removing the gripes with standard partner management and save you several hours per engineer per month. -# Deployment and Getting Started - -If you want to self-host, check out the installation manual [here](https://cipp.app/GettingStarted/Installation/). You will need some knowledge of Static Web Apps, Azure Functions, and Azure Keyvault - -# Why are you making this? - -I'm kind of done waiting for vendors to catch up to what we actually need. All RMM vendors are dramaticaly slow adopting cloud management. Microsoft themselves don't understand the Managed services markets, there are vendors that have tried jumping into the gap but either have unreasonable fees, weird constructions, require Global Admins without MFA, or just don't innovate at a pace that is required of cloud services right now. - -I'm also annoyed the untransparent behaviour that many companies in our market are showing. Most are claiming that working with the Microsoft Partner APIs is difficult, and requires a very heavy development team. I'm a guy that had no webdesign knowledge before this and created the first release of this app in 3 weekends. Vendors that claim high difficulty or issues with integration are simply not giving this _any_ priority. - -I was recently on a call with one of my friends and he said he was changing the world. That insipred me to change the world just a little bit too. :) I'm hoping that this is one of the tools that make you smile. - -# What's the pricing? - -This project is **FREE** but we do have a **Sponsorware** component. The sponsorware structure for this project is pretty simple; the code is available to everyone and free to use. You will need some technical know-how to put it all together. Sponsors receive the following benefits - -### For users of the project that sponsor: - -- The project will be hosted for you. -- The hosted version will always be the latest release and automatically updated. -- You'll also receive a staging environment with the latest (nightly/beta) build, to see new features before anyone else. -- You will receive priority on support issues reported on GitHub. -- You will be able to make 1 priortized feature request per month. - -Sponsorship allows me to sink some more time into this project and keep it free, so please consider it. :) - -### For company sponsors, depending on sponsor level you can get the following benefits; - -- Your company logo will be featured on this readme page at the top. -- Your company logo will be featured on https://cyberdrain.com -- A small version of your company logo with a link to your homepage will be on the footer, each user will see this on each page. - -# How does it look?! - -Check out the GIFs below to see how some of the workflows work. - - - - - - - - - -# What is the functionality? - -The current build functionality is described below, also check out our Changelog in the documentation folder, as the tool has a very rapid development schedule the list below might be out of date. - -## Identity Management - -- Manage M365 users - - List users, email addreses, and licenses. - - View & Edit user settings - - Research if user has been compromised - - Send user an MFA push to confirm their identity - - Convert a user to a shared mailbox - - Block signin, reset passwords - - Delete users -- Manage M365 groups - - List all M365 groups, group types, and e-mail addresses. - - Edit members and group owners -- Offboard users via an easy wizard - - Remove user licenses - - Convert user to shared mailbox - - Disable user sign-in - - Reset user password - - Remove user from all groups - - Hide user from address list - - Set Out of Office - - Give other user access to mailbox, and OneDrive - -## Tenants - -- Manage M365 tenants - - List all tenants and quick-links to the most user portals using delegated access. - - Edit Partner tenant names and default domain for your CSP partner environment - - List tenant conditional access policies - - Apply standard configuration to tenant on a repeat schedule. - - Execute a best practice analysis daily and report on best practice settings - - Analyse current domains, and domains outside of M365 for optimal security settings - - List alerts for tenants - -## Endpoint Management - -- Applications - - List all applications in tenants - - List installation status of a specific application per device - - Add Office Apps to multiple tenants - - Add/Remove Chocolatey Apps to multiple tenants - - Assign Apps to All Devices or All Users - - Report on installation status -- Autopilot - - Manage and create autopilot devices, profiles, status pages. -- Intune - - List Intune policies - - Apply Intune Policies - - Add Intune Policy Templates to deploy over multiple tenants - -## Teams & SharePoint - -- List OneDrive, Teams, and SharePoint usage -- View current Teams, installed applications, Team owners, members, and channels -- Add and edit Teams, members, owners and apps. -- Tenant Alerting - -## Exchange - -- View mailboxes and contacts -- View user mobile devices -- Convert mailboxes to shared or user mailboxes -- Report mailbox statistics, client access settings -- Perform message traces -- Change and view phishing policies. - -## Application settings - -- Use multiple user levels (readonly, editor, admin) to manage access -- Allow excluding of tenants -- Send automated alert emails to webhook or e-mail - -# Security - -Authentication is handled by Azure AD using static web apps security. This means the API is only reachable for authenticated users you've invited. For most of the security info related to that check out our staticwebapp.config.json and/or the doc pages on static web apps. Do you see something that might be a security risk, even the smallest? report it and we will handle it asap. Check out our security reporting options [here](https://github.com/KelvinTegelaar/CIPP/security) - -# Contributions - -Feel free to send pull requests or fill out issues when you encounter them, sponsors get a priority on issues and bugs. I'm also completely open to adding direct maintainers/contributors and working together. - -If you decide to contribute; remember that keeping the portal fast is a key component. CIPP is supposed to go brrrrr, any improvements that help with speed are welcomed. - -## Special thanks - -I'd like to give special thanks to the people that made this project possible; - -- [Kyle Hanslovan](https://huntress.com) -- [Ray Orsini](https://oit.co) -- The Team at [MSP.zone/MSP'R'Us](https://msp.zone) -- Gavin Stone at [MSPGeek](https://mspgeek.org) -- MSP2.0 for helping with some visual input. -- Scott, Chris, Jon, and others that helped me with some of the internals of the app. +for more information, we recommend checking out our website [here](https://cipp.app) diff --git a/package-lock.json b/package-lock.json index 08e6a9e247c5..619b33124282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "enzyme": "^3.11.0", "final-form": "^4.20.4", "fuzzysort": "^1.1.4", + "javascript-time-ago": "^2.5.9", "jspdf": "^2.4.0", "jspdf-autotable": "^3.5.23", "moment": "^2.29.1", @@ -53,6 +54,7 @@ "react-select": "^5.3.0", "react-select-search": "^3.0.8", "react-syntax-highlighter": "^15.4.5", + "react-time-ago": "^7.2.1", "redux": "4.1.1", "redux-persist": "^6.0.0", "simplebar-react": "^2.3.6", @@ -10065,6 +10067,14 @@ "node": ">=8" } }, + "node_modules/javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "dependencies": { + "relative-time-format": "^1.1.6" + } + }, "node_modules/jest": { "version": "27.5.1", "dev": true, @@ -15163,6 +15173,26 @@ "react": "17.0.2" } }, + "node_modules/react-time-ago": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.2.1.tgz", + "integrity": "sha512-X5zwJDZHa1fsMwMvh8mrHN31g85s84zMCp+d7YL6IX50kNnr6YMAS2wpt1BmO9OxBV2Ue5J1ptD6JI8Zjd35HA==", + "dependencies": { + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1", + "raf": "^3.4.1" + }, + "peerDependencies": { + "javascript-time-ago": "^2.3.7", + "react": ">=0.16.8", + "react-dom": ">=0.16.8" + } + }, + "node_modules/react-time-ago/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/react-transition-group": { "version": "4.4.2", "license": "BSD-3-Clause", @@ -15500,6 +15530,11 @@ "node": ">= 0.10" } }, + "node_modules/relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" + }, "node_modules/renderkid": { "version": "3.0.0", "dev": true, @@ -24586,6 +24621,14 @@ } } }, + "javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "requires": { + "relative-time-format": "^1.1.6" + } + }, "jest": { "version": "27.5.1", "dev": true, @@ -27687,6 +27730,23 @@ "scheduler": "^0.20.2" } }, + "react-time-ago": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.2.1.tgz", + "integrity": "sha512-X5zwJDZHa1fsMwMvh8mrHN31g85s84zMCp+d7YL6IX50kNnr6YMAS2wpt1BmO9OxBV2Ue5J1ptD6JI8Zjd35HA==", + "requires": { + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1", + "raf": "^3.4.1" + }, + "dependencies": { + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + } + } + }, "react-transition-group": { "version": "4.4.2", "requires": { @@ -27914,6 +27974,11 @@ "version": "0.2.7", "dev": true }, + "relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==" + }, "renderkid": { "version": "3.0.0", "dev": true, diff --git a/package.json b/package.json index d9e64244f095..7d4a99103c8f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "enzyme": "^3.11.0", "final-form": "^4.20.4", "fuzzysort": "^1.1.4", + "javascript-time-ago": "^2.5.9", "jspdf": "^2.4.0", "jspdf-autotable": "^3.5.23", "moment": "^2.29.1", @@ -72,6 +73,7 @@ "react-select": "^5.3.0", "react-select-search": "^3.0.8", "react-syntax-highlighter": "^15.4.5", + "react-time-ago": "^7.2.1", "redux": "4.1.1", "redux-persist": "^6.0.0", "simplebar-react": "^2.3.6", diff --git a/public/version_latest.txt b/public/version_latest.txt index 15a279981720..fbcbf7380658 100644 --- a/public/version_latest.txt +++ b/public/version_latest.txt @@ -1 +1 @@ -3.3.0 +3.4.0 \ No newline at end of file diff --git a/src/_nav.js b/src/_nav.js index e5a5d4f3f548..2f6644b6ac11 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -649,6 +649,16 @@ const _nav = [ to: '/cipp/gdap', icon: , items: [ + { + component: CNavItem, + name: 'Role Wizard', + to: '/tenant/administration/gdap-role-wizard', + }, + { + component: CNavItem, + name: 'GDAP Roles', + to: '/tenant/administration/gdap-roles', + }, { component: CNavItem, name: 'Migration Wizard', diff --git a/src/adminRoutes.js b/src/adminRoutes.js index fab00d4080e5..e33468c53952 100644 --- a/src/adminRoutes.js +++ b/src/adminRoutes.js @@ -4,6 +4,8 @@ const Setup = React.lazy(() => import('src/views/cipp/Setup')) const ApplyStandard = React.lazy(() => import('src/views/tenant/standards/ApplyStandard')) const GDAPStatus = React.lazy(() => import('src/views/tenant/administration/ListGDAPQueue')) const GDAP = React.lazy(() => import('src/views/tenant/administration/GDAPWizard')) +const GDAPRoleWizard = React.lazy(() => import('src/views/tenant/administration/GDAPRoleWizard')) +const GDAPRoles = React.lazy(() => import('src/views/tenant/administration/ListGDAPRoles')) const appapproval = React.lazy(() => import('src/views/cipp/AppApproval')) const adminRoutes = [ @@ -12,6 +14,16 @@ const adminRoutes = [ { path: '/cipp/settings', name: 'Settings', component: CIPPSettings }, { path: '/cipp/setup', name: 'Setup', component: Setup }, { path: '/tenant/administration/gdap', name: 'GDAP Wizard', component: GDAP }, + { + path: '/tenant/administration/gdap-role-wizard', + name: 'GDAP Role Wizard', + component: GDAPRoleWizard, + }, + { + path: '/tenant/administration/gdap-roles', + name: 'GDAP Roles', + component: GDAPRoles, + }, { path: '/tenant/administration/appapproval', name: 'App Approval', component: appapproval }, { path: '/tenant/administration/gdap-status', name: 'GDAP Status', component: GDAPStatus }, { path: '/tenant/standards/apply-standard', name: 'Apply Standard', component: ApplyStandard }, diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index bb0a0246ca72..568e03211542 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { CAvatar, CDropdown, @@ -7,15 +7,59 @@ import { CDropdownToggle, CLink, } from '@coreui/react' -import { faUser, faBook, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' +import { faUser, faBook, faSignOutAlt, faHistory, faTrash } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Link } from 'react-router-dom' import { authApi } from 'src/store/api/auth' -import { CippProfile, CippOffcanvas } from 'src/components/utilities' +import { CippProfile, CippOffcanvas, CippActionsOffcanvas } from 'src/components/utilities' +import { useLazyGenericGetRequestQuery } from 'src/store/api/app' const AppHeaderDropdown = () => { const [profileVisible, setProfileVisible] = useState(false) + const [cippQueueExtendedInfo, setCippQueueExtendedInfo] = useState([]) + const [cippQueueVisible, setCippQueueVisible] = useState(false) + const [cippQueueRefresh, setCippQueueRefresh] = useState('') const { data: profile } = authApi.endpoints.loadClientPrincipal.useQueryState() + + const [getCippQueueList, cippQueueList] = useLazyGenericGetRequestQuery() + + function loadCippQueue() { + setCippQueueVisible(true) + setCippQueueRefresh((Math.random() + 1).toString(36).substring(7)) + getCippQueueList({ path: `api/ListCippQueue?refresh=${cippQueueRefresh}` }) + } + + useEffect(() => { + if (cippQueueList.isFetching) { + setCippQueueExtendedInfo([ + { + label: 'Fetching recent jobs', + value: 'Please wait', + timpestamp: Date(), + link: '#', + }, + ]) + } + if ( + cippQueueList.isSuccess && + Array.isArray(cippQueueList.data) && + cippQueueList.data.length > 0 + ) { + setCippQueueExtendedInfo( + cippQueueList.data?.map((job) => ({ + label: `${job.Name}`, + value: job.Status, + link: job.Link, + timestamp: job.Timestamp, + })), + ) + } else { + setCippQueueExtendedInfo([ + { label: 'No jobs to display', value: '', timpestamp: Date(), link: '#' }, + ]) + } + }, [cippQueueList]) + return ( <> @@ -30,6 +74,10 @@ const AppHeaderDropdown = () => { Profile + loadCippQueue()}> + + Recent Jobs + Logbook @@ -49,6 +97,25 @@ const AppHeaderDropdown = () => { > + , + }, + ]} + placement="end" + visible={cippQueueVisible} + id="cipp-queue" + hideFunction={() => setCippQueueVisible(false)} + /> ) } diff --git a/src/components/tables/CellBoolean.js b/src/components/tables/CellBoolean.js index 30962b8ec8aa..98f69dfa1adc 100644 --- a/src/components/tables/CellBoolean.js +++ b/src/components/tables/CellBoolean.js @@ -44,13 +44,15 @@ export default function CellBoolean({ cell.toLowerCase() === 'success' || cell.toLowerCase() === 'enabled' || cell.toLowerCase() === 'pass' || - cell.toLowerCase() === 'true' + cell.toLowerCase() === 'true' || + cell.toLowerCase() === 'compliant' ) { normalized = true } else if ( cell.toLowerCase() === 'fail' || cell.toLowerCase() === 'default' || - cell.toLowerCase() === 'false' + cell.toLowerCase() === 'false' || + cell.toLowerCase() === 'noncompliant' ) { normalized = false } diff --git a/src/components/tables/CippOffcanvasTable.js b/src/components/tables/CippOffcanvasTable.js index 0dbf984f721f..fe2f7e0e1c7c 100644 --- a/src/components/tables/CippOffcanvasTable.js +++ b/src/components/tables/CippOffcanvasTable.js @@ -22,6 +22,6 @@ CippOffcanvasTable.propTypes = { label: PropTypes.string, value: PropTypes.any, }), - ).isRequired, + ), guid: PropTypes.string, } diff --git a/src/components/utilities/CippActionsOffcanvas.js b/src/components/utilities/CippActionsOffcanvas.js index a3a61d5373fa..b24aa1a28c12 100644 --- a/src/components/utilities/CippActionsOffcanvas.js +++ b/src/components/utilities/CippActionsOffcanvas.js @@ -1,7 +1,12 @@ import React, { useRef } from 'react' import PropTypes from 'prop-types' import { + CButton, CCallout, + CCard, + CCardBody, + CCardText, + CCardTitle, CFormInput, CListGroup, CListGroupItem, @@ -12,8 +17,13 @@ import { CippOffcanvas, ModalService } from 'src/components/utilities' import { CippOffcanvasPropTypes } from 'src/components/utilities/CippOffcanvas' import { CippOffcanvasTable } from 'src/components/tables' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' -import { useNavigate } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { stringCamelCase } from 'src/components/utilities/CippCamelCase' +import TimeAgo from 'javascript-time-ago' + +import en from 'javascript-time-ago/locale/en.json' +TimeAgo.addDefaultLocale(en) +import ReactTimeAgo from 'react-time-ago' export default function CippActionsOffcanvas(props) { const inputRef = useRef('') @@ -91,6 +101,25 @@ export default function CippActionsOffcanvas(props) { var actualUrl = url.replaceAll('{value1}', value1).replaceAll('{value2}', value2) genericGetRequest({ path: actualUrl }) } + let cardContent + try { + cardContent = props.cards.map((action, index) => ( + <> + + + Report Name: {action.label} + + {action.value && Status: {action.value}} + + {action.timestamp && } + + + + )) + } catch (error) { + console.error('An error occurred building OCanvas actions' + error.toString()) + } + const extendedInfoContent = let actionsContent try { @@ -118,7 +147,7 @@ export default function CippActionsOffcanvas(props) { )) } catch (error) { - console.error('An error occored building OCanvas actions' + error.toString()) + console.error('An error occurred building OCanvas actions' + error.toString()) } let actionsSelectorsContent try { @@ -143,7 +172,7 @@ export default function CippActionsOffcanvas(props) { } catch (error) { // When we create an Off Canvas control without selectors we will get this if (!error.toString().includes("Cannot read properties of undefined (reading '")) { - console.error('An error occored building OCanvas selectors' + error.toString()) + console.error('An error occurred building OCanvas selectors' + error.toString()) } } return ( @@ -176,7 +205,9 @@ export default function CippActionsOffcanvas(props) { {getResults.isError && ( Could not connect to API: {getResults.error.message} )} + Extended Information + {cardContent && cardContent} {extendedInfoContent} {Actions} @@ -193,7 +224,7 @@ const CippActionsOffcanvasPropTypes = { label: PropTypes.string, value: PropTypes.any, }), - ).isRequired, + ), actions: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string, diff --git a/src/data/standards.json b/src/data/standards.json index 72b083fb4aaa..65db91dab662 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -150,7 +150,7 @@ "name": "standards.LegacyMFA", "helpText": "This is the default helptext", "addedComponent": null, - "label": "Enable per-user MFA for all user (Legacy)" + "label": "Enable per-user MFA for all user (Legacy, Requires DAP.)" }, { "cat": "AAD", @@ -161,11 +161,18 @@ }, { "cat": "AAD", - "name": "standards.NudgeMFA", + "name": "standards.NudgeMFA.enable", "helpText": "This is the default helptext", "addedComponent": null, "label": "Request to setup Authenticator if not setup yet." }, + { + "cat": "AAD", + "name": "standards.NudgeMFA.disable", + "helpText": "This is the default helptext", + "addedComponent": null, + "label": "Disables the request to setup Authenticator if setup." + }, { "cat": "AAD", "name": "standards.DisableSelfServiceLicenses", diff --git a/src/views/email-exchange/administration/ViewMobileDevices.js b/src/views/email-exchange/administration/ViewMobileDevices.js index 71e410efbb7e..f257c64bdb5f 100644 --- a/src/views/email-exchange/administration/ViewMobileDevices.js +++ b/src/views/email-exchange/administration/ViewMobileDevices.js @@ -1,89 +1,184 @@ -import React from 'react' +import React, { useState } from 'react' import { useSelector } from 'react-redux' import { CippPageList } from 'src/components/layout' import useQuery from 'src/hooks/useQuery' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { CellTip, cellDateFormatter } from 'src/components/tables' +import { faEye, faEdit, faEllipsisV, faMobileAlt } from '@fortawesome/free-solid-svg-icons' +import { CippActionsOffcanvas } from 'src/components/utilities' +import { Link } from 'react-router-dom' +import { CButton } from '@coreui/react' //TODO: Add CellBoolean -const columns = [ - { - selector: (row) => row['clientType'], - name: 'Client Type', - sortable: true, - cell: (row) => CellTip(row['clientType']), - exportSelector: 'clientType', - }, - { - selector: (row) => row['clientVersion'], - name: 'Client Version', - sortable: true, - exportSelector: 'clientVersion', - }, - { - selector: (row) => row['deviceAccessState'], - name: 'Access State', - sortable: true, - exportSelector: 'deviceAccessState', - }, - { - selector: (row) => row['deviceFriendlyName'], - name: 'Friendly Name', - sortable: true, - cell: (row) => CellTip(row['deviceFriendlyName']), - exportSelector: 'deviceFriendlyName', - }, - { - selector: (row) => row['deviceModel'], - name: 'Model', - sortable: true, - cell: (row) => CellTip(row['deviceModel']), - exportSelector: 'deviceModel', - }, - { - selector: (row) => row['deviceOS'], - name: 'OS', - sortable: true, - cell: (row) => CellTip(row['deviceOS']), - exportSelector: 'deviceOS', - }, - { - selector: (row) => row['deviceType'], - name: 'Device Type', - sortable: true, - cell: (row) => CellTip(row['deviceType']), - exportSelector: 'deviceType', - }, - { - selector: (row) => row['firstSync'], - name: 'First Sync', - sortable: true, - exportSelector: 'firstSync', - cell: cellDateFormatter(), - }, - { - selector: (row) => row['lastSyncAttempt'], - name: 'Last Sync Attempt', - sortable: true, - exportSelector: 'lastSyncAttempt', - cell: cellDateFormatter(), - }, - { - selector: (row) => row['lastSuccessSync'], - name: 'Last Succesfull Sync', - sortable: true, - exportSelector: 'lastSuccessSync', - cell: cellDateFormatter(), - }, - { - selector: (row) => row['status'], - name: 'Status', - sortable: true, - exportSelector: 'status', - }, -] const MobileDeviceList = () => { const tenant = useSelector((state) => state.app.currentTenant) + + const Offcanvas = (row, rowIndex, formatExtraData) => { + const [ocVisible, setOCVisible] = useState(false) + return ( + <> + setOCVisible(true)}> + + + setOCVisible(false)} + /> + + ) + } + + const columns = [ + { + selector: (row) => query.get('userId'), + name: 'User ID', + sortable: true, + cell: (row) => CellTip(query.get('userId')), + exportSelector: 'userId', + }, + { + selector: (row) => row['clientType'], + name: 'Client Type', + sortable: true, + cell: (row) => CellTip(row['clientType']), + exportSelector: 'clientType', + }, + { + selector: (row) => row['clientVersion'], + name: 'Client Version', + sortable: true, + exportSelector: 'clientVersion', + }, + { + selector: (row) => row['deviceAccessState'], + name: 'Access State', + sortable: true, + exportSelector: 'deviceAccessState', + }, + { + selector: (row) => row['deviceFriendlyName'], + name: 'Friendly Name', + sortable: true, + cell: (row) => CellTip(row['deviceFriendlyName']), + exportSelector: 'deviceFriendlyName', + }, + { + selector: (row) => row['deviceModel'], + name: 'Model', + sortable: true, + cell: (row) => CellTip(row['deviceModel']), + exportSelector: 'deviceModel', + }, + { + selector: (row) => row['deviceOS'], + name: 'OS', + sortable: true, + cell: (row) => CellTip(row['deviceOS']), + exportSelector: 'deviceOS', + }, + { + selector: (row) => row['deviceType'], + name: 'Device Type', + sortable: true, + cell: (row) => CellTip(row['deviceType']), + exportSelector: 'deviceType', + }, + { + selector: (row) => row['deviceID'], + name: 'Device ID', + sortable: true, + exportSelector: 'deviceID', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['firstSync'], + name: 'First Sync', + sortable: true, + exportSelector: 'firstSync', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['lastSyncAttempt'], + name: 'Last Sync Attempt', + sortable: true, + exportSelector: 'lastSyncAttempt', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['lastSuccessSync'], + name: 'Last Succesfull Sync', + sortable: true, + exportSelector: 'lastSuccessSync', + cell: cellDateFormatter(), + }, + { + selector: (row) => row['status'], + name: 'Status', + sortable: true, + exportSelector: 'status', + }, + { + selector: (row) => row['Guid'], + name: 'Guid', + sortable: true, + exportSelector: 'Guid', + }, + { + name: 'Actions', + cell: Offcanvas, + maxWidth: '75px', + }, + ] + let query = useQuery() const userId = query.get('userId') return ( @@ -96,9 +191,9 @@ const MobileDeviceList = () => { path: '/api/ListMailboxMobileDevices', columns, params: { TenantFilter: tenant?.defaultDomainName, mailbox: userId }, + selectableRows: true, }} /> ) } - export default MobileDeviceList diff --git a/src/views/email-exchange/reports/MailboxStatisticsList.js b/src/views/email-exchange/reports/MailboxStatisticsList.js index 5a4432e026c2..febb16cc0062 100644 --- a/src/views/email-exchange/reports/MailboxStatisticsList.js +++ b/src/views/email-exchange/reports/MailboxStatisticsList.js @@ -1,80 +1,105 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { CellTip, cellBooleanFormatter } from 'src/components/tables' import { CippPageList } from 'src/components/layout' -const conditionalRowStyles = [ - { - when: (row) => (row.UsedGB / row.QuotaGB) * 100 > 80 && (row.UsedGB / row.QuotaGB) * 100 < 90, - classNames: ['mbusage-warning'], - }, - { - when: (row) => (row.UsedGB / row.QuotaGB) * 100 > 90 && (row.UsedGB / row.QuotaGB) * 100 < 100, - classNames: ['mbusage-danger'], - }, -] - -const columns = [ - { - selector: (row) => row['UPN'], - name: 'User Prinicipal Name', - sortable: true, - cell: (row) => CellTip(row['UPN']), - exportSelector: 'UPN', - minWidth: '200px', - }, - { - selector: (row) => row['displayName'], - name: 'Display Name', - sortable: true, - cell: (row) => CellTip(row['displayName']), - exportSelector: 'displayName', - }, - { - selector: (row) => row['MailboxType'], - name: 'Mailbox Type', - sortable: true, - exportSelector: 'MailboxType', - }, - { - selector: (row) => row['LastActive'], - name: 'Last Active', - sortable: true, - exportSelector: 'LastActive', - }, - { - selector: (row) => row['UsedGB'], - name: 'Used Space(GB)', - sortable: true, - exportSelector: 'UsedGB', - }, - { - selector: (row) => row['QuotaGB'], - name: 'Quota (GB)', - sortable: true, - exportSelector: 'QuotaGB', - }, - { - selector: (row) => row['ItemCount'], - name: 'Item Count (Total)', - sortable: true, - exportSelector: 'ItemCount', - }, - { - selector: (row) => row['HasArchive'], - name: 'Archiving Enabled', - sortable: true, - cell: cellBooleanFormatter({ colourless: true }), - exportSelector: 'HasArchive', - }, -] - const MailboxStatsList = () => { + const [tenantColumnSet, setTenantColumn] = useState(true) const tenant = useSelector((state) => state.app.currentTenant) + const conditionalRowStyles = [ + { + when: (row) => (row.UsedGB / row.QuotaGB) * 100 > 80 && (row.UsedGB / row.QuotaGB) * 100 < 90, + classNames: ['mbusage-warning'], + }, + { + when: (row) => + (row.UsedGB / row.QuotaGB) * 100 > 90 && (row.UsedGB / row.QuotaGB) * 100 < 100, + classNames: ['mbusage-danger'], + }, + ] + const columns = [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + cell: (row) => CellTip(row['Tenant']), + exportSelector: 'Tenant', + omit: tenantColumnSet, + }, + { + name: 'Retrieval Status', + selector: (row) => row['CippStatus'], + sortable: true, + cell: (row) => CellTip(row['CippStatus']), + exportSelector: 'CippStatus', + omit: tenantColumnSet, + }, + { + selector: (row) => row['UPN'], + name: 'User Prinicipal Name', + sortable: true, + cell: (row) => CellTip(row['UPN']), + exportSelector: 'UPN', + minWidth: '200px', + }, + { + selector: (row) => row['displayName'], + name: 'Display Name', + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + }, + { + selector: (row) => row['MailboxType'], + name: 'Mailbox Type', + sortable: true, + exportSelector: 'MailboxType', + }, + { + selector: (row) => row['LastActive'], + name: 'Last Active', + sortable: true, + exportSelector: 'LastActive', + }, + { + selector: (row) => row['UsedGB'], + name: 'Used Space(GB)', + sortable: true, + exportSelector: 'UsedGB', + }, + { + selector: (row) => row['QuotaGB'], + name: 'Quota (GB)', + sortable: true, + exportSelector: 'QuotaGB', + }, + { + selector: (row) => row['ItemCount'], + name: 'Item Count (Total)', + sortable: true, + exportSelector: 'ItemCount', + }, + { + selector: (row) => row['HasArchive'], + name: 'Archiving Enabled', + sortable: true, + cell: cellBooleanFormatter({ colourless: true }), + exportSelector: 'HasArchive', + }, + ] + useEffect(() => { + if (tenant.defaultDomainName === 'AllTenants') { + setTenantColumn(false) + } + if (tenant.defaultDomainName !== 'AllTenants') { + setTenantColumn(true) + } + }, [tenantColumnSet]) return ( { AddContacts: values.AddContacts ? values.AddContacts : '', RemoveContacts: values.RemoveContacts ? values.RemoveContacts : '', allowExternal: values.allowExternal, + sendCopies: values.sendCopies, + mail: group[0].mail, } //window.alert(JSON.stringify(shippedValues)) genericPostRequest({ path: '/api/EditGroup', values: shippedValues }) @@ -235,6 +237,12 @@ const EditGroup = () => { label="Let people outside the organization email the group" /> )} + {group[0].calculatedGroupType === 'Microsoft 365' && ( + + )} diff --git a/src/views/identity/administration/UserActions.js b/src/views/identity/administration/UserActions.js index 4bb0d3818e35..78ca9e360fe8 100644 --- a/src/views/identity/administration/UserActions.js +++ b/src/views/identity/administration/UserActions.js @@ -8,6 +8,7 @@ import { faLockOpen, faUserTimes, faEllipsisH, + faEnvelope, } from '@fortawesome/free-solid-svg-icons' import { ActionContentCard } from 'src/components/contentcards' import { useLazyGenericGetRequestQuery } from 'src/store/api/app' @@ -46,6 +47,16 @@ export default function UserActions({ tenantDomain, userId, userEmail, className `/api/ExecSendPush?TenantFilter=${tenantDomain}&UserEmail=${userEmail}`, ), }, + { + label: 'Enable Online Archive ', + link: '#', + icon: faEnvelope, + onClick: () => + handleModal( + 'Are you sure you want to enable the online archive for this user?', + `/api/ExecEnableArchive?TenantFilter=${tenantDomain}&ID=${userEmail}`, + ), + }, { label: 'Convert to Shared Mailbox', link: '#', diff --git a/src/views/identity/administration/Users.js b/src/views/identity/administration/Users.js index 3b7a0e3a04e1..ca19f07ef253 100644 --- a/src/views/identity/administration/Users.js +++ b/src/views/identity/administration/Users.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { CButton } from '@coreui/react' import { Link } from 'react-router-dom' import { useSelector } from 'react-redux' @@ -200,68 +200,93 @@ const Offcanvas = (row, rowIndex, formatExtraData) => { ) } -const columns = [ - { - name: 'Display Name', - selector: (row) => row['displayName'], - sortable: true, - cell: (row) => CellTip(row['displayName']), - exportSelector: 'displayName', - minWidth: '300px', - }, - { - name: 'Email', - selector: (row) => row['mail'], - sortable: true, - cell: (row) => CellTip(row['mail']), - exportSelector: 'mail', - minWidth: '250px', - }, - { - name: 'User Type', - selector: (row) => row['userType'], - sortable: true, - exportSelector: 'userType', - minWidth: '140px', - }, - { - name: 'Enabled', - selector: (row) => row['accountEnabled'], - cell: cellBooleanFormatter({ colourless: true }), - sortable: true, - exportSelector: 'accountEnabled', - minWidth: '100px', - }, - { - name: 'AD Synced', - selector: (row) => row['onPremisesSyncEnabled'], - cell: cellBooleanFormatter({ colourless: true }), - sortable: true, - exportSelector: 'onPremisesSyncEnabled', - minWidth: '120px', - }, - { - name: 'Licenses', - selector: (row) => row['LicJoined'], - exportSelector: 'LicJoined', - sortable: true, - grow: 5, - wrap: true, - minWidth: '200px', - }, - { - name: 'id', - selector: (row) => row['id'], - omit: true, - }, - { - name: 'Actions', - cell: Offcanvas, - }, -] - const Users = (row) => { + const [tenantColumnSet, setTenantColumn] = useState(true) + const columns = [ + { + name: 'Tenant', + selector: (row) => row['Tenant'], + sortable: true, + cell: (row) => CellTip(row['Tenant']), + exportSelector: 'Tenant', + omit: tenantColumnSet, + }, + { + name: 'Retrieval Status', + selector: (row) => row['CippStatus'], + sortable: true, + cell: (row) => CellTip(row['CippStatus']), + exportSelector: 'CippStatus', + omit: tenantColumnSet, + }, + { + name: 'Display Name', + selector: (row) => row['displayName'], + sortable: true, + cell: (row) => CellTip(row['displayName']), + exportSelector: 'displayName', + minWidth: '300px', + }, + { + name: 'Email', + selector: (row) => row['mail'], + sortable: true, + cell: (row) => CellTip(row['mail']), + exportSelector: 'mail', + minWidth: '250px', + }, + { + name: 'User Type', + selector: (row) => row['userType'], + sortable: true, + exportSelector: 'userType', + minWidth: '140px', + }, + { + name: 'Enabled', + selector: (row) => row['accountEnabled'], + cell: cellBooleanFormatter({ colourless: true }), + sortable: true, + exportSelector: 'accountEnabled', + minWidth: '100px', + }, + { + name: 'AD Synced', + selector: (row) => row['onPremisesSyncEnabled'], + cell: cellBooleanFormatter({ colourless: true }), + sortable: true, + exportSelector: 'onPremisesSyncEnabled', + minWidth: '120px', + }, + { + name: 'Licenses', + selector: (row) => row['LicJoined'], + exportSelector: 'LicJoined', + sortable: true, + grow: 5, + wrap: true, + minWidth: '200px', + }, + { + name: 'id', + selector: (row) => row['id'], + omit: true, + }, + { + name: 'Actions', + cell: Offcanvas, + }, + ] const tenant = useSelector((state) => state.app.currentTenant) + useEffect(() => { + if (tenant.defaultDomainName === 'AllTenants') { + setTenantColumn(false) + } + if (tenant.defaultDomainName !== 'AllTenants') { + setTenantColumn(true) + } + }, [tenantColumnSet]) + const titleButtons = (

@@ -276,7 +301,7 @@ const Users = (row) => { ) return ( ( + + touched && error ? ( + + + {error} + + ) : null + } + /> +) + +Error.propTypes = { + name: PropTypes.string.isRequired, +} + +const requiredArray = (value) => (value && value.length !== 0 ? undefined : 'Required') + +const GDAPRoleWizard = () => { + const [genericPostRequest, postResults] = useLazyGenericPostRequestQuery() + const [genericGetRequest, getResults] = useLazyGenericGetRequestQuery() + + const handleSubmit = async (values) => { + genericPostRequest({ path: '/api/ExecAddGDAPRole', values: values }) + } + + const formValues = {} + + return ( + + +
+

Step 1

+
+ Select which roles you want to map to Groups in your Partner Tenant +
+
+
+ + + For each role you select a new group will be created inside of your partner tenant + called "M365 GDAP RoleName". Add your users to these new groups to set their GDAP + permissions. If you need to segment your groups for different teams or to define custom + permissions, use the Custom Suffix to create additional group mappings per role. + + + + + + + + + + {(props) => ( + row['Name'], + sortable: true, + exportselector: 'Name', + }, + { + name: 'Description', + selector: (row) => row['Description'], + sortable: true, + }, + ]} + fieldProps={props} + /> + )} + + + + + +
+
+ +
+

Step 2

+
Confirm and apply
+
+
+ {!postResults.isSuccess && ( + + {(props) => { + return ( + <> + + + +
Roles and group names
+ + {props.values.gdapRoles.map((role, idx) => ( +
  • + {role.Name} - M365 GDAP {role.Name} +
  • + ))} +
    + {props.values.customSuffix != null && ( + <> +
    Custom Group Suffix
    + +
  • {props.values.customSuffix}
  • +
    + + )} +
    +
    + + ) + }} +
    + )} + {postResults.isFetching && ( + + Loading + + )} + {postResults.isSuccess && ( + <> + + {postResults.data.Results.map((message, idx) => { + return
  • {message}
  • + })} +
    + + Start GDAP Migration + + + )} +
    +
    +
    + ) +} + +export default GDAPRoleWizard diff --git a/src/views/tenant/administration/GDAPWizard.js b/src/views/tenant/administration/GDAPWizard.js index 6ccac1b57599..b8c110be6c9c 100644 --- a/src/views/tenant/administration/GDAPWizard.js +++ b/src/views/tenant/administration/GDAPWizard.js @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' import { CippWizard } from 'src/components/layout' import { WizardTableField } from 'src/components/tables' +import { TitleButton } from 'src/components/buttons' import PropTypes from 'prop-types' import { useLazyGenericGetRequestQuery, useLazyGenericPostRequestQuery } from 'src/store/api/app' @@ -45,7 +46,7 @@ const GDAPWizard = () => { onSubmit={handleSubmit} wizardTitle="GDAP Migration Wizard" > - +

    Step 1

    Setup GDAP Migration tool
    @@ -82,48 +83,13 @@ const GDAPWizard = () => { )}
    - -
    -

    Step 2

    -
    Choose a tenant
    -
    -
    - - {(props) => ( - row['displayName'], - sortable: true, - exportselector: 'displayName', - }, - { - name: 'Default Domain Name', - selector: (row) => row['defaultDomainName'], - sortable: true, - exportselector: 'mail', - }, - ]} - fieldProps={props} - /> - )} - - -
    -
    +
    -

    Step 3

    +

    Step 2

    Select which roles you want to add to GDAP relationship.
    @@ -131,31 +97,30 @@ const GDAPWizard = () => {
    - For each role you select a new group will be created inside of your partner tenant - called "M365 GDAP RoleName". Add your users to these new groups to set their GDAP - permissions. -

    CIPP will create a single relationship with all roles you've selected for the maximum duration of 730 days using a GUID as a random name for the relationship.
    It is recommend to put CIPP user in the correct GDAP Role Groups to manage your environment secure after deployment of GDAP.
    +
    + +
    {(props) => ( row['Name'], + selector: (row) => row['RoleName'], sortable: true, exportselector: 'Name', }, { - name: 'Description', - selector: (row) => row['Description'], + name: 'Group', + selector: (row) => row['GroupName'], sortable: true, }, ]} @@ -167,6 +132,42 @@ const GDAPWizard = () => {

    + +
    +

    Step 3

    +
    Choose a tenant
    +
    +
    + + {(props) => ( + row['displayName'], + sortable: true, + exportselector: 'displayName', + }, + { + name: 'Default Domain Name', + selector: (row) => row['defaultDomainName'], + sortable: true, + exportselector: 'mail', + }, + ]} + fieldProps={props} + /> + )} + + +
    +

    Step 4

    @@ -193,7 +194,7 @@ const GDAPWizard = () => { {props.values.gdapRoles.map((role, idx) => (
  • - {role.Name} - M365 GDAP {role.Name} + {role.RoleName} - {role.GroupName}
  • ))}
    diff --git a/src/views/tenant/administration/ListAlertsQueue.js b/src/views/tenant/administration/ListAlertsQueue.js index 4d6bf2e72e8f..485c11b5444c 100644 --- a/src/views/tenant/administration/ListAlertsQueue.js +++ b/src/views/tenant/administration/ListAlertsQueue.js @@ -167,11 +167,11 @@ const ListAlertsQueue = () => { selectableRows: true, actionsList: [ { - label: 'Delete Template', + label: 'Delete alerts', color: 'info', modal: true, modalUrl: `/api/RemoveQueuedAlert?ID=!tenantId`, - modalMessage: 'Are you sure you want to delete these templates?', + modalMessage: 'Are you sure you want to delete these alerts?', }, ], }, diff --git a/src/views/tenant/administration/ListGDAPRoles.js b/src/views/tenant/administration/ListGDAPRoles.js new file mode 100644 index 000000000000..f7279740b20c --- /dev/null +++ b/src/views/tenant/administration/ListGDAPRoles.js @@ -0,0 +1,42 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { CSpinner, CCallout } from '@coreui/react' +import { CippPageList } from 'src/components/layout' +import { TitleButton } from 'src/components/buttons' + +const ListGDAPRoles = () => { + const columns = [ + { + name: 'Role', + selector: (row) => row['RoleName'], + sortable: true, + exportSelector: 'RoleName', + }, + { + name: 'Group', + selector: (row) => row['GroupName'], + sortable: true, + exportSelector: 'GroupName', + }, + ] + return ( +
    + + } + tenantSelector={false} + datatable={{ + keyField: 'id', + columns, + reportName: `GDAPRole-List`, + path: '/api/ListGDAPRoles', + }} + /> +
    + ) +} + +export default ListGDAPRoles diff --git a/version_latest.txt b/version_latest.txt index 0fa4ae489037..fbcbf7380658 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -3.3.0 \ No newline at end of file +3.4.0 \ No newline at end of file