Skip to content

Commit 6d01b28

Browse files
committed
fix(credentials): verify Databricks through the organization's egress, keep keyless reconnects on their mode, and harden held-subject renewal
1 parent bd0bd52 commit 6d01b28

18 files changed

Lines changed: 318 additions & 63 deletions

‎apps/docs/content/docs/cli/credentials.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ The printed audience is the default. To have Sim sign tokens for your own audien
9797

9898
Step 2: add those values to keylessConfig and pass it to `sim credentials create <providerId> --credentials @file`.
9999

100-
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest.
100+
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently.
101101

102102
If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, renew the proof and retry. If it reports WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry.
103103

@@ -384,7 +384,7 @@ The printed audience is the default. To have Sim sign tokens for your own audien
384384

385385
Step 2: add those values to keylessConfig and pass it to `sim credentials create <providerId> --credentials @file`.
386386

387-
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest.
387+
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently.
388388

389389
If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, renew the proof and retry. If it reports WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry.
390390

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ The printed audience is the default. To have Sim sign tokens for your own audien
784784

785785
Step 2: add those values to keylessConfig and pass it to `sim credentials create <providerId> --credentials @file`.
786786

787-
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest.
787+
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently.
788788

789789
If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, renew the proof and retry. If it reports WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry.
790790

@@ -834,7 +834,7 @@ The printed audience is the default. To have Sim sign tokens for your own audien
834834

835835
Step 2: add those values to keylessConfig and pass it to `sim credentials create <providerId> --credentials @file`.
836836

837-
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest.
837+
The proof expires after 24 hours and works only for the user who requested it, with the same providerId. Sim holds the subject for you for 30 days until a credential saves it, so a trust configuration that takes longer to apply still works: renew the proof with `sim credentials keyless-subject <providerId> --subject <subject>`. The subject stays the same, and each renewal restarts the 30 days. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently.
838838

839839
If creation reports WORKLOAD_IDENTITY_PROOF_EXPIRED, renew the proof and retry. If it reports WORKLOAD_IDENTITY_PROOF_INVALID, WORKLOAD_IDENTITY_SUBJECT_REQUIRED, WORKLOAD_IDENTITY_SUBJECT_IN_USE, run step 1 again, trust the new subject, and retry.
840840

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4382,7 +4382,7 @@
43824382
"post": {
43834383
"operationId": "createCredentialWorkloadIdentitySubject",
43844384
"summary": "Create workload identity subject",
4385-
"description": "Start a keyless credential. Returns the issuer, audience, and a fresh subject to configure in the cloud's trust policy, plus `keylessConfig`: a prefilled `serviceAccountJson` whose `subjectProof` is valid for 24 hours and only for the requesting user. Sim holds the subject for you until `subjectHeldUntil` (30 days, restarted by each renewal) unless a credential saves it first; pass it back as `subject` to renew the proof without changing the subject, so a trust policy that takes days to apply still matches. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest. Add the `requiredConfigFields`, then call Create service-account credential with the same `providerId`; Sim verifies the trust while saving. The returned `audience` is the default; set `audience` in `keylessConfig` to have Sim sign tokens for your own value instead, and trust that value in the cloud. Google Cloud has no fixed default: it is the credential's own pool provider. Without keyless support the deployment returns `400` with `details.code` `WORKLOAD_IDENTITY_NOT_CONFIGURED`. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
4385+
"description": "Start a keyless credential. Returns the issuer, audience, and a new subject to configure in the cloud's trust policy (or, when `subject` names one Sim is holding for you, that same subject with a renewed proof), plus `keylessConfig`: a prefilled `serviceAccountJson` whose `subjectProof` is valid for 24 hours and only for the requesting user. Sim holds the subject for you until `subjectHeldUntil` (30 days, restarted by each renewal) unless a credential saves it first; pass it back as `subject` to renew the proof without changing the subject, so a trust policy that takes days to apply still matches. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently. Add the `requiredConfigFields`, then call Create service-account credential with the same `providerId`; Sim verifies the trust while saving. The returned `audience` is the default; set `audience` in `keylessConfig` to have Sim sign tokens for your own value instead, and trust that value in the cloud. Google Cloud has no fixed default: it is the credential's own pool provider. Without keyless support the deployment returns `400` with `details.code` `WORKLOAD_IDENTITY_NOT_CONFIGURED`. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
43864386
"x-sim-operation": "credentials.workload_identity.prepare",
43874387
"x-oauth-scope": "api:write",
43884388
"tags": ["Credentials"],

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function useKeylessSetup<P extends WorkloadIdentityProvider>({
9898
const storedAuthType = providerAuthType(provider, setup.data?.reconnectAuthType)
9999
// A keyless credential reopens on keyless only where this deployment can still sign.
100100
const reconnectAuthType =
101-
storedAuthType === WORKLOAD_IDENTITY_KEYLESS_CONFIGS[provider].authType && !trust
101+
storedAuthType === WORKLOAD_IDENTITY_KEYLESS_CONFIGS[provider].authType && !heldTrust
102102
? undefined
103103
: storedAuthType
104104
return {

‎apps/sim/lib/api/contracts/v2/openapi/resources.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1842,7 +1842,7 @@ const declaredRoutes = [
18421842
applicationOperation: credentialOperations.prepareWorkloadIdentity,
18431843
operationId: 'createCredentialWorkloadIdentitySubject',
18441844
summary: 'Create workload identity subject',
1845-
description: `Start a keyless credential. Returns the issuer, audience, and a fresh subject to configure in the cloud's trust policy, plus \`keylessConfig\`: a prefilled \`serviceAccountJson\` whose \`subjectProof\` is valid for 24 hours and only for the requesting user. Sim holds the subject for you until \`subjectHeldUntil\` (30 days, restarted by each renewal) unless a credential saves it first; pass it back as \`subject\` to renew the proof without changing the subject, so a trust policy that takes days to apply still matches. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest. Add the \`requiredConfigFields\`, then call Create service-account credential with the same \`providerId\`; Sim verifies the trust while saving. The returned \`audience\` is the default; set \`audience\` in \`keylessConfig\` to have Sim sign tokens for your own value instead, and trust that value in the cloud. Google Cloud has no fixed default: it is the credential's own pool provider. Without keyless support the deployment returns \`400\` with \`details.code\` \`WORKLOAD_IDENTITY_NOT_CONFIGURED\`. ${WORKSPACE_API_KEY_DENIED}`,
1845+
description: `Start a keyless credential. Returns the issuer, audience, and a new subject to configure in the cloud's trust policy (or, when \`subject\` names one Sim is holding for you, that same subject with a renewed proof), plus \`keylessConfig\`: a prefilled \`serviceAccountJson\` whose \`subjectProof\` is valid for 24 hours and only for the requesting user. Sim holds the subject for you until \`subjectHeldUntil\` (30 days, restarted by each renewal) unless a credential saves it first; pass it back as \`subject\` to renew the proof without changing the subject, so a trust policy that takes days to apply still matches. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently. Add the \`requiredConfigFields\`, then call Create service-account credential with the same \`providerId\`; Sim verifies the trust while saving. The returned \`audience\` is the default; set \`audience\` in \`keylessConfig\` to have Sim sign tokens for your own value instead, and trust that value in the cloud. Google Cloud has no fixed default: it is the credential's own pool provider. Without keyless support the deployment returns \`400\` with \`details.code\` \`WORKLOAD_IDENTITY_NOT_CONFIGURED\`. ${WORKSPACE_API_KEY_DENIED}`,
18461846
errors: RESOURCE_ERRORS,
18471847
success: { description: 'A subject and proof for one new keyless credential.' },
18481848
}),

‎apps/sim/lib/api/mcp/generated/v2-operations.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ export const V2_MCP_OPERATIONS = {
665665
contract: v2CreateCredentialWorkloadIdentitySubjectContract,
666666
summary: 'Create workload identity subject',
667667
description:
668-
"Start a keyless credential. Returns the issuer, audience, and a fresh subject to configure in the cloud's trust policy, plus `keylessConfig`: a prefilled `serviceAccountJson` whose `subjectProof` is valid for 24 hours and only for the requesting user. Sim holds the subject for you until `subjectHeldUntil` (30 days, restarted by each renewal) unless a credential saves it first; pass it back as `subject` to renew the proof without changing the subject, so a trust policy that takes days to apply still matches. Sim holds your 20 newest unsaved subjects per provider in a workspace; issuing more drops the oldest. Add the `requiredConfigFields`, then call Create service-account credential with the same `providerId`; Sim verifies the trust while saving. The returned `audience` is the default; set `audience` in `keylessConfig` to have Sim sign tokens for your own value instead, and trust that value in the cloud. Google Cloud has no fixed default: it is the credential's own pool provider. Without keyless support the deployment returns `400` with `details.code` `WORKLOAD_IDENTITY_NOT_CONFIGURED`. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
668+
"Start a keyless credential. Returns the issuer, audience, and a new subject to configure in the cloud's trust policy (or, when `subject` names one Sim is holding for you, that same subject with a renewed proof), plus `keylessConfig`: a prefilled `serviceAccountJson` whose `subjectProof` is valid for 24 hours and only for the requesting user. Sim holds the subject for you until `subjectHeldUntil` (30 days, restarted by each renewal) unless a credential saves it first; pass it back as `subject` to renew the proof without changing the subject, so a trust policy that takes days to apply still matches. Sim holds up to 20 unsaved subjects per person and provider in a workspace; issuing more drops the one shown least recently. Add the `requiredConfigFields`, then call Create service-account credential with the same `providerId`; Sim verifies the trust while saving. The returned `audience` is the default; set `audience` in `keylessConfig` to have Sim sign tokens for your own value instead, and trust that value in the cloud. Google Cloud has no fixed default: it is the credential's own pool provider. Without keyless support the deployment returns `400` with `details.code` `WORKLOAD_IDENTITY_NOT_CONFIGURED`. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
669669
workspaceKeyUnsupported: true,
670670
handler: () =>
671671
import('@/app/api/v2/credentials/workload-identity/subjects/route').then(

‎apps/sim/lib/credentials/__integration__/workload-identity-pending-subject.integration.ts‎

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ vi.mock('@/lib/credentials/workload-identity/issuer', async (importOriginal) =>
2626
isWorkloadIdentityConfigured: async () => true,
2727
mintWorkloadIdentityToken: async ({ subject }: { subject: string }) => `token-for:${subject}`,
2828
}))
29+
/** The identity check goes through the egress guard, not global fetch; serve its fixture there. */
30+
vi.mock('@/lib/core/security/input-validation.server', async (importOriginal) => {
31+
const actual =
32+
await importOriginal<typeof import('@/lib/core/security/input-validation.server')>()
33+
return {
34+
...actual,
35+
secureFetchWithValidation: async (
36+
...args: Parameters<typeof actual.secureFetchWithValidation>
37+
) => {
38+
if (args[0] !== 'https://dbc-5678abcd-1234.cloud.databricks.com/api/2.0/preview/scim/v2/Me')
39+
return actual.secureFetchWithValidation(...args)
40+
const body = JSON.stringify({ userName: 'service-principal@example.com' })
41+
return {
42+
ok: true,
43+
status: 200,
44+
statusText: 'OK',
45+
headers: new actual.SecureFetchHeaders({ 'content-type': 'application/json' }),
46+
body: null,
47+
text: async () => body,
48+
json: async () => JSON.parse(body),
49+
arrayBuffer: async () => new TextEncoder().encode(body).buffer,
50+
}
51+
},
52+
}
53+
})
2954

3055
import { updateCredentialRecord } from '@/lib/credentials/orchestration'
3156
import { createCredentialRecord } from '@/lib/credentials/orchestration/credential-create'
@@ -120,9 +145,6 @@ describe('holding an unsaved keyless subject', () => {
120145
? jsonResponse({ access_token: `access-for:${subject}` })
121146
: jsonResponse({ error: 'invalid_client' }, 401)
122147
}
123-
if (target === `https://${HOST}/api/2.0/preview/scim/v2/Me`) {
124-
return jsonResponse({ userName: 'service-principal@example.com' })
125-
}
126148
return originalFetch(input, init)
127149
})
128150
}, 60_000)
@@ -194,6 +216,7 @@ describe('holding an unsaved keyless subject', () => {
194216
userId,
195217
provider: 'databricks',
196218
subject: held.subject,
219+
createdAt: new Date(),
197220
expiresAt: new Date(Date.now() + DAY_MS),
198221
})
199222

@@ -266,6 +289,31 @@ describe('holding an unsaved keyless subject', () => {
266289
expect(await heldSubjects()).toContain(held.subject)
267290
})
268291

292+
it('keeps a subject shown recently when the cap drops older ones', async () => {
293+
const capped = { ...issuer, provider: 'microsoft' as const }
294+
const tenDaysAgo = new Date(Date.now() - 10 * DAY_MS)
295+
const underReview = await resolvePendingWorkloadIdentitySubject(
296+
capped,
297+
{ kind: 'new' },
298+
tenDaysAgo
299+
)
300+
for (let index = 0; index < 19; index++) {
301+
await resolvePendingWorkloadIdentitySubject(
302+
capped,
303+
{ kind: 'new' },
304+
new Date(tenDaysAgo.getTime() + (index + 1) * 60_000)
305+
)
306+
}
307+
await resolvePendingWorkloadIdentitySubject(capped, {
308+
kind: 'renew',
309+
subject: underReview.subject,
310+
})
311+
312+
await resolvePendingWorkloadIdentitySubject(capped, { kind: 'new' })
313+
314+
expect(await heldSubjects()).toContain(underReview.subject)
315+
})
316+
269317
it('keeps one person from piling up held subjects', async () => {
270318
const capped = { ...issuer, provider: 'snowflake' as const }
271319
for (let index = 0; index < 25; index++) {

‎apps/sim/lib/credentials/__integration__/workload-identity-subject-reuse.integration.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ vi.mock('@/lib/credentials/workload-identity/issuer', async (importOriginal) =>
1919
isWorkloadIdentityConfigured: async () => true,
2020
mintWorkloadIdentityToken: async ({ subject }: { subject: string }) => `token-for:${subject}`,
2121
}))
22+
/** The identity check goes through the egress guard, not global fetch; serve its fixture there. */
23+
vi.mock('@/lib/core/security/input-validation.server', async (importOriginal) => {
24+
const actual =
25+
await importOriginal<typeof import('@/lib/core/security/input-validation.server')>()
26+
return {
27+
...actual,
28+
secureFetchWithValidation: async (
29+
...args: Parameters<typeof actual.secureFetchWithValidation>
30+
) => {
31+
if (args[0] !== 'https://dbc-1234abcd-5678.cloud.databricks.com/api/2.0/preview/scim/v2/Me')
32+
return actual.secureFetchWithValidation(...args)
33+
const body = JSON.stringify({ userName: 'service-principal@example.com' })
34+
return {
35+
ok: true,
36+
status: 200,
37+
statusText: 'OK',
38+
headers: new actual.SecureFetchHeaders({ 'content-type': 'application/json' }),
39+
body: null,
40+
text: async () => body,
41+
json: async () => JSON.parse(body),
42+
arrayBuffer: async () => new TextEncoder().encode(body).buffer,
43+
}
44+
},
45+
}
46+
})
2247

2348
import { encryptSecret } from '@/lib/core/security/encryption'
2449
import { updateCredentialRecord } from '@/lib/credentials/orchestration'
@@ -114,9 +139,6 @@ describe('reusing a keyless trust setup', () => {
114139
? jsonResponse({ access_token: `access-for:${subject}` })
115140
: jsonResponse({ error: 'invalid_client' }, 401)
116141
}
117-
if (target === `https://${HOST}/api/2.0/preview/scim/v2/Me`) {
118-
return jsonResponse({ userName: 'service-principal@example.com' })
119-
}
120142
return originalFetch(input, init)
121143
})
122144
}, 60_000)

0 commit comments

Comments
 (0)