Skip to content

Commit 6993890

Browse files
committed
fix(credentials): reopen every keyless-capable reconnect on its stored auth mode and reject native credential tools without a policy
- prepareWorkloadIdentity reports the reconnected credential's stored authType (reconnectAuthType), for an admin of a credential in the same workspace, whether or not this deployment can sign. reconnectsKeyless is removed from the contract and hook. The AWS, Databricks, Snowflake, Microsoft and Google dialogs reopen on that mode, so an assume_role, personal_access_token or certificate credential keeps its auth. - The saved-credential contract fails a native credential block whose tool has no oauth policy: the executor would never fill its token. - The Snowflake hint and docs say where to change SIM_WORKFLOWS; the DocuSign account_not_found message covers a credential with no Account ID; the docs baseline's reviewed CLI example entries carry their new byte length; the contract test fixtures use a real OAuth service id.
1 parent f882882 commit 6993890

19 files changed

Lines changed: 334 additions & 275 deletions

‎apps/docs/content/docs/integrations/snowflake-service-account.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Snowflake's [workload identity federation](https://docs.snowflake.com/en/user-gu
2424
In Sim, add a **Snowflake** connection, choose **Service user (keyless, via Sim OIDC)**, and enter the account host (`myorg-myaccount.snowflakecomputing.com`). The dialog shows the issuer, audience, and subject for this connection, and the statement to run.
2525
</Step>
2626
<Step>
27-
A user whose role can create users (for example USERADMIN) runs the statement from the dialog. Rename the user if SIM_WORKFLOWS already exists. It looks like this:
27+
A user whose role can create users (for example USERADMIN) runs the statement from the dialog. If SIM_WORKFLOWS already exists, change the name in the statement you run and in the follow-up GRANT and ALTER commands below. It looks like this:
2828

2929
```sql
3030
CREATE USER SIM_WORKFLOWS

‎apps/docs/scripts/fixtures/content-baseline.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3017,7 +3017,7 @@
30173017
"lang": "bash",
30183018
"meta": null,
30193019
"sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944",
3020-
"bytes": 262,
3020+
"bytes": 276,
30213021
"line": 102
30223022
},
30233023
{
@@ -3080,7 +3080,7 @@
30803080
"lang": "bash",
30813081
"meta": null,
30823082
"sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944",
3083-
"bytes": 262,
3083+
"bytes": 276,
30843084
"line": 375
30853085
}
30863086
],
@@ -11191,7 +11191,7 @@
1119111191
"lang": "bash",
1119211192
"meta": null,
1119311193
"sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944",
11194-
"bytes": 262,
11194+
"bytes": 276,
1119511195
"line": 788
1119611196
},
1119711197
{
@@ -11205,7 +11205,7 @@
1120511205
"lang": "bash",
1120611206
"meta": null,
1120711207
"sha256": "48d4019bc1905094aec776873af1d5a4e94ec2f52d043c51bbbb559b6a876944",
11208-
"bytes": 262,
11208+
"bytes": 276,
1120911209
"line": 824
1121011210
},
1121111211
{

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,10 @@ function AwsAccountForm({
114114
const [displayName, setDisplayName] = useState(initialDisplayName ?? '')
115115
const [description, setDescription] = useState(initialDescription ?? '')
116116

117-
// A reconnect opens on keyless only for a keyless credential, so a key-based connection is
118-
// never switched to keyless by default; a new connection defaults to keyless wherever this
119-
// deployment supports it.
117+
// A reconnect opens on the credential's stored mode, so it never switches auth by default;
118+
// a new connection defaults to keyless wherever this deployment supports it.
120119
const defaultAuthType: AwsAuthType = credentialId
121-
? keyless.reconnectsKeyless
122-
? 'oidc_role'
123-
: 'access_keys'
120+
? (keyless.reconnectAuthType ?? 'access_keys')
124121
: keyless.available
125122
? 'oidc_role'
126123
: 'access_keys'

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -675,12 +675,12 @@ function GoogleServiceAccountModalForm({
675675
const keylessOffer = googleKeylessOffer({
676676
oauthServiceId,
677677
available: Boolean(keyless.available),
678-
reconnectsKeyless: keyless.reconnectsKeyless,
678+
reconnectsKeyless: keyless.reconnectAuthType === 'workload_identity',
679679
})
680680
const offerKeyless = keylessOffer === 'offer'
681681
const [selectedAuthType, setSelectedAuthType] = useState<GoogleAuthType | null>(null)
682682
const authType: GoogleAuthType = offerKeyless
683-
? (selectedAuthType ?? (keyless.reconnectsKeyless ? 'workload_identity' : 'json_key'))
683+
? (selectedAuthType ?? keyless.reconnectAuthType ?? 'json_key')
684684
: 'json_key'
685685
const [keylessInput, setKeylessInput] = useState<GoogleKeylessInput>(EMPTY_GOOGLE_KEYLESS_INPUT)
686686
const [jsonInput, setJsonInput] = useState('')

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,10 @@ function DatabricksAccountForm({
9393
const [displayName, setDisplayName] = useState(initialDisplayName ?? '')
9494
const [description, setDescription] = useState(initialDescription ?? '')
9595

96-
// A reconnect opens on keyless only for a keyless credential, so a key-based connection is
97-
// never switched to keyless by default; a new connection defaults to keyless wherever this
98-
// deployment supports it.
96+
// A reconnect opens on the credential's stored mode, so it never switches auth by default;
97+
// a new connection defaults to keyless wherever this deployment supports it.
9998
const defaultAuthType: DatabricksAuthType = credentialId
100-
? keyless.reconnectsKeyless
101-
? 'oidc_federation'
102-
: 'service_principal'
99+
? (keyless.reconnectAuthType ?? 'service_principal')
103100
: keyless.available
104101
? 'oidc_federation'
105102
: 'service_principal'

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { ChipModalError, ChipModalField } from '@sim/emcn'
55
import { generateShortId } from '@sim/utils/id'
66
import { isApiClientError } from '@/lib/api/client/errors'
77
import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope'
8-
import type { WorkloadIdentityProvider } from '@/lib/credentials/workload-identity/audiences'
8+
import {
9+
WORKLOAD_IDENTITY_KEYLESS_CONFIGS,
10+
WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES,
11+
type WorkloadIdentityProvider,
12+
type WorkloadIdentityProviderAuthType,
13+
} from '@/lib/credentials/workload-identity/audiences'
914
import {
1015
useCreateScopedCredential,
1116
useUpdateScopedCredential,
@@ -20,8 +25,8 @@ import {
2025

2126
export type TrustSubjectChoice = 'keep' | 'new'
2227

23-
interface KeylessSetupInput {
24-
provider: WorkloadIdentityProvider
28+
interface KeylessSetupInput<P extends WorkloadIdentityProvider> {
29+
provider: P
2530
workspaceId?: string
2631
organizationId?: string
2732
credentialId?: string
@@ -37,13 +42,13 @@ interface KeylessSetupInput {
3742
* reconnect the configured trust is kept by default: issuing a new subject
3843
* would break the role or federation policy the customer already set up.
3944
*/
40-
export function useKeylessSetup({
45+
export function useKeylessSetup<P extends WorkloadIdentityProvider>({
4146
provider,
4247
workspaceId,
4348
organizationId,
4449
credentialId,
4550
enabled = true,
46-
}: KeylessSetupInput) {
51+
}: KeylessSetupInput<P>) {
4752
const [sessionKey] = useState(() => generateShortId())
4853
const [subjectChoice, setSubjectChoice] = useState<TrustSubjectChoice>(
4954
credentialId ? 'keep' : 'new'
@@ -57,11 +62,17 @@ export function useKeylessSetup({
5762
credentialId,
5863
})
5964
const trust = setup.data?.available ? setup.data : undefined
65+
const storedAuthType = providerAuthType(provider, setup.data?.reconnectAuthType)
66+
// A keyless credential reopens on keyless only where this deployment can still sign.
67+
const reconnectAuthType =
68+
storedAuthType === WORKLOAD_IDENTITY_KEYLESS_CONFIGS[provider].authType && !trust
69+
? undefined
70+
: storedAuthType
6071
return {
6172
/** Undefined while this deployment's support is still loading. */
6273
available: workspaceScoped ? setup.data?.available : false,
63-
/** The credential being reconnected is keyless (reported for Snowflake and Google only). */
64-
reconnectsKeyless: Boolean(setup.data?.available && setup.data.reconnectsKeyless),
74+
/** The stored mode of the credential being reconnected; a reconnect reopens on it. */
75+
reconnectAuthType,
6576
isReconnect: Boolean(credentialId),
6677
subjectChoice,
6778
setSubjectChoice,
@@ -73,7 +84,17 @@ export function useKeylessSetup({
7384
}
7485
}
7586

76-
export type KeylessSetup = ReturnType<typeof useKeylessSetup>
87+
export type KeylessSetup<P extends WorkloadIdentityProvider = WorkloadIdentityProvider> =
88+
ReturnType<typeof useKeylessSetup<P>>
89+
90+
function providerAuthType<P extends WorkloadIdentityProvider>(
91+
provider: P,
92+
value: string | undefined
93+
): WorkloadIdentityProviderAuthType<P> | undefined {
94+
const modes: readonly WorkloadIdentityProviderAuthType<P>[] =
95+
WORKLOAD_IDENTITY_PROVIDER_AUTH_TYPES[provider]
96+
return modes.find((mode) => mode === value)
97+
}
7798

7899
/** The subject fields of a keyless config: a new subject with its proof, or none to keep the stored one. */
79100
export function keylessSubjectFields(setup: KeylessSetup) {

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,10 @@ function MicrosoftAppAccountForm({
8989
const [displayName, setDisplayName] = useState(initialDisplayName ?? '')
9090
const [description, setDescription] = useState(initialDescription ?? '')
9191

92-
// A reconnect opens on keyless only for a keyless credential, which has a subject to keep; a
93-
// new connection defaults to keyless wherever this deployment supports it.
92+
// A reconnect opens on the credential's stored mode, so it never switches auth by default;
93+
// a new connection defaults to keyless wherever this deployment supports it.
9494
const defaultAuthType: MicrosoftAuthType = credentialId
95-
? keyless.reconnectsKeyless
96-
? 'federated'
97-
: 'client_secret'
95+
? (keyless.reconnectAuthType ?? 'client_secret')
9896
: keyless.available
9997
? 'federated'
10098
: 'client_secret'

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function SnowflakeAccountForm({
135135
initialDescription,
136136
onCreated,
137137
keyless,
138-
}: SnowflakeAccountModalProps & { keyless: KeylessSetup }) {
138+
}: SnowflakeAccountModalProps & { keyless: KeylessSetup<'snowflake'> }) {
139139
const close = () => onOpenChange(false)
140140
const keylessSubmit = useSavedConnectionSubmit({
141141
providerId: SNOWFLAKE_SERVICE_ACCOUNT_PROVIDER_ID,
@@ -155,13 +155,10 @@ function SnowflakeAccountForm({
155155
const [displayName, setDisplayName] = useState(initialDisplayName ?? '')
156156
const [description, setDescription] = useState(initialDescription ?? '')
157157

158-
// A reconnect opens on the credential's current mode, so a keyless connection is never
159-
// turned into a token one by default; a new connection defaults to keyless wherever this
160-
// deployment supports it.
158+
// A reconnect opens on the credential's stored mode, so it never switches auth by default;
159+
// a new connection defaults to keyless wherever this deployment supports it.
161160
const defaultAuthType: SnowflakeAuthType = credentialId
162-
? keyless.reconnectsKeyless
163-
? 'workload_identity'
164-
: 'programmatic_access_token'
161+
? (keyless.reconnectAuthType ?? 'programmatic_access_token')
165162
: keyless.available
166163
? 'workload_identity'
167164
: 'programmatic_access_token'
@@ -301,7 +298,7 @@ function SnowflakeAccountForm({
301298
<ChipModalField
302299
type='custom'
303300
title='Service user'
304-
hint='An admin runs this once in the account, then grants the user a role and sets it as its default role, before you save. Rename the user if SIM_WORKFLOWS already exists.'
301+
hint='An admin runs this once in the account, then grants the user a role and sets it as its default role, before you save. If SIM_WORKFLOWS already exists, change the name in the statement you run and in any follow-up GRANT or ALTER commands.'
305302
>
306303
{(aria) => (
307304
<ChipTextarea

‎apps/sim/lib/api/contracts/credentials.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
ATLASSIAN_PRODUCTS,
66
getMissingServiceAccountFields,
77
} from '@/lib/credentials/service-account-fields'
8-
import { WORKLOAD_IDENTITY_PROVIDERS } from '@/lib/credentials/workload-identity/audiences'
8+
import {
9+
WORKLOAD_IDENTITY_AUTH_TYPE_LIST,
10+
WORKLOAD_IDENTITY_PROVIDERS,
11+
} from '@/lib/credentials/workload-identity/audiences'
912
import type { OAuthProvider } from '@/lib/oauth/types'
1013

1114
export const atlassianProductSchema = z.enum(ATLASSIAN_PRODUCTS)
@@ -560,6 +563,9 @@ export const createWorkspaceCredentialContract = defineRouteContract({
560563
* dialog's contract and the public `createCredentialWorkloadIdentitySubject`
561564
* operation so the two cannot describe the same grant differently.
562565
*/
566+
/** The reconnected credential's stored auth mode, so the dialog reopens on it. */
567+
const reconnectAuthTypeSchema = z.enum(WORKLOAD_IDENTITY_AUTH_TYPE_LIST).optional()
568+
563569
export const workloadIdentitySubjectGrantShape = {
564570
issuer: z
565571
.string()
@@ -598,12 +604,11 @@ export const prepareWorkloadIdentityContract = defineRouteContract({
598604
mode: 'json',
599605
schema: z.discriminatedUnion('available', [
600606
/** This deployment has no workload-identity signing key; offer key-based modes only. */
601-
z.object({ available: z.literal(false) }),
607+
z.object({ available: z.literal(false), reconnectAuthType: reconnectAuthTypeSchema }),
602608
z.object({
603609
available: z.literal(true),
604610
...workloadIdentitySubjectGrantShape,
605-
/** The reconnected credential is keyless (Snowflake, Microsoft app, and Google only). */
606-
reconnectsKeyless: z.literal(true).optional(),
611+
reconnectAuthType: reconnectAuthTypeSchema,
607612
}),
608613
]),
609614
},

0 commit comments

Comments
 (0)