Skip to content

Commit d4e62c2

Browse files
committed
fix(sso): support registered provider IDs in settings
1 parent b36efc0 commit d4e62c2

5 files changed

Lines changed: 64 additions & 23 deletions

File tree

apps/sim/app/api/auth/sso/providers/[providerId]/route.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ describe('DELETE /api/auth/sso/providers/[providerId]', () => {
8080
expect(allowed.status).toBe(200)
8181
})
8282

83+
it.each([129, 256])('deletes a provider with a %i-character ID', async (length) => {
84+
const providerId = 'a'.repeat(length)
85+
queueTableRows(schemaMock.ssoProvider, [
86+
{ id: 'row-1', organizationId: 'org1', userId: 'u1', domain: 'acme.com' },
87+
])
88+
queueTableRows(schemaMock.member, [{ role: 'owner' }])
89+
90+
const res = await DELETE(request(), { params: Promise.resolve({ providerId }) })
91+
92+
expect(res.status).toBe(200)
93+
await expect(res.json()).resolves.toEqual({ success: true, providerId })
94+
expect(dbChainMockFns.delete).toHaveBeenCalledWith(schemaMock.ssoProvider)
95+
})
96+
8397
it('answers 404 when the row vanished between the check and the delete', async () => {
8498
queueTableRows(schemaMock.ssoProvider, [
8599
{ id: 'row-1', organizationId: 'org1', userId: 'u1', domain: 'acme.com' },

apps/sim/ee/sso/components/sso-settings.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,10 @@ function startEditing(providerId = 'provider-a') {
310310
let container: HTMLDivElement
311311
let root: Root
312312

313-
function renderSso(organizationId: string) {
313+
function renderSso(organizationId: string, searchParams = '') {
314314
act(() => {
315315
root.render(
316-
<NuqsTestingAdapter>
316+
<NuqsTestingAdapter searchParams={searchParams}>
317317
<SSO organizationId={organizationId} />
318318
</NuqsTestingAdapter>
319319
)
@@ -678,6 +678,40 @@ describe('SAML callback URLs', () => {
678678
})
679679

680680
describe('SSO provider list', () => {
681+
it.each(['', '?provider=new'])(
682+
'opens and edits a provider named new with initial search params %s',
683+
(searchParams) => {
684+
mockUseSSOProviders.mockReturnValue({
685+
data: { providers: [{ ...provider('org-a'), providerId: 'new' }] },
686+
isLoading: false,
687+
})
688+
renderSso('org-a', searchParams)
689+
if (!searchParams) openProvider('new')
690+
691+
expect(findButton('Edit')).toBeDefined()
692+
expect(findButton('Save')).toBeUndefined()
693+
act(() => findButton('Edit')?.click())
694+
expect(container.querySelector('input[value="client-a"]')).not.toBeNull()
695+
}
696+
)
697+
698+
it('keeps creation separate from an existing provider named new', () => {
699+
mockUseSSOProviders.mockReturnValue({
700+
data: { providers: [{ ...provider('org-a'), providerId: 'new' }] },
701+
isLoading: false,
702+
})
703+
renderSso('org-a', '?provider=new&createProvider=true')
704+
705+
expect(findButton('Save')).toBeDefined()
706+
expect(findButton('Edit')).toBeUndefined()
707+
expect(container.querySelector('input[value="client-a"]')).toBeNull()
708+
709+
act(() => findButton('Identity providers')?.click())
710+
expect(container.querySelector('[aria-label="Open new"]')).not.toBeNull()
711+
openProvider('new')
712+
expect(findButton('Edit')).toBeDefined()
713+
})
714+
681715
it('lists every provider with its protocol and domain, one row each', () => {
682716
mockUseSSOProviders.mockReturnValue({
683717
data: {

apps/sim/ee/sso/components/sso-settings.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { SsoProviderList } from '@/ee/sso/components/sso-provider-list'
1616
import { SsoProviderSettings } from '@/ee/sso/components/sso-provider-settings'
1717
import { VerifiedDomainsSection } from '@/ee/sso/components/verified-domains-section'
1818
import { useDeleteSSOProvider, useSSOProviders } from '@/ee/sso/hooks/sso'
19-
import { NEW_SSO_PROVIDER, ssoSettingsParsers, ssoSettingsUrlKeys } from '@/ee/sso/search-params'
19+
import { ssoSettingsParsers, ssoSettingsUrlKeys } from '@/ee/sso/search-params'
2020
import { useOrganizationBilling } from '@/hooks/queries/organization'
2121

2222
const SETTINGS_TABS = [
@@ -40,28 +40,23 @@ export function SSO({ organizationId }: SSOProps) {
4040
}
4141

4242
function OrganizationSsoSettings({ organizationId }: SSOProps) {
43-
const [{ tab: requestedTab, provider: requestedProvider }, setParams] = useQueryStates(
44-
ssoSettingsParsers,
45-
ssoSettingsUrlKeys
46-
)
43+
const [{ tab: requestedTab, provider: requestedProvider, createProvider }, setParams] =
44+
useQueryStates(ssoSettingsParsers, ssoSettingsUrlKeys)
4745
const { billingEnabled, features } = useDeploymentShape()
4846
const billing = useOrganizationBilling(organizationId)
4947
const providers = useSSOProviders({ organizationId })
5048
const provisioningAvailable = features.scim
5149
const tab = requestedTab === 'provisioning' && !provisioningAvailable ? 'sign-in' : requestedTab
5250
const providerList = providers.data?.providers ?? []
5351
const selectedProvider =
54-
requestedProvider && requestedProvider !== NEW_SSO_PROVIDER
52+
requestedProvider && !createProvider
5553
? providerList.find((entry) => entry.providerId === requestedProvider)
5654
: undefined
5755
const signInView: 'create' | 'detail' | 'list' =
58-
providerList.length === 0 || requestedProvider === NEW_SSO_PROVIDER
59-
? 'create'
60-
: selectedProvider
61-
? 'detail'
62-
: 'list'
56+
providerList.length === 0 || createProvider ? 'create' : selectedProvider ? 'detail' : 'list'
6357
/** Opening pushed a history entry; closing must not push another. */
64-
const showList = () => void setParams({ provider: null }, { history: 'replace' })
58+
const showList = () =>
59+
void setParams({ provider: null, createProvider: null }, { history: 'replace' })
6560
const deleteProvider = useDeleteSSOProvider()
6661
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null)
6762
const pendingDelete = providerList.find((entry) => entry.providerId === pendingDeleteId)
@@ -128,19 +123,19 @@ function OrganizationSsoSettings({ organizationId }: SSOProps) {
128123
providers={providerList}
129124
active={tab === 'sign-in'}
130125
docsLink={DOCS_LINKS['sign-in']}
131-
onAdd={() => void setParams({ provider: NEW_SSO_PROVIDER })}
132-
onOpen={(providerId) => void setParams({ provider: providerId })}
126+
onAdd={() => void setParams({ provider: null, createProvider: true })}
127+
onOpen={(providerId) => void setParams({ provider: providerId, createProvider: null })}
133128
onDelete={setPendingDeleteId}
134129
/>
135130
) : (
136131
<SsoProviderSettings
137-
key={selectedProvider?.providerId ?? NEW_SSO_PROVIDER}
132+
key={selectedProvider ? `provider:${selectedProvider.providerId}` : 'create'}
138133
organizationId={organizationId}
139134
existingProvider={selectedProvider}
140135
active={tab === 'sign-in'}
141136
onOpenDomains={() => void setParams({ tab: 'domains' })}
142137
onSaved={(providerId) =>
143-
void setParams({ provider: providerId }, { history: 'replace' })
138+
void setParams({ provider: providerId, createProvider: null }, { history: 'replace' })
144139
}
145140
onBack={providerList.length > 0 ? showList : undefined}
146141
onDelete={

apps/sim/ee/sso/search-params.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { parseAsString, parseAsStringLiteral } from 'nuqs/server'
1+
import { parseAsBoolean, parseAsString, parseAsStringLiteral } from 'nuqs/server'
22

33
export const SSO_SETTINGS_TABS = ['sign-in', 'domains', 'provisioning'] as const
44

5-
/** The value of `provider` that opens the add-provider form rather than an existing one. */
6-
export const NEW_SSO_PROVIDER = 'new'
7-
85
export const ssoSettingsParsers = {
96
tab: parseAsStringLiteral(SSO_SETTINGS_TABS).withDefault('sign-in'),
107
/** Provider id being viewed or edited on the sign-in tab; absent shows the list. */
118
provider: parseAsString,
9+
createProvider: parseAsBoolean.withDefault(false),
1210
}
1311

1412
export const ssoSettingsUrlKeys = { history: 'push', clearOnDefault: true } as const

apps/sim/lib/api/contracts/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export type SsoProviderView = z.output<typeof ssoProviderListEntrySchema>
123123
export const deleteSsoProviderContract = defineRouteContract({
124124
method: 'DELETE',
125125
path: '/api/auth/sso/providers/[providerId]',
126-
params: z.object({ providerId: z.string().min(1).max(128) }),
126+
params: z.object({ providerId: z.string().min(1) }),
127127
response: {
128128
mode: 'json',
129129
schema: z.object({ success: z.literal(true), providerId: z.string() }),

0 commit comments

Comments
 (0)