Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/sso-bypass-allowlist-by-role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@clerk/localizations': minor
'@clerk/clerk-js': minor
'@clerk/shared': minor
'@clerk/ui': minor
---

The "Add members" card on the SSO allow list page of `<OrganizationProfile />` now offers two ways to add people: by email address, or every member with a given role at once. Members whose email address is not served by one of the organization's enterprise connections are skipped. When nothing could be added the card stays open and says why, and when some were added it moves to a success step that reports how many were skipped.

For custom flows, `organization.ssoBypassAllowlist` gains `addUsers({ userIds })`, which calls the new bulk endpoint in batches of 100 and returns the added entries together with the users that could not be added and why.

Inputs marked to be ignored by password managers now also carry the Bitwarden, LastPass and Dashlane opt-out attributes, so those extensions stop offering to fill fields such as the allow list email address.

The member picker that the "Add member" card shipped with in 4.18.0 is gone, and so are its localization keys under `organizationProfile.securityPage.ssoBypassPage.addForm`: `memberLabel`, `memberPlaceholder`, `changeButton` and `noResults`. The feature was never enabled on any instance, so no application depends on them.

New customization handles: the `organizationProfileSecuritySsoBypassEmailInput`, `organizationProfileSecuritySsoBypassRoleWarning`, `organizationProfileSecuritySsoBypassFailure` and `organizationProfileSecuritySsoBypassBulkResult` appearance elements.
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"files": [
{ "path": "./dist/clerk.js", "maxSize": "554KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "81KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "122.5KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "320KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "124KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "322KB" },
{ "path": "./dist/clerk.native.js", "maxSize": "80KB" },
{ "path": "./dist/vendors*.js", "maxSize": "7KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "36KB" },
Expand Down
24 changes: 24 additions & 0 deletions packages/clerk-js/src/core/resources/SSOBypassAllowlist.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type {
AddSSOBypassAllowlistUserParams,
AddSSOBypassAllowlistUsersParams,
DeletedObjectJSON,
DeletedObjectResource,
SSOBypassAllowlistBulkCreateJSON,
SSOBypassAllowlistBulkCreateResult,
SSOBypassAllowlistResource,
SSOBypassAllowlistUserJSON,
SSOBypassAllowlistUserResource,
Expand All @@ -11,6 +14,8 @@ import { BaseResource } from './Base';
import { DeletedObject } from './DeletedObject';
import { SSOBypassAllowlistUser } from './SSOBypassAllowlistUser';

const BULK_SIZE = 100;

export class SSOBypassAllowlist implements SSOBypassAllowlistResource {
declare private readonly organization: { id: string };

Expand Down Expand Up @@ -45,6 +50,25 @@ export class SSOBypassAllowlist implements SSOBypassAllowlistResource {
return new SSOBypassAllowlistUser(json);
};

addUsers = async (params: AddSSOBypassAllowlistUsersParams): Promise<SSOBypassAllowlistBulkCreateResult> => {
const result: SSOBypassAllowlistBulkCreateResult = { data: [], errors: [] };

for (let start = 0; start < params.userIds.length; start += BULK_SIZE) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless I'm missing it (totally possible) I don't think we partition inputs into batches like this for other endpoints (such as the invite members flow). Is there something specific about this endpoint that requires the batching support built in? Do we think it's likely that customers will be attempting to add more than 100 members to the allowlist at a time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the problem here is that invitations and “adding all members of a given role” are a bit different.

When an admin invites members, they probably aren't typing or pasting 100+ members at once. it is more like a series of individual invites, which is different from being able to select a role with 300 members and add them all at once. does that make sense?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's fine for this PR then, but I think we should look at updating the API to support adding by role instead of expecting the client to handle it. What if you have a massive organization of 10k members with the same role and you add them? Extreme example but I'd rather tell the API "add all members with to the allowlist" than have the client individually batch those. but again, fine for this PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked to Stephen about it a few days ago. It is a follow-up, and we agreed that the current approach isn't ideal. At least we are using the same primitives, so any future work will be additive and won’t break existing customers

const json = (
await BaseResource._fetch({
path: `${this.path}/bulk`,
method: 'POST',
body: { user_id: params.userIds.slice(start, start + BULK_SIZE) } as any,
})
)?.response as unknown as SSOBypassAllowlistBulkCreateJSON;

result.data.push(...(json?.data ?? []).map(entry => new SSOBypassAllowlistUser(entry)));
result.errors.push(...(json?.errors ?? []).map(error => ({ userId: error.user_id, code: error.code })));
}

return result;
};

removeUser = async (userId: string): Promise<DeletedObjectResource> => {
const json = (
await BaseResource._fetch<DeletedObjectJSON>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,49 @@ describe('Organization', () => {
expect(entry.userId).toBe('user_1');
});

it('adds users in chunks of 100 and merges the partial results', async () => {
const userIds = Array.from({ length: 150 }, (_, i) => `user_${i}`);
const fetchMock = vi
.fn()
.mockResolvedValueOnce({
response: { data: [entryJSON], errors: [{ user_id: 'user_5', code: 'sso_bypass_domain_not_served' }] },
})
.mockResolvedValueOnce({
response: { data: [{ ...entryJSON, user_id: 'user_120' }], errors: [] },
});
// @ts-ignore
BaseResource._fetch = fetchMock;

const organization = createOrganization();
const result = await organization.ssoBypassAllowlist.addUsers({ userIds });

expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock).toHaveBeenNthCalledWith(1, {
method: 'POST',
path: `${ALLOWLIST_PATH}/bulk`,
body: { user_id: userIds.slice(0, 100) },
});
expect(fetchMock).toHaveBeenNthCalledWith(2, {
method: 'POST',
path: `${ALLOWLIST_PATH}/bulk`,
body: { user_id: userIds.slice(100) },
});
expect(result.data.map(entry => entry.userId)).toEqual(['user_1', 'user_120']);
expect(result.errors).toEqual([{ userId: 'user_5', code: 'sso_bypass_domain_not_served' }]);
});

it('sends nothing for an empty batch', async () => {
// @ts-ignore
BaseResource._fetch = vi.fn();

const organization = createOrganization();
const result = await organization.ssoBypassAllowlist.addUsers({ userIds: [] });

// @ts-ignore
expect(BaseResource._fetch).not.toHaveBeenCalled();
expect(result).toEqual({ data: [], errors: [] });
});

it('removes a user by id', async () => {
// @ts-ignore
BaseResource._fetch = vi
Expand Down
25 changes: 21 additions & 4 deletions packages/localizations/src/ar-SA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1379,14 +1379,30 @@ export const arSA: LocalizationResource = {
action__add: undefined,
action__search: undefined,
addForm: {
changeButton: undefined,
memberLabel: undefined,
memberPlaceholder: undefined,
noResults: undefined,
emailPlaceholder: undefined,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user previously customized these values in their application, how are we thinking about the fact this is a breaking change for them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point! I thought I could remove the keys since the feature isn’t being used in production yet, but that is not how things work in the SDK, right?

I kept the existing keys and marked them as deprecated. does that work?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah if the feature wasn't turned on from the backend it's fine to drop these. I thought since it was merged and released in an SDK version that the feature was live.

error__allAlreadyAdded: undefined,
error__alreadyAdded: undefined,
error__memberNotFound: undefined,
modeLabel: undefined,
mode__email: undefined,
mode__role: undefined,
roleOption: undefined,
roleWarning: undefined,
Comment thread
mauricioabreu marked this conversation as resolved.
submitButton: undefined,
subtitle: undefined,
title: undefined,
},
bulkResult: {
added: undefined,
addedMember: undefined,
added__one: undefined,
domainNotServed: undefined,
domainNotServed__one: undefined,
notMember: undefined,
notMember__one: undefined,
unknown: undefined,
unknown__one: undefined,
},
table: {
emptyState: undefined,
emptyState__search: undefined,
Expand Down Expand Up @@ -2049,6 +2065,7 @@ export const arSA: LocalizationResource = {
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
session_exists: 'لقد قمت بتسجيل الدخول بالفعل',
sso_bypass_domain_not_served: undefined,
ticket_expired_code: undefined,
ticket_invalid_code: undefined,
web3_missing_identifier: undefined,
Expand Down
25 changes: 21 additions & 4 deletions packages/localizations/src/be-BY.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1382,14 +1382,30 @@ export const beBY: LocalizationResource = {
action__add: undefined,
action__search: undefined,
addForm: {
changeButton: undefined,
memberLabel: undefined,
memberPlaceholder: undefined,
noResults: undefined,
emailPlaceholder: undefined,
error__allAlreadyAdded: undefined,
error__alreadyAdded: undefined,
error__memberNotFound: undefined,
modeLabel: undefined,
mode__email: undefined,
mode__role: undefined,
roleOption: undefined,
roleWarning: undefined,
submitButton: undefined,
subtitle: undefined,
title: undefined,
},
bulkResult: {
added: undefined,
addedMember: undefined,
added__one: undefined,
domainNotServed: undefined,
domainNotServed__one: undefined,
notMember: undefined,
notMember__one: undefined,
unknown: undefined,
unknown__one: undefined,
},
table: {
emptyState: undefined,
emptyState__search: undefined,
Expand Down Expand Up @@ -2060,6 +2076,7 @@ export const beBY: LocalizationResource = {
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
session_exists: 'Вы ўжо ўвайшлі.',
sso_bypass_domain_not_served: undefined,
ticket_expired_code: undefined,
ticket_invalid_code: undefined,
web3_missing_identifier: undefined,
Expand Down
25 changes: 21 additions & 4 deletions packages/localizations/src/bg-BG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1382,14 +1382,30 @@ export const bgBG: LocalizationResource = {
action__add: undefined,
action__search: undefined,
addForm: {
changeButton: undefined,
memberLabel: undefined,
memberPlaceholder: undefined,
noResults: undefined,
emailPlaceholder: undefined,
error__allAlreadyAdded: undefined,
error__alreadyAdded: undefined,
error__memberNotFound: undefined,
modeLabel: undefined,
mode__email: undefined,
mode__role: undefined,
roleOption: undefined,
roleWarning: undefined,
submitButton: undefined,
subtitle: undefined,
title: undefined,
},
bulkResult: {
added: undefined,
addedMember: undefined,
added__one: undefined,
domainNotServed: undefined,
domainNotServed__one: undefined,
notMember: undefined,
notMember__one: undefined,
unknown: undefined,
unknown__one: undefined,
},
table: {
emptyState: undefined,
emptyState__search: undefined,
Expand Down Expand Up @@ -2053,6 +2069,7 @@ export const bgBG: LocalizationResource = {
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
session_exists: 'Вече сте влезнали.',
sso_bypass_domain_not_served: undefined,
ticket_expired_code: undefined,
ticket_invalid_code: undefined,
web3_missing_identifier: undefined,
Expand Down
25 changes: 21 additions & 4 deletions packages/localizations/src/bn-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,14 +1389,30 @@ export const bnIN: LocalizationResource = {
action__add: undefined,
action__search: undefined,
addForm: {
changeButton: undefined,
memberLabel: undefined,
memberPlaceholder: undefined,
noResults: undefined,
emailPlaceholder: undefined,
error__allAlreadyAdded: undefined,
error__alreadyAdded: undefined,
error__memberNotFound: undefined,
modeLabel: undefined,
mode__email: undefined,
mode__role: undefined,
roleOption: undefined,
roleWarning: undefined,
submitButton: undefined,
subtitle: undefined,
title: undefined,
},
bulkResult: {
added: undefined,
addedMember: undefined,
added__one: undefined,
domainNotServed: undefined,
domainNotServed__one: undefined,
notMember: undefined,
notMember__one: undefined,
unknown: undefined,
unknown__one: undefined,
},
table: {
emptyState: undefined,
emptyState__search: undefined,
Expand Down Expand Up @@ -2078,6 +2094,7 @@ export const bnIN: LocalizationResource = {
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
session_exists: undefined,
sso_bypass_domain_not_served: undefined,
ticket_expired_code: undefined,
ticket_invalid_code: undefined,
web3_missing_identifier: 'একটি Web3 ওয়ালেট এক্সটেনশন পাওয়া যায়নি। চালিয়ে যেতে দয়া করে একটি ইনস্টল করুন।',
Expand Down
25 changes: 21 additions & 4 deletions packages/localizations/src/ca-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,14 +1389,30 @@ export const caES: LocalizationResource = {
action__add: undefined,
action__search: undefined,
addForm: {
changeButton: undefined,
memberLabel: undefined,
memberPlaceholder: undefined,
noResults: undefined,
emailPlaceholder: undefined,
error__allAlreadyAdded: undefined,
error__alreadyAdded: undefined,
error__memberNotFound: undefined,
modeLabel: undefined,
mode__email: undefined,
mode__role: undefined,
roleOption: undefined,
roleWarning: undefined,
submitButton: undefined,
subtitle: undefined,
title: undefined,
},
bulkResult: {
added: undefined,
addedMember: undefined,
added__one: undefined,
domainNotServed: undefined,
domainNotServed__one: undefined,
notMember: undefined,
notMember__one: undefined,
unknown: undefined,
unknown__one: undefined,
},
table: {
emptyState: undefined,
emptyState__search: undefined,
Expand Down Expand Up @@ -2066,6 +2082,7 @@ export const caES: LocalizationResource = {
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
session_exists: 'Ja estàs connectat.',
sso_bypass_domain_not_served: undefined,
ticket_expired_code: undefined,
ticket_invalid_code: undefined,
web3_missing_identifier: undefined,
Expand Down
25 changes: 21 additions & 4 deletions packages/localizations/src/cs-CZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1387,14 +1387,30 @@ export const csCZ: LocalizationResource = {
action__add: undefined,
action__search: undefined,
addForm: {
changeButton: undefined,
memberLabel: undefined,
memberPlaceholder: undefined,
noResults: undefined,
emailPlaceholder: undefined,
error__allAlreadyAdded: undefined,
error__alreadyAdded: undefined,
error__memberNotFound: undefined,
modeLabel: undefined,
mode__email: undefined,
mode__role: undefined,
roleOption: undefined,
roleWarning: undefined,
submitButton: undefined,
subtitle: undefined,
title: undefined,
},
bulkResult: {
added: undefined,
addedMember: undefined,
added__one: undefined,
domainNotServed: undefined,
domainNotServed__one: undefined,
notMember: undefined,
notMember__one: undefined,
unknown: undefined,
unknown__one: undefined,
},
table: {
emptyState: undefined,
emptyState__search: undefined,
Expand Down Expand Up @@ -2065,6 +2081,7 @@ export const csCZ: LocalizationResource = {
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
session_exists: 'Jste již přihlášen.',
sso_bypass_domain_not_served: undefined,
ticket_expired_code: undefined,
ticket_invalid_code: undefined,
web3_missing_identifier: 'Rozšíření peněženky Web3 nebylo nalezeno. Pro pokračování prosím nainstalujte jednu.',
Expand Down
Loading
Loading