From c689dbfffab924aba174c4aedc8cb9605c57399a Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:28:55 -0500 Subject: [PATCH] Merge pull request #17315 from overleaf/ab-accounts-settings-sso-status [web] Show Group SSO linking status on the account settings page GitOrigin-RevId: ae45e1bd7a90a672c5fb023e7f3e603a00e364e5 --- .../src/Features/User/UserPagesController.js | 29 ++ services/web/app/views/user/settings.pug | 1 + .../web/frontend/extracted-translations.json | 6 + .../js/features/settings/components/root.tsx | 2 + .../settings/components/security-section.tsx | 105 ++++++ .../stylesheets/app/account-settings.less | 51 +++ services/web/locales/en.json | 6 + .../components/linking-section.test.tsx | 6 +- .../components/security-section.test.tsx | 45 +++ .../unit/src/User/UserPagesControllerTests.js | 311 +++++++++++------- services/web/types/subscription/sso.ts | 7 + 11 files changed, 446 insertions(+), 123 deletions(-) create mode 100644 services/web/frontend/js/features/settings/components/security-section.tsx create mode 100644 services/web/test/frontend/features/settings/components/security-section.test.tsx diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js index b1af6d5546..b98bd6efc7 100644 --- a/services/web/app/src/Features/User/UserPagesController.js +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -11,6 +11,7 @@ const _ = require('lodash') const { expressify } = require('@overleaf/promise-utils') const Features = require('../../infrastructure/Features') const SplitTestHandler = require('../SplitTests/SplitTestHandler') +const Modules = require('../../infrastructure/Modules') async function settingsPage(req, res) { const userId = SessionManager.getLoggedInUserId(req.session) @@ -110,6 +111,33 @@ async function settingsPage(req, res) { logger.error({ err }, 'error getting subscription admin email') } + const memberOfSSOEnabledGroups = [] + try { + const memberOfGroups = + await SubscriptionLocator.promises.getMemberSubscriptions(user._id) + for (const group of memberOfGroups) { + const hasSSOEnabled = ( + await Modules.promises.hooks.fire('hasGroupSSOEnabled', group) + )?.[0] + if (hasSSOEnabled) { + const groupId = group._id.toString() + memberOfSSOEnabledGroups.push({ + groupId, + linked: user.enrollment?.sso?.some( + sso => sso.groupId.toString() === groupId + ), + groupName: group.teamName, + adminEmail: group.admin_id?.email, + }) + } + } + } catch (error) { + logger.error( + { err: error }, + 'error fetching groups with Group SSO enabled the user may be member of' + ) + } + res.render('user/settings', { title: 'account_settings', user: { @@ -164,6 +192,7 @@ async function settingsPage(req, res) { currentManagedUserAdminEmail, gitBridgeEnabled: Settings.enableGitBridge, isSaas: Features.hasFeature('saas'), + memberOfSSOEnabledGroups, }) } diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 95560ca5a5..37803aa243 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -29,6 +29,7 @@ block append meta meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail) meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled) meta(name="ol-isSaas" data-type="boolean" content=isSaas) + meta(name="ol-memberOfSSOEnabledGroups" data-type="json" content=memberOfSSOEnabledGroups) block content main.content.content-alt#main-content diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 0c997981ca..59d4830112 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -29,6 +29,7 @@ "account_settings": "", "acct_linked_to_institution_acct_2": "", "actions": "", + "active": "", "add": "", "add_additional_certificate": "", "add_affiliation": "", @@ -975,6 +976,7 @@ "read_write_token": "", "ready_to_join_x": "", "ready_to_join_x_in_group_y": "", + "ready_to_set_up": "", "realtime_track_changes": "", "reasons_for_compile_timeouts": "", "reauthorize_github_account": "", @@ -1217,6 +1219,10 @@ "sso_test_interstitial_info_2": "", "sso_test_interstitial_title": "", "sso_test_result_error_message": "", + "sso_user_explanation_enabled_with_admin_email": "", + "sso_user_explanation_enabled_with_group_name": "", + "sso_user_explanation_ready_with_admin_email": "", + "sso_user_explanation_ready_with_group_name": "", "start_a_free_trial": "", "start_by_adding_your_email": "", "start_free_trial": "", diff --git a/services/web/frontend/js/features/settings/components/root.tsx b/services/web/frontend/js/features/settings/components/root.tsx index ccad776df7..37d70594bc 100644 --- a/services/web/frontend/js/features/settings/components/root.tsx +++ b/services/web/frontend/js/features/settings/components/root.tsx @@ -1,3 +1,4 @@ +import SecuritySection from '@/features/settings/components/security-section' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import getMeta from '../../../utils/meta' @@ -64,6 +65,7 @@ function SettingsPageContent() {
+ diff --git a/services/web/frontend/js/features/settings/components/security-section.tsx b/services/web/frontend/js/features/settings/components/security-section.tsx new file mode 100644 index 0000000000..4ecc615001 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/security-section.tsx @@ -0,0 +1,105 @@ +import MaterialIcon from '@/shared/components/material-icon' +import { Trans, useTranslation } from 'react-i18next' +import { GroupSSOLinkingStatus } from '../../../../../types/subscription/sso' +import getMeta from '../../../utils/meta' + +function SecuritySection() { + const { t } = useTranslation() + + const memberOfSSOEnabledGroups = getMeta( + 'ol-memberOfSSOEnabledGroups', + [] + ) as GroupSSOLinkingStatus[] + + return ( + <> + {memberOfSSOEnabledGroups?.length > 0 ? ( + <> +

{t('security')}

+ {memberOfSSOEnabledGroups.map( + ({ + groupId, + linked, + groupName, + adminEmail, + }: GroupSSOLinkingStatus) => ( +
+ + + +
+ + {t('single_sign_on_sso')}{' '} + {linked ? ( + + {t('active')} + + ) : ( + + {t('ready_to_set_up')} + + )} + +
+ {linked ? ( + groupName ? ( + ]} + values={{ groupName }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> + ) : ( + ]} + values={{ adminEmail }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> + ) + ) : groupName ? ( + , ]} + values={{ groupName, buttonText: t('set_up_sso') }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> + ) : ( + , ]} + values={{ adminEmail, buttonText: t('set_up_sso') }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> + )} +
+
+ {linked ? null : ( + + )} +
+ ) + )} +
+ + ) : null} + + ) +} + +export default SecuritySection diff --git a/services/web/frontend/stylesheets/app/account-settings.less b/services/web/frontend/stylesheets/app/account-settings.less index 3fea13f700..b6fb318591 100644 --- a/services/web/frontend/stylesheets/app/account-settings.less +++ b/services/web/frontend/stylesheets/app/account-settings.less @@ -252,3 +252,54 @@ tbody > tr.affiliations-table-warning-row > td { text-decoration: underline; } } + +.security-row { + .line-header > b { + color: @ol-blue-gray-6; + } + + line-height: 24px; + color: @ol-blue-gray-4; + display: flex; + flex-direction: row; + padding: 6px 0; + + .icon { + color: @ol-blue-gray-6; + display: flex; + flex: 1 1 7%; + padding: 0 16px; + margin-top: 16px; + } + + .text { + flex: 1 1 93%; + display: flex; + flex-direction: column; + margin-right: 16px; + } + + .button-column { + display: flex; + align-items: center; + } + + .status-label { + font-size: @font-size-small; + border-radius: 4px; + padding: 2px 4px; + margin-top: 4px; + margin-left: 8px; + flex-shrink: 0; + + &.status-label-configured { + background-color: @ol-green; + color: @neutral-10; + } + + &.status-label-ready { + background-color: @neutral-20; + color: @neutral-90; + } + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index bff90bf094..5258c5d5b9 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -297,6 +297,7 @@ "complete": "Complete", "compliance": "Compliance", "configure_sso": "Configure SSO", + "configured": "Configured", "confirm": "Confirm", "confirm_affiliation": "Confirm Affiliation", "confirm_affiliation_to_relink_dropbox": "Please confirm you are still at the institution and on their license, or upgrade your account in order to relink your Dropbox account.", @@ -1440,6 +1441,7 @@ "read_write_token": "Read-Write Token", "ready_to_join_x": "You’re ready to join __inviterName__", "ready_to_join_x_in_group_y": "You’re ready to join __inviterName__ in __groupName__", + "ready_to_set_up": "Ready to set up", "real_time_track_changes": "Real-time <0>track-changes", "realtime_collab": "Real-time collaboration", "realtime_collab_info": "When you’re working together, you can see your collaborators’ cursors and their changes in real time, so everyone always has the latest version.", @@ -1765,6 +1767,10 @@ "sso_test_result_error_message": "The test hasn’t worked this time, but don’t worry — errors can usually be quickly addressed by adjusting the configuration settings. Our <0>SSO troubleshooting guide provides help with some of the common causes of testing errors.", "sso_title": "Single sign-on", "sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.", + "sso_user_explanation_enabled_with_admin_email": "Your group administered by <0>__adminEmail__ has SSO enabled so you can log in without needing to remember a password.", + "sso_user_explanation_enabled_with_group_name": "Your group <0>__groupName__ has SSO enabled so you can log in without needing to remember a password.", + "sso_user_explanation_ready_with_admin_email": "Your group administered by <0>__adminEmail__ has SSO enabled so you can log in without needing to remember a password. Click <1>__buttonText__ to get started.", + "sso_user_explanation_ready_with_group_name": "Your group <0>__groupName__ has SSO enabled so you can log in without needing to remember a password. Click <1>__buttonText__ to get started.", "standard": "Standard", "start_a_free_trial": "Start a free trial", "start_by_adding_your_email": "Start by adding your email address.", diff --git a/services/web/test/frontend/features/settings/components/linking-section.test.tsx b/services/web/test/frontend/features/settings/components/linking-section.test.tsx index 317bace8d0..b0e63ec6f4 100644 --- a/services/web/test/frontend/features/settings/components/linking-section.test.tsx +++ b/services/web/test/frontend/features/settings/components/linking-section.test.tsx @@ -1,9 +1,9 @@ import { expect } from 'chai' import { screen, render } from '@testing-library/react' import fetchMock from 'fetch-mock' -import LinkingSection from '../../../../../frontend/js/features/settings/components/linking-section' -import { UserProvider } from '../../../../../frontend/js/shared/context/user-context' -import { SSOProvider } from '../../../../../frontend/js/features/settings/context/sso-context' +import LinkingSection from '@/features/settings/components/linking-section' +import { UserProvider } from '@/shared/context/user-context' +import { SSOProvider } from '@/features/settings/context/sso-context' import { SplitTestProvider } from '@/shared/context/split-test-context' function renderSectionWithProviders() { diff --git a/services/web/test/frontend/features/settings/components/security-section.test.tsx b/services/web/test/frontend/features/settings/components/security-section.test.tsx new file mode 100644 index 0000000000..0145e76da8 --- /dev/null +++ b/services/web/test/frontend/features/settings/components/security-section.test.tsx @@ -0,0 +1,45 @@ +import SecuritySection from '@/features/settings/components/security-section' +import { expect } from 'chai' +import { screen, render } from '@testing-library/react' +import fetchMock from 'fetch-mock' + +describe('', function () { + beforeEach(function () { + window.metaAttributesCache = window.metaAttributesCache || new Map() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + fetchMock.reset() + }) + + it('shows Group SSO rows in security section', async function () { + window.metaAttributesCache.set('ol-memberOfSSOEnabledGroups', [ + { + groupId: 'abc123abc123', + linked: true, + }, + { + groupId: 'fff999fff999', + linked: false, + }, + ]) + render() + + expect(screen.getAllByText('Single Sign-On (SSO)').length).to.equal(2) + const link = screen.getByRole('link', { + name: /Set up SSO/i, + }) + expect(link).to.exist + expect(link.getAttribute('href')).to.equal( + '/subscription/fff999fff999/sso_enrollment' + ) + }) + + it('does not show the security section with no groups with SSO enabled', async function () { + window.metaAttributesCache.set('ol-memberOfSSOEnabledGroups', []) + render() + + expect(screen.queryByText('Security')).to.not.exist + }) +}) diff --git a/services/web/test/unit/src/User/UserPagesControllerTests.js b/services/web/test/unit/src/User/UserPagesControllerTests.js index e4c53a86bc..76d1aeed5b 100644 --- a/services/web/test/unit/src/User/UserPagesControllerTests.js +++ b/services/web/test/unit/src/User/UserPagesControllerTests.js @@ -1,15 +1,3 @@ -/* eslint-disable - max-len, - no-return-assign, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const SandboxedModule = require('sandboxed-module') const assert = require('assert') const path = require('path') @@ -19,6 +7,8 @@ const modulePath = path.join( '../../../../app/src/Features/User/UserPagesController' ) const { expect } = require('chai') +const MockResponse = require('../helpers/MockResponse') +const MockRequest = require('../helpers/MockRequest') describe('UserPagesController', function () { beforeEach(function () { @@ -80,6 +70,7 @@ describe('UserPagesController', function () { this.SubscriptionLocator = { promises: { getAdminEmail: sinon.stub().returns(this.adminEmail), + getMemberSubscriptions: sinon.stub().resolves(), }, } this.SplitTestHandler = { @@ -87,6 +78,13 @@ describe('UserPagesController', function () { getAssignment: sinon.stub().returns('default'), }, } + this.Modules = { + promises: { + hooks: { + fire: sinon.stub().resolves(), + }, + }, + } this.UserPagesController = SandboxedModule.require(modulePath, { requires: { '@overleaf/settings': this.settings, @@ -102,67 +100,71 @@ describe('UserPagesController', function () { this.PersonalAccessTokenManager, '../Authentication/SessionManager': this.SessionManager, '../SplitTests/SplitTestHandler': this.SplitTestHandler, + '../../infrastructure/Modules': this.Modules, request: (this.request = sinon.stub()), }, }) - this.req = { - query: {}, - session: { - user: this.user, - }, - } - return (this.res = {}) + this.req = new MockRequest() + this.req.session.user = this.user + this.res = new MockResponse() }) describe('registerPage', function () { it('should render the register page', function (done) { - this.res.render = page => { - page.should.equal('user/register') - return done() + this.res.callback = () => { + this.res.renderedTemplate.should.equal('user/register') + done() } - return this.UserPagesController.registerPage(this.req, this.res) + this.UserPagesController.registerPage(this.req, this.res, done) }) it('should set sharedProjectData', function (done) { this.req.query.project_name = 'myProject' this.req.query.user_first_name = 'user_first_name_here' - this.res.render = (page, opts) => { - opts.sharedProjectData.project_name.should.equal('myProject') - opts.sharedProjectData.user_first_name.should.equal( + this.res.callback = () => { + this.res.renderedVariables.sharedProjectData.project_name.should.equal( + 'myProject' + ) + this.res.renderedVariables.sharedProjectData.user_first_name.should.equal( 'user_first_name_here' ) - return done() + done() } - return this.UserPagesController.registerPage(this.req, this.res) + this.UserPagesController.registerPage(this.req, this.res, done) }) it('should set newTemplateData', function (done) { this.req.session.templateData = { templateName: 'templateName' } - this.res.render = (page, opts) => { - opts.newTemplateData.templateName.should.equal('templateName') - return done() + this.res.callback = () => { + this.res.renderedVariables.newTemplateData.templateName.should.equal( + 'templateName' + ) + done() } - return this.UserPagesController.registerPage(this.req, this.res) + this.UserPagesController.registerPage(this.req, this.res, done) }) it('should not set the newTemplateData if there is nothing in the session', function (done) { - this.res.render = (page, opts) => { - assert.equal(opts.newTemplateData.templateName, undefined) - return done() + this.res.callback = () => { + assert.equal( + this.res.renderedVariables.newTemplateData.templateName, + undefined + ) + done() } - return this.UserPagesController.registerPage(this.req, this.res) + this.UserPagesController.registerPage(this.req, this.res, done) }) }) describe('loginForm', function () { it('should render the login page', function (done) { - this.res.render = page => { - page.should.equal('user/login') - return done() + this.res.callback = () => { + this.res.renderedTemplate.should.equal('user/login') + done() } - return this.UserPagesController.loginPage(this.req, this.res) + this.UserPagesController.loginPage(this.req, this.res, done) }) describe('when an explicit redirect is set via query string', function () { @@ -171,63 +173,59 @@ describe('UserPagesController', function () { .stub() .returns(null) this.AuthenticationController.setRedirectInSession = sinon.stub() - return (this.req.query.redir = '/somewhere/in/particular') + this.req.query.redir = '/somewhere/in/particular' }) it('should set a redirect', function (done) { - this.res.render = page => { + this.res.callback = page => { this.AuthenticationController.setRedirectInSession.callCount.should.equal( 1 ) expect( this.AuthenticationController.setRedirectInSession.lastCall.args[1] ).to.equal(this.req.query.redir) - return done() + done() } - return this.UserPagesController.loginPage(this.req, this.res) + this.UserPagesController.loginPage(this.req, this.res, done) }) }) }) describe('sessionsPage', function () { beforeEach(function () { - return this.UserSessionsManager.getAllUserSessions.callsArgWith( - 2, - null, - [] - ) + this.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, []) }) it('should render user/sessions', function (done) { - this.res.render = function (page) { - page.should.equal('user/sessions') - return done() + this.res.callback = () => { + this.res.renderedTemplate.should.equal('user/sessions') + done() } - return this.UserPagesController.sessionsPage(this.req, this.res) + this.UserPagesController.sessionsPage(this.req, this.res, done) }) it('should include current session data in the view', function (done) { - this.res.render = (page, opts) => { - expect(opts.currentSession).to.deep.equal({ + this.res.callback = () => { + expect(this.res.renderedVariables.currentSession).to.deep.equal({ ip_address: '1.1.1.1', session_created: 'timestamp', }) - return done() + done() } - return this.UserPagesController.sessionsPage(this.req, this.res) + this.UserPagesController.sessionsPage(this.req, this.res, done) }) it('should have called getAllUserSessions', function (done) { - this.res.render = page => { + this.res.callback = page => { this.UserSessionsManager.getAllUserSessions.callCount.should.equal(1) - return done() + done() } - return this.UserPagesController.sessionsPage(this.req, this.res) + this.UserPagesController.sessionsPage(this.req, this.res, done) }) describe('when getAllUserSessions produces an error', function () { beforeEach(function () { - return this.UserSessionsManager.getAllUserSessions.callsArgWith( + this.UserSessionsManager.getAllUserSessions.callsArgWith( 2, new Error('woops') ) @@ -237,13 +235,9 @@ describe('UserPagesController', function () { this.next = err => { assert(err !== null) assert(err instanceof Error) - return done() + done() } - return this.UserPagesController.sessionsPage( - this.req, - this.res, - this.next - ) + this.UserPagesController.sessionsPage(this.req, this.res, this.next) }) }) }) @@ -255,24 +249,24 @@ describe('UserPagesController', function () { it('render page with subscribed status', function (done) { this.NewsletterManager.subscribed.yields(null, true) - this.res.render = function (page, data) { - page.should.equal('user/email-preferences') - data.title.should.equal('newsletter_info_title') - data.subscribed.should.equal(true) - return done() + this.res.callback = () => { + this.res.renderedTemplate.should.equal('user/email-preferences') + this.res.renderedVariables.title.should.equal('newsletter_info_title') + this.res.renderedVariables.subscribed.should.equal(true) + done() } - return this.UserPagesController.emailPreferencesPage(this.req, this.res) + this.UserPagesController.emailPreferencesPage(this.req, this.res, done) }) it('render page with unsubscribed status', function (done) { this.NewsletterManager.subscribed.yields(null, false) - this.res.render = function (page, data) { - page.should.equal('user/email-preferences') - data.title.should.equal('newsletter_info_title') - data.subscribed.should.equal(false) - return done() + this.res.callback = () => { + this.res.renderedTemplate.should.equal('user/email-preferences') + this.res.renderedVariables.title.should.equal('newsletter_info_title') + this.res.renderedVariables.subscribed.should.equal(false) + done() } - return this.UserPagesController.emailPreferencesPage(this.req, this.res) + this.UserPagesController.emailPreferencesPage(this.req, this.res, done) }) }) @@ -285,103 +279,180 @@ describe('UserPagesController', function () { }) it('should render user/settings', function (done) { - this.res.render = function (page) { - page.should.equal('user/settings') - return done() + this.res.callback = () => { + this.res.renderedTemplate.should.equal('user/settings') + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) it('should send user', function (done) { - this.res.render = (page, opts) => { - opts.user.id.should.equal(this.user._id) - opts.user.email.should.equal(this.user.email) - return done() + this.res.callback = () => { + this.res.renderedVariables.user.id.should.equal(this.user._id) + this.res.renderedVariables.user.email.should.equal(this.user.email) + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) it("should set 'shouldAllowEditingDetails' to true", function (done) { - this.res.render = (page, opts) => { - opts.shouldAllowEditingDetails.should.equal(true) - return done() + this.res.callback = () => { + this.res.renderedVariables.shouldAllowEditingDetails.should.equal(true) + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) it('should restructure thirdPartyIdentifiers data for template use', function (done) { const expectedResult = { google: 'testId', } - this.res.render = (page, opts) => { - expect(opts.thirdPartyIds).to.include(expectedResult) - return done() + this.res.callback = () => { + expect(this.res.renderedVariables.thirdPartyIds).to.include( + expectedResult + ) + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) it("should set and clear 'projectSyncSuccessMessage'", function (done) { this.req.session.projectSyncSuccessMessage = 'Some Sync Success' - this.res.render = (page, opts) => { - opts.projectSyncSuccessMessage.should.equal('Some Sync Success') + this.res.callback = () => { + this.res.renderedVariables.projectSyncSuccessMessage.should.equal( + 'Some Sync Success' + ) expect(this.req.session.projectSyncSuccessMessage).to.not.exist - return done() + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) it('should cast refProviders to booleans', function (done) { - this.res.render = function (page, opts) { - expect(opts.user.refProviders).to.deep.equal({ + this.res.callback = () => { + expect(this.res.renderedVariables.user.refProviders).to.deep.equal({ mendeley: true, zotero: true, }) - return done() + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) it('should send the correct managed user admin email', function (done) { - this.res.render = (page, opts) => { - expect(opts.currentManagedUserAdminEmail).to.equal(this.adminEmail) - return done() + this.res.callback = () => { + expect( + this.res.renderedVariables.currentManagedUserAdminEmail + ).to.equal(this.adminEmail) + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) + }) + + it('should send info for groups with SSO enabled', function (done) { + this.user.enrollment = { + sso: [ + { + groupId: 'abc123abc123', + primary: true, + linkedAt: new Date(), + }, + ], + } + const group1 = { + _id: 'abc123abc123', + teamName: 'Group SSO Rulz', + admin_id: { + email: 'admin.email@ssolove.com', + }, + } + const group2 = { + _id: 'def456def456', + admin_id: { + email: 'someone.else@noname.co.uk', + }, + } + const group3 = { + _id: 'fff999fff999', + admin_id: { + email: 'foo@bar.baz', + }, + } + this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([ + group1, + group2, + group3, + ]) + this.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', group1) + .resolves([true]) + this.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', group2) + .resolves([true]) + this.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', group3) + .resolves([false]) + + this.res.callback = () => { + expect( + this.res.renderedVariables.memberOfSSOEnabledGroups + ).to.deep.equal([ + { + groupId: 'abc123abc123', + groupName: 'Group SSO Rulz', + adminEmail: 'admin.email@ssolove.com', + linked: true, + }, + { + groupId: 'def456def456', + groupName: undefined, + adminEmail: 'someone.else@noname.co.uk', + linked: false, + }, + ]) + done() + } + + this.UserPagesController.settingsPage(this.req, this.res, done) }) describe('when ldap.updateUserDetailsOnLogin is true', function () { beforeEach(function () { - return (this.settings.ldap = { updateUserDetailsOnLogin: true }) + this.settings.ldap = { updateUserDetailsOnLogin: true } }) afterEach(function () { - return delete this.settings.ldap + delete this.settings.ldap }) it('should set "shouldAllowEditingDetails" to false', function (done) { - this.res.render = (page, opts) => { - opts.shouldAllowEditingDetails.should.equal(false) - return done() + this.res.callback = () => { + this.res.renderedVariables.shouldAllowEditingDetails.should.equal( + false + ) + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) }) describe('when saml.updateUserDetailsOnLogin is true', function () { beforeEach(function () { - return (this.settings.saml = { updateUserDetailsOnLogin: true }) + this.settings.saml = { updateUserDetailsOnLogin: true } }) afterEach(function () { - return delete this.settings.saml + delete this.settings.saml }) it('should set "shouldAllowEditingDetails" to false', function (done) { - this.res.render = (page, opts) => { - opts.shouldAllowEditingDetails.should.equal(false) - return done() + this.res.callback = () => { + this.res.renderedVariables.shouldAllowEditingDetails.should.equal( + false + ) + done() } - return this.UserPagesController.settingsPage(this.req, this.res) + this.UserPagesController.settingsPage(this.req, this.res, done) }) }) }) diff --git a/services/web/types/subscription/sso.ts b/services/web/types/subscription/sso.ts index c1743b4c5a..cf869ec741 100644 --- a/services/web/types/subscription/sso.ts +++ b/services/web/types/subscription/sso.ts @@ -14,3 +14,10 @@ export type SSOConfig = { validated?: boolean enabled?: boolean } + +export type GroupSSOLinkingStatus = { + groupId: string + linked?: boolean + groupName?: string + adminEmail: string +}