Revert "Revert "Group SSO - Adding a bug fix for sending emails"" (#15307)

* Revert "Revert "Group SSO - Adding a bug fix for sending emails""
* adding conditional rendering of columns and styling fixes for each render mode with some cypress test

GitOrigin-RevId: 168011503ffacff61c8f37bee4c4bfb012909c1f
This commit is contained in:
Davinder Singh
2023-10-20 08:04:08 +00:00
committed by Copybot
parent 554cd6a4d9
commit 8801d03f55
14 changed files with 237 additions and 22 deletions
@@ -17,9 +17,14 @@ const EmailHelper = require('../Helpers/EmailHelper')
const { csvAttachment } = require('../../infrastructure/Response') const { csvAttachment } = require('../../infrastructure/Response')
const { UserIsManagerError } = require('./UserMembershipErrors') const { UserIsManagerError } = require('./UserMembershipErrors')
const CSVParser = require('json2csv').Parser const CSVParser = require('json2csv').Parser
const SSOConfigManager = require('../../../../modules/managed-users/app/src/SSOConfigManager')
async function manageGroupMembers(req, res, next) { async function manageGroupMembers(req, res, next) {
const { entity, entityConfig } = req const { entity, entityConfig } = req
const ssoConfig = await SSOConfigManager.promises.getSSOConfig(
entity.ssoConfig
)
return entity.fetchV1Data(function (error, entity) { return entity.fetchV1Data(function (error, entity) {
if (error != null) { if (error != null) {
return next(error) return next(error)
@@ -37,12 +42,14 @@ async function manageGroupMembers(req, res, next) {
if (entityConfig.fields.name) { if (entityConfig.fields.name) {
entityName = entity[entityConfig.fields.name] entityName = entity[entityConfig.fields.name]
} }
return res.render('user_membership/group-members-react', { return res.render('user_membership/group-members-react', {
name: entityName, name: entityName,
groupId: entityPrimaryKey, groupId: entityPrimaryKey,
users, users,
groupSize: entity.membersLimit, groupSize: entity.membersLimit,
managedUsersActive: entity.groupPolicy != null, managedUsersActive: entity.groupPolicy != null,
groupSSOActive: ssoConfig?.enabled,
}) })
} }
) )
@@ -9,6 +9,7 @@ block append meta
meta(name="ol-groupName", data-type="string", content=name) meta(name="ol-groupName", data-type="string", content=name)
meta(name="ol-groupSize", data-type="json", content=groupSize) meta(name="ol-groupSize", data-type="json", content=groupSize)
meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive) meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive)
meta(name="ol-groupSSOActive", data-type="boolean", content=groupSSOActive)
block content block content
main.content.content-alt#subscription-manage-group-root main.content.content-alt#subscription-manage-group-root
@@ -1058,6 +1058,7 @@
"sort_by_x": "", "sort_by_x": "",
"source": "", "source": "",
"spell_check": "", "spell_check": "",
"sso": "",
"sso_config_prop_help_certificate": "", "sso_config_prop_help_certificate": "",
"sso_config_prop_help_first_name": "", "sso_config_prop_help_first_name": "",
"sso_config_prop_help_last_name": "", "sso_config_prop_help_last_name": "",
@@ -1075,7 +1076,9 @@
"sso_is_enabled_explanation_1": "", "sso_is_enabled_explanation_1": "",
"sso_is_enabled_explanation_2": "", "sso_is_enabled_explanation_2": "",
"sso_link_error": "", "sso_link_error": "",
"sso_linked": "",
"sso_logs": "", "sso_logs": "",
"sso_unlinked": "",
"start_a_free_trial": "", "start_a_free_trial": "",
"start_by_adding_your_email": "", "start_by_adding_your_email": "",
"start_free_trial": "", "start_free_trial": "",
@@ -6,8 +6,10 @@ import Badge from '../../../../shared/components/badge'
import Tooltip from '../../../../shared/components/tooltip' import Tooltip from '../../../../shared/components/tooltip'
import type { ManagedUserAlert } from '../../utils/types' import type { ManagedUserAlert } from '../../utils/types'
import ManagedUserStatus from './managed-user-status' import ManagedUserStatus from './managed-user-status'
import SSOStatus from './sso-status'
import ManagedUserDropdownButton from './managed-user-dropdown-button' import ManagedUserDropdownButton from './managed-user-dropdown-button'
import ManagedUsersSelectUserCheckbox from './managed-users-select-user-checkbox' import ManagedUsersSelectUserCheckbox from './managed-users-select-user-checkbox'
import getMeta from '@/utils/meta'
type ManagedUserRowProps = { type ManagedUserRowProps = {
user: User user: User
@@ -23,6 +25,7 @@ export default function ManagedUserRow({
groupId, groupId,
}: ManagedUserRowProps) { }: ManagedUserRowProps) {
const { t } = useTranslation() const { t } = useTranslation()
const groupSSOActive = getMeta('ol-groupSSOActive')
return ( return (
<tr <tr
@@ -30,7 +33,7 @@ export default function ManagedUserRow({
className={`managed-user-row ${user.invite ? 'text-muted' : ''}`} className={`managed-user-row ${user.invite ? 'text-muted' : ''}`}
> >
<ManagedUsersSelectUserCheckbox user={user} /> <ManagedUsersSelectUserCheckbox user={user} />
<td className="cell-email"> <td className={groupSSOActive ? 'cell-email-with-sso-col' : 'cell-email'}>
<span> <span>
{user.email} {user.email}
{user.invite ? ( {user.invite ? (
@@ -73,7 +76,14 @@ export default function ManagedUserRow({
? moment(user.last_active_at).format('Do MMM YYYY') ? moment(user.last_active_at).format('Do MMM YYYY')
: 'N/A'} : 'N/A'}
</td> </td>
<td className="cell-security"> {groupSSOActive && (
<td className="cell-security">
<div className="managed-user-security">
<SSOStatus user={user} />
</div>
</td>
)}
<td className="cell-managed">
<div className="managed-user-security"> <div className="managed-user-security">
<ManagedUserStatus user={user} /> <ManagedUserStatus user={user} />
</div> </div>
@@ -9,6 +9,7 @@ import ManagedUserRow from './managed-user-row'
import OffboardManagedUserModal from './offboard-managed-user-modal' import OffboardManagedUserModal from './offboard-managed-user-modal'
import ManagedUsersListAlert from './managed-users-list-alert' import ManagedUsersListAlert from './managed-users-list-alert'
import ManagedUsersSelectAllCheckbox from './managed-users-select-all-checkbox' import ManagedUsersSelectAllCheckbox from './managed-users-select-all-checkbox'
import getMeta from '@/utils/meta'
type ManagedUsersListProps = { type ManagedUsersListProps = {
groupId: string groupId: string
@@ -22,6 +23,7 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
const [managedUserAlert, setManagedUserAlert] = const [managedUserAlert, setManagedUserAlert] =
useState<ManagedUserAlert>(undefined) useState<ManagedUserAlert>(undefined)
const { users } = useGroupMembersContext() const { users } = useGroupMembersContext()
const groupSSOActive = getMeta('ol-groupSSOActive')
return ( return (
<div> <div>
@@ -40,7 +42,13 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
<thead> <thead>
<tr> <tr>
<ManagedUsersSelectAllCheckbox /> <ManagedUsersSelectAllCheckbox />
<td className="cell-email"> <td
className={
groupSSOActive
? 'cell-email-with-sso-col'
: 'cell-email'
}
>
<span className="header">{t('email')}</span> <span className="header">{t('email')}</span>
</td> </td>
<td className="cell-name"> <td className="cell-name">
@@ -60,8 +68,13 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
</span> </span>
</Tooltip> </Tooltip>
</td> </td>
<td className="cell-security"> {groupSSOActive && (
<span className="header">{t('security')}</span> <td className="cell-security">
<span className="header">{t('security')}</span>
</td>
)}
<td className="cell-managed">
<span className="header">{t('managed')}</span>
</td> </td>
<td /> <td />
</tr> </tr>
@@ -1,9 +1,11 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useGroupMembersContext } from '../../context/group-members-context' import { useGroupMembersContext } from '../../context/group-members-context'
import getMeta from '@/utils/meta'
export default function ManagedUsersSelectAllCheckbox() { export default function ManagedUsersSelectAllCheckbox() {
const { t } = useTranslation() const { t } = useTranslation()
const groupSSOActive = getMeta('ol-groupSSOActive')
const { selectedUsers, users, selectAllNonManagedUsers, unselectAllUsers } = const { selectedUsers, users, selectAllNonManagedUsers, unselectAllUsers } =
useGroupMembersContext() useGroupMembersContext()
@@ -28,7 +30,11 @@ export default function ManagedUsersSelectAllCheckbox() {
} }
return ( return (
<td className="cell-checkbox"> <td
className={
groupSSOActive ? 'cell-checkbox-with-sso-col ' : 'cell-checkbox'
}
>
<label htmlFor="select-all" className="sr-only"> <label htmlFor="select-all" className="sr-only">
{t('select_all')} {t('select_all')}
</label> </label>
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
import type { User } from '../../../../../../types/group-management/user' import type { User } from '../../../../../../types/group-management/user'
import { useGroupMembersContext } from '../../context/group-members-context' import { useGroupMembersContext } from '../../context/group-members-context'
import { useCallback } from 'react' import { useCallback } from 'react'
import getMeta from '@/utils/meta'
type ManagedUsersSelectUserCheckboxProps = { type ManagedUsersSelectUserCheckboxProps = {
user: User user: User
@@ -11,6 +12,7 @@ export default function ManagedUsersSelectUserCheckbox({
user, user,
}: ManagedUsersSelectUserCheckboxProps) { }: ManagedUsersSelectUserCheckboxProps) {
const { t } = useTranslation() const { t } = useTranslation()
const groupSSOActive = getMeta('ol-groupSSOActive')
const { users, selectedUsers, selectUser, unselectUser } = const { users, selectedUsers, selectUser, unselectUser } =
useGroupMembersContext() useGroupMembersContext()
@@ -38,7 +40,11 @@ export default function ManagedUsersSelectUserCheckbox({
const selected = selectedUsers.includes(user) const selected = selectedUsers.includes(user)
return ( return (
<td className="cell-checkbox"> <td
className={
groupSSOActive ? 'cell-checkbox-with-sso-col ' : 'cell-checkbox'
}
>
{/* the next check will hide the `checkbox` but still show the `td` */} {/* the next check will hide the `checkbox` but still show the `td` */}
{user.enrollment?.managedBy ? null : ( {user.enrollment?.managedBy ? null : (
<> <>
@@ -0,0 +1,41 @@
import { useTranslation } from 'react-i18next'
import { User } from '../../../../../../types/group-management/user'
import MaterialIcon from '@/shared/components/material-icon'
type SSOStatusProps = {
user: User
}
export default function SSOStatus({ user }: SSOStatusProps) {
const { t } = useTranslation()
return (
<span>
{user.invite ? (
<span className="security-state-invite-pending">
<MaterialIcon
type="schedule"
category="outlined"
accessibilityLabel={t('pending_invite')}
/>
&nbsp; {t('sso')}
</span>
) : (
<>
{user.enrollment?.sso ? (
<span className="security-state-managed">
<MaterialIcon type="check" accessibilityLabel={t('sso_linked')} />
&nbsp; {t('sso')}
</span>
) : (
<span className="security-state-not-managed">
<MaterialIcon
type="close"
accessibilityLabel={t('sso_unlinked')}
/>
&nbsp; {t('sso')}
</span>
)}
</>
)}
</span>
)
}
@@ -106,10 +106,16 @@
.cell-checkbox { .cell-checkbox {
width: 5%; width: 5%;
} }
.cell-checkbox-with-sso-col {
width: 2.5%;
}
.cell-email { .cell-email {
width: 45%; width: 45%;
} }
.cell-email-with-sso-col {
width: 37%;
}
.cell-name { .cell-name {
width: 15%; width: 15%;
@@ -126,6 +132,13 @@
} }
.cell-security { .cell-security {
width: 10%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-managed {
width: 15%; width: 15%;
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -142,9 +155,16 @@
width: 5%; width: 5%;
} }
.cell-checkbox-with-sso-col {
width: 2.5%;
}
.cell-email { .cell-email {
width: 34%; width: 34%;
} }
.cell-email-with-sso-col {
width: 29%;
}
.cell-name { .cell-name {
width: 20%; width: 20%;
@@ -155,6 +175,10 @@
} }
.cell-security { .cell-security {
width: 12%;
}
.cell-managed {
width: 15%; width: 15%;
} }
@@ -168,10 +192,16 @@
.cell-checkbox { .cell-checkbox {
width: 5%; width: 5%;
} }
.cell-checkbox-with-sso-col {
width: 2.5%;
}
.cell-email { .cell-email {
width: 43%; width: 43%;
} }
.cell-email-with-sso-col {
width: 37%;
}
.cell-name { .cell-name {
width: 20%; width: 20%;
@@ -182,6 +212,10 @@
} }
.cell-security { .cell-security {
width: 10%;
}
.cell-managed {
width: 12%; width: 12%;
} }
+3
View File
@@ -1628,6 +1628,7 @@
"source": "Source", "source": "Source",
"spell_check": "Spell check", "spell_check": "Spell check",
"spread_the_word_and_fill_bar": "Spread the word and fill this bar up", "spread_the_word_and_fill_bar": "Spread the word and fill this bar up",
"sso": "SSO",
"sso_account_already_linked": "Account already linked to another __appName__ user", "sso_account_already_linked": "Account already linked to another __appName__ user",
"sso_config_prop_help_certificate": "Base64 encoded certificate without whitespace", "sso_config_prop_help_certificate": "Base64 encoded certificate without whitespace",
"sso_config_prop_help_first_name": "Property in SAML assertion to use for first name", "sso_config_prop_help_first_name": "Property in SAML assertion to use for first name",
@@ -1648,8 +1649,10 @@
"sso_is_enabled_explanation_1": "Group members will <0>only</0> be able to sign in via SSO", "sso_is_enabled_explanation_1": "Group members will <0>only</0> be able to sign in via SSO",
"sso_is_enabled_explanation_2": "If there are any problems with the configuration, only you (as the group administrator) will be able to disable SSO.", "sso_is_enabled_explanation_2": "If there are any problems with the configuration, only you (as the group administrator) will be able to disable SSO.",
"sso_link_error": "Error linking account", "sso_link_error": "Error linking account",
"sso_linked": "SSO linked",
"sso_logs": "SSO Logs", "sso_logs": "SSO Logs",
"sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.", "sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.",
"sso_unlinked": "SSO unlinked",
"sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.", "sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.",
"standard": "Standard", "standard": "Standard",
"start_a_free_trial": "Start a free trial", "start_a_free_trial": "Start a free trial",
@@ -28,6 +28,10 @@ const CLAIRE_JENNINGS = {
enrollment: { enrollment: {
managedBy: GROUP_ID, managedBy: GROUP_ID,
enrolledAt: new Date('2023-01-03'), enrolledAt: new Date('2023-01-03'),
sso: {
providerId: '123',
externalId: '123',
},
}, },
} }
const PATHS = { const PATHS = {
@@ -37,6 +41,14 @@ const PATHS = {
exportMembers: `/manage/groups/${GROUP_ID}/members/export`, exportMembers: `/manage/groups/${GROUP_ID}/members/export`,
} }
function mountGroupMembersProvider() {
cy.mount(
<GroupMembersProvider>
<GroupMembers />
</GroupMembersProvider>
)
}
describe('group members, with managed users', function () { describe('group members, with managed users', function () {
beforeEach(function () { beforeEach(function () {
cy.window().then(win => { cy.window().then(win => {
@@ -51,12 +63,7 @@ describe('group members, with managed users', function () {
win.metaAttributesCache.set('ol-groupSize', 10) win.metaAttributesCache.set('ol-groupSize', 10)
win.metaAttributesCache.set('ol-managedUsersActive', true) win.metaAttributesCache.set('ol-managedUsersActive', true)
}) })
mountGroupMembersProvider()
cy.mount(
<GroupMembersProvider>
<GroupMembers />
</GroupMembersProvider>
)
}) })
it('renders the group members page', function () { it('renders the group members page', function () {
@@ -209,3 +216,55 @@ describe('group members, with managed users', function () {
cy.get('.alert').contains('Sorry, something went wrong') cy.get('.alert').contains('Sorry, something went wrong')
}) })
}) })
describe('Group members when group SSO is enabled', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
win.metaAttributesCache.set('ol-users', [
JOHN_DOE,
BOBBY_LAPOINTE,
CLAIRE_JENNINGS,
])
win.metaAttributesCache.set('ol-groupId', GROUP_ID)
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
win.metaAttributesCache.set('ol-groupSize', 10)
win.metaAttributesCache.set('ol-managedUsersActive', true)
})
})
it('should not display SSO Column when group sso is not enabled', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', false)
})
mountGroupMembersProvider()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.get('.sr-only').contains('SSO unlinked').should('not.exist')
})
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO linked').should('not.exist')
})
})
})
it('should display SSO Column when group sso is not enabled', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
mountGroupMembersProvider()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.get('.sr-only').contains('SSO unlinked')
})
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO linked')
})
})
})
})
@@ -1,9 +1,17 @@
import ManagedUsersList from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-users-list' import ManagedUsersList from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-users-list'
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context' import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
describe('ManagedUsersList', function () { const groupId = 'somegroup'
const groupId = 'somegroup'
function mountManagedUsersList() {
cy.mount(
<GroupMembersProvider>
<ManagedUsersList groupId={groupId} />
</GroupMembersProvider>
)
}
describe('ManagedUsersList', function () {
describe('with users', function () { describe('with users', function () {
const users = [ const users = [
{ {
@@ -32,15 +40,31 @@ describe('ManagedUsersList', function () {
cy.window().then(win => { cy.window().then(win => {
win.metaAttributesCache.set('ol-users', users) win.metaAttributesCache.set('ol-users', users)
}) })
mountManagedUsersList()
cy.mount(
<GroupMembersProvider>
<ManagedUsersList groupId={groupId} />
</GroupMembersProvider>
)
}) })
it('should render the table headers', function () { it('should render the table headers but not SSO Column', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', false)
})
mountManagedUsersList()
cy.get('#managed-users-list-headers').should('exist')
// Select-all checkbox
cy.get('#managed-users-list-headers .select-all').should('exist')
cy.get('#managed-users-list-headers').contains('Email')
cy.get('#managed-users-list-headers').contains('Name')
cy.get('#managed-users-list-headers').contains('Last Active')
cy.get('#managed-users-list-headers')
.contains('Security')
.should('not.exist')
})
it('should render the table headers with SSO Column', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
mountManagedUsersList()
cy.get('#managed-users-list-headers').should('exist') cy.get('#managed-users-list-headers').should('exist')
// Select-all checkbox // Select-all checkbox
@@ -69,6 +69,11 @@ describe('UserMembershipController', function () {
getSessionUser: sinon.stub().returns(this.user), getSessionUser: sinon.stub().returns(this.user),
getLoggedInUserId: sinon.stub().returns(this.user._id), getLoggedInUserId: sinon.stub().returns(this.user._id),
} }
this.SSOConfigManager = {
promises: {
getSSOConfig: sinon.stub().resolves({ enabled: true }),
},
}
this.UserMembershipHandler = { this.UserMembershipHandler = {
getEntity: sinon.stub().yields(null, this.subscription), getEntity: sinon.stub().yields(null, this.subscription),
createEntity: sinon.stub().yields(null, this.institution), createEntity: sinon.stub().yields(null, this.institution),
@@ -91,6 +96,8 @@ describe('UserMembershipController', function () {
'../SplitTests/SplitTestHandler': this.SplitTestHandler, '../SplitTests/SplitTestHandler': this.SplitTestHandler,
'./UserMembershipHandler': this.UserMembershipHandler, './UserMembershipHandler': this.UserMembershipHandler,
'@overleaf/settings': this.Settings, '@overleaf/settings': this.Settings,
'../../../../modules/managed-users/app/src/SSOConfigManager':
this.SSOConfigManager,
}, },
} }
)) ))
@@ -1,6 +1,7 @@
export type UserEnrollment = { export type UserEnrollment = {
managedBy?: string managedBy?: string
enrolledAt?: Date enrolledAt?: Date
sso?: object
} }
export type User = { export type User = {