Skip to content

Commit b8932fa

Browse files
committed
refactor(servicenow): mark the username with requiredForAuthMethods so the catalog scopes it to the password sign-in
1 parent bf62b4a commit b8932fa

4 files changed

Lines changed: 49 additions & 16 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/token-service-account-modal.tsx‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import {
2323
CLIENT_CREDENTIALS_AUTH_METHOD,
2424
getTokenServiceAccountErrorMessage,
2525
isTokenServiceAccountFieldRequired,
26+
type TokenServiceAccountAuthMethod,
2627
type TokenServiceAccountDescriptor,
2728
type TokenServiceAccountField,
29+
tokenFieldBelongsToAuthMethod,
2830
} from '@/lib/credentials/token-service-accounts/descriptors'
2931
import { ClientCredentialsFields } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credentials-fields'
3032
import { withBrandIcon } from '@/blocks/brand-icon'
@@ -42,8 +44,6 @@ function normalizeDomainInput(raw: string): string {
4244
.replace(/[/?#].*$/, '')
4345
}
4446

45-
type TokenAuthMethod = 'token' | typeof CLIENT_CREDENTIALS_AUTH_METHOD
46-
4747
function openDocs(url: string): void {
4848
window.open(url, '_blank', 'noopener,noreferrer')
4949
}
@@ -101,25 +101,30 @@ function TokenServiceAccountModalForm({
101101
submitPending,
102102
}: TokenServiceAccountModalProps) {
103103
const clientCredentials = descriptor.clientCredentials
104-
const [selectedAuthMethod, setSelectedAuthMethod] = useState<TokenAuthMethod | null>(null)
104+
const [selectedAuthMethod, setSelectedAuthMethod] =
105+
useState<TokenServiceAccountAuthMethod | null>(null)
105106
/**
106107
* A reconnect cannot pre-select the mode: the stored one lives inside the encrypted secret and
107108
* is never returned to the client, so the admin restates it while retyping the secret. Until
108109
* then neither mode's fields show, so a reconnect never switches modes by default.
109110
*/
110111
const mustRestateAuthMethod = Boolean(clientCredentials && credentialId && !selectedAuthMethod)
111-
const authMethod: TokenAuthMethod | null =
112+
const authMethod: TokenServiceAccountAuthMethod | null =
112113
selectedAuthMethod ??
113114
(mustRestateAuthMethod
114115
? null
115116
: clientCredentials?.preferred
116117
? CLIENT_CREDENTIALS_AUTH_METHOD
117118
: 'token')
118119
const usesClientCredentials = authMethod === CLIENT_CREDENTIALS_AUTH_METHOD
119-
/** A field of the pasted-token sign-in only (ServiceNow's username) shows once that mode is chosen. */
120+
/** A field that belongs to one sign-in mode (ServiceNow's username) shows once that mode is chosen. */
120121
const shownField = (id: TokenServiceAccountField['id']) =>
121122
descriptor.fields.find(
122-
(field) => field.id === id && !(field.pastedTokenOnly && authMethod !== 'token')
123+
(field) =>
124+
field.id === id &&
125+
(authMethod
126+
? tokenFieldBelongsToAuthMethod(field, authMethod)
127+
: !field.requiredForAuthMethods)
123128
)
124129
const tokenField = descriptor.fields.find((field) => field.id === 'apiToken')
125130
const domainField = shownField('domain')
@@ -278,7 +283,7 @@ function TokenServiceAccountModalForm({
278283
title='Authentication'
279284
value={authMethod ?? undefined}
280285
onChange={(value) => {
281-
setSelectedAuthMethod(value as TokenAuthMethod)
286+
setSelectedAuthMethod(value as TokenServiceAccountAuthMethod)
282287
if (error) setError(null)
283288
}}
284289
options={[

‎apps/sim/lib/credentials/application/provider-catalog-contract.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,23 @@ describe('v2 credential provider catalog contract', () => {
199199
expect(configuration?.hint).not.toContain('Keyless')
200200
})
201201

202+
it('tells headless clients the ServiceNow username belongs to the password sign-in only', async () => {
203+
const providers = await listProviders('servicenow')
204+
const servicenow = providers.find(
205+
(provider) =>
206+
provider.type === 'service_account' && provider.providerId === 'servicenow-service-account'
207+
)
208+
209+
expect(responseSchema.safeParse({ data: providers, nextCursor: null }).success).toBe(true)
210+
expect(servicenow?.fields.find((field) => field.id === 'providerWorkspaceId')).toMatchObject({
211+
required: true,
212+
requiredForAuthMethods: ['token'],
213+
})
214+
expect(
215+
servicenow?.fields.find((field) => field.id === 'domain')?.requiredForAuthMethods
216+
).toBeUndefined()
217+
})
218+
202219
it('lists the native Claude Platform provider when filtered', async () => {
203220
const providers = await listProviders('claude')
204221

‎apps/sim/lib/credentials/service-account-secret.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -651,9 +651,7 @@ async function buildTokenClientCredentialSecret(
651651
}
652652
const clientId = fields.clientId?.trim()
653653
const clientSecret = fields.clientSecret?.trim()
654-
const domainField = descriptor.fields.find(
655-
(field) => field.id === 'domain' && !field.pastedTokenOnly
656-
)
654+
const domainField = descriptor.fields.find((field) => field.id === 'domain')
657655
const domain = fields.domain?.trim() || domainField?.defaultValue
658656
const requiresDomain = Boolean(domainField)
659657
if (!clientId || !clientSecret || (requiresDomain && !domain)) {

‎apps/sim/lib/credentials/token-service-accounts/descriptors.ts‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ export interface TokenServiceAccountField {
4242
*/
4343
url?: boolean
4444
/**
45-
* Belongs to the pasted-token sign-in only (ServiceNow's username, which a minted token replaces):
46-
* the client-credentials mode neither shows, requires, nor stores it.
45+
* Auth methods this field belongs to, for a provider that also offers client credentials:
46+
* `token` is the pasted-token sign-in and `client_credentials` the grant. A mode not listed
47+
* neither shows, requires, nor stores it (ServiceNow's username, which a minted token replaces).
48+
* Absent means the field belongs to every mode.
4749
*/
48-
pastedTokenOnly?: boolean
50+
requiredForAuthMethods?: readonly TokenServiceAccountAuthMethod[]
4951
/** Soft-format hint shown while the current value doesn't match `hintPattern`. */
5052
hintPattern?: RegExp
5153
hintMessage?: string
@@ -131,6 +133,17 @@ interface TokenServiceAccountClientCredentials {
131133
/** `authMethod` value selecting the client-credentials mode of a provider whose default is a pasted token (a token provider, or the Atlassian service account). */
132134
export const CLIENT_CREDENTIALS_AUTH_METHOD = 'client_credentials' as const
133135

136+
/** A token provider's sign-in modes: the pasted token (the default) or its client-credentials grant. */
137+
export type TokenServiceAccountAuthMethod = 'token' | typeof CLIENT_CREDENTIALS_AUTH_METHOD
138+
139+
/** Whether a descriptor field belongs to the given sign-in mode. */
140+
export function tokenFieldBelongsToAuthMethod(
141+
field: Pick<TokenServiceAccountField, 'requiredForAuthMethods'>,
142+
authMethod: TokenServiceAccountAuthMethod
143+
): boolean {
144+
return !field.requiredForAuthMethods || field.requiredForAuthMethods.includes(authMethod)
145+
}
146+
134147
export const HUBSPOT_SERVICE_ACCOUNT_PROVIDER_ID = 'hubspot-service-account' as const
135148
export const AIRTABLE_SERVICE_ACCOUNT_PROVIDER_ID = 'airtable-service-account' as const
136149
export const NOTION_SERVICE_ACCOUNT_PROVIDER_ID = 'notion-service-account' as const
@@ -2597,7 +2610,7 @@ export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record<
25972610
label: 'Username',
25982611
placeholder: 'Enter the ServiceNow username',
25992612
secret: false,
2600-
pastedTokenOnly: true,
2613+
requiredForAuthMethods: ['token'],
26012614
},
26022615
{
26032616
id: 'domain',
@@ -2631,7 +2644,7 @@ export function isTokenServiceAccountFieldRequired(field: TokenServiceAccountFie
26312644
/**
26322645
* Required contract fields per token provider in client-credentials mode: the client ID and
26332646
* secret replace the token, and every other descriptor field without a default (a store
2634-
* domain) stays required unless it belongs to the pasted-token sign-in only.
2647+
* domain) stays required unless it belongs only to the pasted-token sign-in.
26352648
*/
26362649
export const TOKEN_SERVICE_ACCOUNT_CLIENT_CREDENTIAL_FIELDS: Record<
26372650
string,
@@ -2646,7 +2659,7 @@ export const TOKEN_SERVICE_ACCOUNT_CLIENT_CREDENTIAL_FIELDS: Record<
26462659
'clientSecret',
26472660
...descriptor.fields.flatMap((field) =>
26482661
field.id === 'apiToken' ||
2649-
field.pastedTokenOnly ||
2662+
!tokenFieldBelongsToAuthMethod(field, CLIENT_CREDENTIALS_AUTH_METHOD) ||
26502663
!isTokenServiceAccountFieldRequired(field)
26512664
? []
26522665
: [field.id]

0 commit comments

Comments
 (0)