diff --git a/.changeset/cyan-garlics-act.md b/.changeset/cyan-garlics-act.md new file mode 100644 index 0000000000..220f791df5 --- /dev/null +++ b/.changeset/cyan-garlics-act.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Password, first name & last name fields will be disabled if there are active SAML accounts. diff --git a/packages/clerk-js/src/core/resources/SamlAccount.ts b/packages/clerk-js/src/core/resources/SamlAccount.ts index 4488e071f1..41e8be0f02 100644 --- a/packages/clerk-js/src/core/resources/SamlAccount.ts +++ b/packages/clerk-js/src/core/resources/SamlAccount.ts @@ -9,6 +9,8 @@ import { Verification } from './Verification'; export class SamlAccount extends BaseResource implements SamlAccountResource { id!: string; provider: SamlIdpSlug = 'saml_custom'; + providerUserId: string | null = null; + active = false; emailAddress = ''; firstName = ''; lastName = ''; @@ -28,6 +30,8 @@ export class SamlAccount extends BaseResource implements SamlAccountResource { this.id = data.id; this.provider = data.provider; + this.providerUserId = data.provider_user_id; + this.active = data.active; this.emailAddress = data.email_address; this.firstName = data.first_name; this.lastName = data.last_name; diff --git a/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx index 38185b7a43..2bcb3edae1 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PasswordPage.tsx @@ -44,7 +44,7 @@ export const PasswordPage = withCardStateProvider(() => { const wizard = useWizard(); const { navigateToFlowStart } = useNavigateToFlowStart(); - const canEditPassword = user.samlAccounts.length == 0; + const passwordEditDisabled = user.samlAccounts.some(sa => sa.active); // Ensure that messages will not use the updated state of User after a password has been set or changed const successPagePropsRef = useRef[0]>({ @@ -127,7 +127,7 @@ export const PasswordPage = withCardStateProvider(() => { headerTitle={title} Breadcrumbs={UserProfileBreadcrumbs} > - {!canEditPassword && } + {passwordEditDisabled && } { minLength={6} required autoFocus - isDisabled={!canEditPassword} + isDisabled={passwordEditDisabled} /> )} @@ -158,7 +158,7 @@ export const PasswordPage = withCardStateProvider(() => { minLength={6} required autoFocus={!user.passwordEnabled} - isDisabled={!canEditPassword} + isDisabled={passwordEditDisabled} /> @@ -168,18 +168,16 @@ export const PasswordPage = withCardStateProvider(() => { displayConfirmPasswordFeedback(e.target.value); return confirmField.props.onChange(e); }} - isDisabled={!canEditPassword} + isDisabled={passwordEditDisabled} /> - {canEditPassword ? ( - - ) : ( + {passwordEditDisabled ? ( { onClick={navigateToFlowStart} /> + ) : ( + )} diff --git a/packages/clerk-js/src/ui/components/UserProfile/ProfilePage.tsx b/packages/clerk-js/src/ui/components/UserProfile/ProfilePage.tsx index 00a40e9bce..744040bd84 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ProfilePage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ProfilePage.tsx @@ -49,7 +49,7 @@ export const ProfilePage = withCardStateProvider(() => { const requiredFieldsFilled = hasRequiredFields && !!lastNameField.value && !!firstNameField.value && optionalFieldsChanged; - const canEditName = user.samlAccounts.length == 0; + const nameEditDisabled = user.samlAccounts.some(sa => sa.active); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -93,7 +93,7 @@ export const ProfilePage = withCardStateProvider(() => { headerTitle={title} Breadcrumbs={UserProfileBreadcrumbs} > - {!canEditName && } + {nameEditDisabled && } { autoFocus {...firstNameField.props} required={first_name.required} - isDisabled={!canEditName} + isDisabled={nameEditDisabled} /> )} @@ -116,7 +116,7 @@ export const ProfilePage = withCardStateProvider(() => { )} diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx index fd1c0953f8..0f11439f82 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/PasswordPage.test.tsx @@ -52,7 +52,7 @@ describe('PasswordPage', () => { }); describe('with SAML', () => { - it('prevents adding a password if user has enterprise connections', async () => { + it('prevents adding a password if user has active enterprise connections', async () => { const emailAddress = 'george@jungle.com'; const config = createFixtures.config(f => { @@ -64,6 +64,7 @@ describe('PasswordPage', () => { { id: 'samlacc_foo', provider: 'saml_okta', + active: true, email_address: emailAddress, }, ], @@ -78,12 +79,48 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled(); - screen.getByText( - 'Your password can currently not be edited because you can sign in only via the enterprise connection.', - ); + expect( + screen.getByText( + 'Your password can currently not be edited because you can sign in only via the enterprise connection.', + ), + ).toBeInTheDocument(); }); - it('prevents changing a password if user has enterprise connections', async () => { + it('does not prevent adding a password if user has no active enterprise connections', async () => { + const emailAddress = 'george@jungle.com'; + + const config = createFixtures.config(f => { + f.withEmailAddress(); + f.withSaml(); + f.withUser({ + email_addresses: [emailAddress], + saml_accounts: [ + { + id: 'samlacc_foo', + provider: 'saml_okta', + active: false, + email_address: emailAddress, + }, + ], + }); + }); + + const { wrapper } = await createFixtures(config); + + render(, { wrapper }); + + expect(screen.getByLabelText(/new password/i)).not.toBeDisabled(); + expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); + expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled(); + + expect( + screen.queryByText( + 'Your password can currently not be edited because you can sign in only via the enterprise connection.', + ), + ).not.toBeInTheDocument(); + }); + + it('prevents changing a password if user has active enterprise connections', async () => { const emailAddress = 'george@jungle.com'; const config = createFixtures.config(f => { @@ -96,6 +133,7 @@ describe('PasswordPage', () => { { id: 'samlacc_foo', provider: 'saml_okta', + active: true, email_address: emailAddress, }, ], @@ -111,9 +149,47 @@ describe('PasswordPage', () => { expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled(); - screen.getByText( - 'Your password can currently not be edited because you can sign in only via the enterprise connection.', - ); + expect( + screen.getByText( + 'Your password can currently not be edited because you can sign in only via the enterprise connection.', + ), + ).toBeInTheDocument(); + }); + + it('does not prevent changing a password if user has no active enterprise connections', async () => { + const emailAddress = 'george@jungle.com'; + + const config = createFixtures.config(f => { + f.withEmailAddress(); + f.withSaml(); + f.withUser({ + password_enabled: true, + email_addresses: [emailAddress], + saml_accounts: [ + { + id: 'samlacc_foo', + provider: 'saml_okta', + active: false, + email_address: emailAddress, + }, + ], + }); + }); + + const { wrapper } = await createFixtures(config); + + render(, { wrapper }); + + expect(screen.getByLabelText(/current password/i)).not.toBeDisabled(); + expect(screen.getByLabelText(/new password/i)).not.toBeDisabled(); + expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); + expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled(); + + expect( + screen.queryByText( + 'Your password can currently not be edited because you can sign in only via the enterprise connection.', + ), + ).not.toBeInTheDocument(); }); }); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/ProfilePage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/ProfilePage.test.tsx index 17ea809af0..a39975a284 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/ProfilePage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/ProfilePage.test.tsx @@ -43,7 +43,7 @@ describe('ProfilePage', () => { }); describe('with SAML', () => { - it('disables the first & last name inputs if user has enterprise connections', async () => { + it('disables the first & last name inputs if user has active enterprise connections', async () => { const emailAddress = 'george@jungle.com'; const firstName = 'George'; const lastName = 'Clerk'; @@ -60,6 +60,7 @@ describe('ProfilePage', () => { { id: 'samlacc_foo', provider: 'saml_okta', + active: true, email_address: emailAddress, }, ], @@ -72,8 +73,47 @@ describe('ProfilePage', () => { expect(screen.getByRole('textbox', { name: 'First name' })).toBeDisabled(); expect(screen.getByRole('textbox', { name: 'Last name' })).toBeDisabled(); + screen.getByText('Your profile information has been provided by the enterprise connection and cannot be edited.'); }); + + it('does not disable the first & last name inputs if user has no active enterprise connections', async () => { + const emailAddress = 'george@jungle.com'; + const firstName = 'George'; + const lastName = 'Clerk'; + + const config = createFixtures.config(f => { + f.withEmailAddress(); + f.withSaml(); + f.withName(); + f.withUser({ + first_name: firstName, + last_name: lastName, + email_addresses: [emailAddress], + saml_accounts: [ + { + id: 'samlacc_foo', + provider: 'saml_okta', + active: false, + email_address: emailAddress, + }, + ], + }); + }); + + const { wrapper } = await createFixtures(config); + + render(, { wrapper }); + + expect(screen.getByRole('textbox', { name: 'First name' })).not.toBeDisabled(); + expect(screen.getByRole('textbox', { name: 'Last name' })).not.toBeDisabled(); + + expect( + screen.queryByText( + 'Your profile information has been provided by the enterprise connection and cannot be edited.', + ), + ).not.toBeInTheDocument(); + }); }); describe('Profile image', () => { diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index ebfc119e59..498fe69736 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -168,6 +168,8 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { export interface SamlAccountJSON extends ClerkResourceJSON { object: 'saml_account'; provider: SamlIdpSlug; + provider_user_id: string | null; + active: boolean; email_address: string; first_name: string; last_name: string; diff --git a/packages/types/src/samlAccount.ts b/packages/types/src/samlAccount.ts index aadf15e15a..7927db8faa 100644 --- a/packages/types/src/samlAccount.ts +++ b/packages/types/src/samlAccount.ts @@ -7,6 +7,8 @@ import type { VerificationResource } from './verification'; */ export interface SamlAccountResource extends ClerkResource { provider: SamlIdpSlug; + providerUserId: string | null; + active: boolean; emailAddress: string; firstName: string; lastName: string;