diff --git a/assets/jest.config.js b/assets/jest.config.js index 0442bfd103..636d692185 100644 --- a/assets/jest.config.js +++ b/assets/jest.config.js @@ -67,6 +67,8 @@ module.exports = { config: { checksServiceBaseUrl: '', suseManagerEnabled: true, + adminUsername: 'admin', + oidcEnabled: false, aTestVariable: 123, }, }, diff --git a/assets/js/lib/auth/config.js b/assets/js/lib/auth/config.js new file mode 100644 index 0000000000..58efbe0838 --- /dev/null +++ b/assets/js/lib/auth/config.js @@ -0,0 +1,5 @@ +import { getFromConfig } from '@lib/config'; + +const OIDC_ENABLED = getFromConfig('oidcEnabled') || false; + +export const isSingleSignOnEnabled = () => OIDC_ENABLED; diff --git a/assets/js/lib/auth/config.test.js b/assets/js/lib/auth/config.test.js new file mode 100644 index 0000000000..612fb31e4c --- /dev/null +++ b/assets/js/lib/auth/config.test.js @@ -0,0 +1,7 @@ +import { isSingleSignOnEnabled } from './config'; + +describe('auth config', () => { + it('should check if single sign on is enabled', () => { + expect(isSingleSignOnEnabled()).toBe(false); + }); +}); diff --git a/assets/js/lib/model/users.test.js b/assets/js/lib/model/users.test.js index 5c530e42cf..6fd5328dc5 100644 --- a/assets/js/lib/model/users.test.js +++ b/assets/js/lib/model/users.test.js @@ -4,10 +4,10 @@ import { isAdmin } from './users'; describe('users', () => { it('should check if a user is admin', () => { - const admin = adminUser.build(); + const admin = adminUser.build({ username: 'admin' }); expect(isAdmin(admin)).toBe(true); - const user = userFactory.build({ id: 2 }); + const user = userFactory.build({ username: 'other' }); expect(isAdmin(user)).toBe(false); }); }); diff --git a/assets/js/pages/Profile/ProfileForm.jsx b/assets/js/pages/Profile/ProfileForm.jsx index 0459f16787..de91eadc25 100644 --- a/assets/js/pages/Profile/ProfileForm.jsx +++ b/assets/js/pages/Profile/ProfileForm.jsx @@ -25,6 +25,7 @@ function ProfileForm({ disableForm, passwordModalOpen = false, totpBoxOpen = false, + singleSignOnEnabled = false, toggleTotpBox = noop, togglePasswordModal = noop, onSave = noop, @@ -94,6 +95,7 @@ function ProfileForm({ setFullName(value); setFullNameError(null); }} + disabled={singleSignOnEnabled} /> {fullNameErrorState && errorMessage(fullNameErrorState)} @@ -108,6 +110,7 @@ function ProfileForm({ setEmailAddress(value); setEmailAddressError(null); }} + disabled={singleSignOnEnabled} /> {emailAddressErrorState && errorMessage(emailAddressErrorState)} @@ -115,54 +118,58 @@ function ProfileForm({
- -
- -
- - -
-
- - {totpBoxOpen && ( - toggleTotpBox(false)} - aria-hidden="true" + {!singleSignOnEnabled && ( + <> + +
+
+ Change Password + +
- {totpBoxOpen && ( +
- +
+ + {totpBoxOpen && ( + toggleTotpBox(false)} + aria-hidden="true" + > + Cancel + + )} +
+ + {totpBoxOpen && ( +
+ +
+ )}
- )} -
+ + )}
@@ -174,15 +181,17 @@ function ProfileForm({ />
-
- -
+ {!singleSignOnEnabled && ( +
+ +
+ )} ( @@ -129,3 +133,10 @@ export const WithErrors = { ], }, }; + +export const SingleSignOnEnabled = { + args: { + ...Default.args, + singleSignOnEnabled: true, + }, +}; diff --git a/assets/js/pages/Profile/ProfileForm.test.jsx b/assets/js/pages/Profile/ProfileForm.test.jsx index 73b71d4fc5..23b088dcd8 100644 --- a/assets/js/pages/Profile/ProfileForm.test.jsx +++ b/assets/js/pages/Profile/ProfileForm.test.jsx @@ -305,4 +305,59 @@ describe('ProfileForm', () => { ); expect(screen.getByText('Error validating totp code')).toBeVisible(); }); + + describe('Single sign on', () => { + it('should disable fullname, email and username fields', () => { + const { username, fullname, email, abilities } = profileFactory.build(); + + render( + + ); + + expect(screen.getByLabelText('fullname')).toBeDisabled(); + expect(screen.getByLabelText('email')).toBeDisabled(); + expect(screen.getByLabelText('username')).toBeDisabled(); + expect(screen.getByLabelText('permissions')).toBeDisabled(); + }); + + it('should remove password and totp fields', () => { + const { username, fullname, email, abilities } = profileFactory.build(); + + render( + + ); + + expect(screen.queryByText('Password')).not.toBeInTheDocument(); + expect(screen.queryByText('Authenticator App')).not.toBeInTheDocument(); + expect(screen.getByText('Permissions')).toBeVisible(); + }); + + it('should remove save button', () => { + const { username, fullname, email, abilities } = profileFactory.build(); + + render( + + ); + + expect(screen.queryByText('Save')).not.toBeInTheDocument(); + }); + }); }); diff --git a/assets/js/pages/Profile/ProfilePage.jsx b/assets/js/pages/Profile/ProfilePage.jsx index 945e07d29d..3373b457e4 100644 --- a/assets/js/pages/Profile/ProfilePage.jsx +++ b/assets/js/pages/Profile/ProfilePage.jsx @@ -3,6 +3,7 @@ import { toast } from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import PageHeader from '@common/PageHeader'; import { isAdmin } from '@lib/model/users'; +import { isSingleSignOnEnabled } from '@lib/auth/config'; import ProfileForm from '@pages/Profile/ProfileForm'; import { getUserProfile, @@ -162,6 +163,7 @@ function ProfilePage() { toggleTotpBox={setTotpBoxOpen} loading={loading || saving} disableForm={isDefaultAdmin} + singleSignOnEnabled={isSingleSignOnEnabled()} onSave={updateProfile} onEnableTotp={totpInitiateEnrolling} onVerifyTotp={verifyTotpEnrollment} diff --git a/assets/js/pages/Users/CreateUserPage.jsx b/assets/js/pages/Users/CreateUserPage.jsx index af38ac1884..06aad73533 100644 --- a/assets/js/pages/Users/CreateUserPage.jsx +++ b/assets/js/pages/Users/CreateUserPage.jsx @@ -4,7 +4,9 @@ import { toast } from 'react-hot-toast'; import BackButton from '@common/BackButton'; import PageHeader from '@common/PageHeader'; +import NotFound from '@pages/NotFound'; +import { isSingleSignOnEnabled } from '@lib/auth/config'; import { listAbilities } from '@lib/api/abilities'; import { createUser } from '@lib/api/users'; @@ -55,6 +57,10 @@ function CreateUserPage() { navigate('/users'); }; + if (isSingleSignOnEnabled()) { + return ; + } + useEffect(() => { fetchAbilities(setAbilities); }, []); diff --git a/assets/js/pages/Users/CreateUserPage.test.jsx b/assets/js/pages/Users/CreateUserPage.test.jsx index 3ac8ef373e..1dc2c5c2c3 100644 --- a/assets/js/pages/Users/CreateUserPage.test.jsx +++ b/assets/js/pages/Users/CreateUserPage.test.jsx @@ -13,7 +13,7 @@ import { faker } from '@faker-js/faker'; import * as router from 'react-router'; import { networkClient } from '@lib/network'; - +import * as authConfig from '@lib/auth/config'; import { abilityFactory, userFactory } from '@lib/test-utils/factories/users'; import CreateUserPage from './CreateUserPage'; @@ -144,4 +144,16 @@ describe('CreateUserPage', () => { await user.click(screen.getByRole('button', { name: 'Create' })); expect(toast.error).toHaveBeenCalledWith(toastMessage); }); + + describe('Single sign on', () => { + it('should redirect to not found page', async () => { + jest.spyOn(authConfig, 'isSingleSignOnEnabled').mockReturnValue(true); + + render(); + + expect( + screen.getByText('the page is in another castle', { exact: false }) + ).toBeVisible(); + }); + }); }); diff --git a/assets/js/pages/Users/EditUserPage.jsx b/assets/js/pages/Users/EditUserPage.jsx index 503665b7ed..f5bb732213 100644 --- a/assets/js/pages/Users/EditUserPage.jsx +++ b/assets/js/pages/Users/EditUserPage.jsx @@ -7,6 +7,8 @@ import Banner from '@common/Banners/Banner'; import PageHeader from '@common/PageHeader'; import { isAdmin } from '@lib/model/users'; +import { isSingleSignOnEnabled } from '@lib/auth/config'; + import { editUser, getUser } from '@lib/api/users'; import { fetchAbilities } from './CreateUserPage'; @@ -123,6 +125,7 @@ function EditUserPage() { onSave={onEditUser} onCancel={onCancel} editing + singleSignOnEnabled={isSingleSignOnEnabled()} /> ); diff --git a/assets/js/pages/Users/UserForm.jsx b/assets/js/pages/Users/UserForm.jsx index 4d34cd96d5..50355a9e8a 100644 --- a/assets/js/pages/Users/UserForm.jsx +++ b/assets/js/pages/Users/UserForm.jsx @@ -38,6 +38,7 @@ function UserForm({ saveEnabled = true, saveText = 'Create', editing = false, + singleSignOnEnabled = false, onSave = noop, onCancel = noop, }) { @@ -95,23 +96,32 @@ function UserForm({ return error; }; + const buildUserPayload = () => ({ + fullname: fullNameState, + email: emailAddressState, + enabled: statusState === USER_ENABLED, + ...(!editing && { username: usernameState }), + ...(passwordState && { password: passwordState }), + ...(confirmPasswordState && { + password_confirmation: confirmPasswordState, + }), + abilities: abilities.filter(({ id }) => selectedAbilities.includes(id)), + ...(totpEnabledAt && !totpState && { totp_disabled: true }), + }); + + const buildSSOUserPayload = () => ({ + enabled: statusState === USER_ENABLED, + abilities: abilities.filter(({ id }) => selectedAbilities.includes(id)), + }); + const onSaveClicked = () => { if (validateRequired()) { return; } - const user = { - fullname: fullNameState, - email: emailAddressState, - enabled: statusState === USER_ENABLED, - ...(!editing && { username: usernameState }), - ...(passwordState && { password: passwordState }), - ...(confirmPasswordState && { - password_confirmation: confirmPasswordState, - }), - abilities: abilities.filter(({ id }) => selectedAbilities.includes(id)), - ...(totpEnabledAt && !totpState && { totp_disabled: true }), - }; + const user = singleSignOnEnabled + ? buildSSOUserPayload() + : buildUserPayload(); onSave(user); }; @@ -139,6 +149,7 @@ function UserForm({ setFullName(value); setFullNameError(null); }} + disabled={singleSignOnEnabled} /> {fullNameErrorState && errorMessage(fullNameErrorState)} @@ -155,6 +166,7 @@ function UserForm({ setEmailAddress(value); setEmailAddressError(null); }} + disabled={singleSignOnEnabled} /> {emailAddressErrorState && errorMessage(emailAddressErrorState)} @@ -171,56 +183,64 @@ function UserForm({ setUsername(value); setUsernameError(null); }} - disabled={editing} + disabled={editing || singleSignOnEnabled} /> {usernameErrorState && errorMessage(usernameErrorState)} - -
- { - setPassword(value); - setPasswordError(null); - }} - /> - {passwordErrorState && errorMessage(passwordErrorState)} -
- -
- { - setConfirmPassword(value); - setConfirmPasswordError(null); - }} - /> - {confirmPasswordErrorState && - errorMessage(confirmPasswordErrorState)} -
-
- -
+ {!singleSignOnEnabled && ( + <> + +
+ { + setPassword(value); + setPasswordError(null); + }} + /> + {passwordErrorState && errorMessage(passwordErrorState)} +
+ +
+ { + setConfirmPassword(value); + setConfirmPasswordError(null); + }} + /> + {confirmPasswordErrorState && + errorMessage(confirmPasswordErrorState)} +
+
+ +
+ + )}
- {editing && ( + {editing && !singleSignOnEnabled && ( <>