Skip to content

Commit 773ffcc

Browse files
committed
improvement(auth): unify resource-scoped human delegation
1 parent 2af5443 commit 773ffcc

89 files changed

Lines changed: 458 additions & 329 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/api/files/uploads/purposes.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Principal } from '@sim/auth/principal'
2+
import { isWorkspaceDelegation } from '@sim/auth/principal'
23
import { db } from '@sim/db'
34
import { workspace } from '@sim/db/schema'
45
import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow'
@@ -250,11 +251,6 @@ function requireSessionScope(value: string | null, label = 'scope'): string {
250251

251252
async function principalUserId(principal: Principal, workspaceId?: string): Promise<string> {
252253
switch (principal.kind) {
253-
case 'resource_delegated':
254-
throw new UploadSessionError(
255-
'forbidden',
256-
'Resource delegation cannot create workspace uploads'
257-
)
258254
case 'slack_app':
259255
case 'slack_installation':
260256
throw new UploadSessionError('forbidden', 'Slack installations cannot create uploads')
@@ -274,6 +270,11 @@ async function principalUserId(principal: Principal, workspaceId?: string): Prom
274270
return context.billedAccountUserId
275271
}
276272
case 'delegated':
273+
if (!isWorkspaceDelegation(principal))
274+
throw new UploadSessionError(
275+
'forbidden',
276+
'Resource delegation cannot create workspace uploads'
277+
)
277278
if (
278279
principal.serviceId === 'copilot' &&
279280
principal.workspaceId === workspaceId &&

‎apps/sim/ee/access-requests/lib/application/operations.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { DelegatedPrincipal, Principal } from '@sim/auth/principal'
1+
import type { Principal, WorkspaceDelegatedPrincipal } from '@sim/auth/principal'
22
import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
33
import { defineWorkspaceOperation } from '@/lib/core/application/workspace-operation'
44

55
/** Chat acting for the requester joins the direct callers on the workspace routes only. */
66
export type AccessRequestPrincipal =
77
| Extract<Principal, { kind: 'session' | 'personal_api_key' | 'oauth_access_token' }>
8-
| (DelegatedPrincipal & { serviceId: 'copilot' })
8+
| (WorkspaceDelegatedPrincipal & { serviceId: 'copilot' })
99

1010
export const ACCESS_REQUEST_DELEGATION_AUDIENCE = 'sim:settings'
1111

‎apps/sim/lib/api/server/routes/copilot-project-file-request.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('private v2 Project file authority', () => {
4848
)
4949
if (!principal) throw new Error('The admitted Project file request lost its principal')
5050
expect(principal).toMatchObject({
51-
kind: 'resource_delegated',
51+
kind: 'delegated',
5252
subjectUserId: 'actor',
5353
invocation: { kind: 'chat', chatId: 'private-chat' },
5454
scope: { kind: 'entity', entityType: 'project', entityId: 'project', fileId: 'file' },

‎apps/sim/lib/api/server/routes/copilot-request.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DelegatedPrincipal, ResourceDelegatedPrincipal } from '@sim/auth/principal'
1+
import type { ResourceScopedDelegation, WorkspaceDelegatedPrincipal } from '@sim/auth/principal'
22
import { noteContentDeliveryOperation } from '@/lib/core/application'
33
import { markCopilotWorkspaceInvocation } from '@/lib/core/application/copilot-workspace-invocation'
44
import type { ApplicationOperation, OperationUseCase } from '@/lib/core/application/operation'
@@ -15,7 +15,7 @@ import {
1515
type CopilotResourceRequestGrant = (
1616
operation: ApplicationOperation,
1717
audience: string
18-
) => ResourceDelegatedPrincipal | undefined
18+
) => ResourceScopedDelegation | undefined
1919

2020
type CopilotRequestInvocation =
2121
| { kind: 'resource'; grant: CopilotResourceRequestGrant }
@@ -71,7 +71,7 @@ export function copilotRequestPrincipal(
7171
request: Request,
7272
operation: ApplicationOperation,
7373
useCase?: CopilotRouteUseCase
74-
): DelegatedPrincipal | ResourceDelegatedPrincipal | undefined {
74+
): WorkspaceDelegatedPrincipal | ResourceScopedDelegation | undefined {
7575
const invocation = INVOCATIONS.get(request)
7676
if (!invocation) return undefined
7777
const audience = copilotRouteAudience(operation, useCase)

‎apps/sim/lib/api/server/routes/internal-json-route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
2-
type DelegatedPrincipal,
32
describePrincipalAuth,
43
type Principal,
54
resolvePrincipalSubjectUserId,
65
type SessionPrincipal,
76
type WorkflowExecutionDelegatedPrincipal,
7+
type WorkspaceDelegatedPrincipal,
88
} from '@sim/auth/principal'
99
import { setRequestAuth } from '@sim/logger'
1010
import type { NextRequest } from 'next/server'
@@ -66,7 +66,7 @@ interface InternalSessionOrExecutorAuthOptions {
6666
audience: string
6767
resourceScope?(
6868
params: Record<string, string | string[] | undefined>
69-
): DelegatedPrincipal['resourceScope']
69+
): WorkspaceDelegatedPrincipal['resourceScope']
7070
}
7171

7272
export function createInternalSessionOrExecutorAuth(

‎apps/sim/lib/auth/internal-delegation.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {
22
BoundWorkflowExecutionDelegatedPrincipal,
3-
DelegatedPrincipal,
3+
WorkspaceDelegatedPrincipal,
44
} from '@sim/auth/principal'
55
import type { VerifiedInternalDelegation } from '@/lib/auth/internal'
66
import { asOrchestrationError } from '@/lib/core/orchestration/types'
@@ -15,7 +15,7 @@ import {
1515

1616
export interface BindInternalExecutorDelegationOptions {
1717
audience: string
18-
resourceScope?: DelegatedPrincipal['resourceScope']
18+
resourceScope?: WorkspaceDelegatedPrincipal['resourceScope']
1919
compatibilityActorUserId?: string
2020
}
2121

‎apps/sim/lib/auth/realtime-file-delegation.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ResourceDelegatedPrincipal } from '@sim/auth/principal'
1+
import type { ResourceScopedDelegation } from '@sim/auth/principal'
22
import { FILE_DOC_INTERNAL_HEADERS } from '@sim/realtime-protocol/file-doc'
33
import { generateId } from '@sim/utils/id'
44
import {
@@ -9,7 +9,7 @@ import { checkInternalApiKey } from '@/lib/mothership/request/http'
99
import { PROJECT_FILE_DELEGATION_TTL_MS } from '@/lib/projects/files/application/operations'
1010

1111
/** Internal relay requests carry the authenticated socket actor, bounded to exactly one Project file. */
12-
export const realtimeProjectFileAuth: InternalAuthPolicy<ResourceDelegatedPrincipal> = {
12+
export const realtimeProjectFileAuth: InternalAuthPolicy<ResourceScopedDelegation> = {
1313
async authenticate(request, params) {
1414
if (!checkInternalApiKey(request).success) throw new InternalUnauthenticatedError()
1515
const subjectUserId = request.headers.get(FILE_DOC_INTERNAL_HEADERS.userId)
@@ -28,7 +28,7 @@ export const realtimeProjectFileAuth: InternalAuthPolicy<ResourceDelegatedPrinci
2828
}
2929
const now = Date.now()
3030
return {
31-
kind: 'resource_delegated',
31+
kind: 'delegated',
3232
serviceId: 'realtime',
3333
subjectUserId,
3434
invocation: { kind: 'realtime', connectionId },

‎apps/sim/lib/auth/realtime-file-list-delegation.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ResourceDelegatedPrincipal } from '@sim/auth/principal'
1+
import type { ResourceScopedDelegation } from '@sim/auth/principal'
22
import { FILE_DOC_INTERNAL_HEADERS } from '@sim/realtime-protocol/file-doc'
33
import { generateId } from '@sim/utils/id'
44
import {
@@ -9,7 +9,7 @@ import { checkInternalApiKey } from '@/lib/mothership/request/http'
99
import { PROJECT_FILE_DELEGATION_TTL_MS } from '@/lib/projects/files/application/operations'
1010

1111
/** The relay attests the socket actor and connection, bounded to observing one Project collection. */
12-
export const realtimeProjectFileListAuth: InternalAuthPolicy<ResourceDelegatedPrincipal> = {
12+
export const realtimeProjectFileListAuth: InternalAuthPolicy<ResourceScopedDelegation> = {
1313
async authenticate(request, params) {
1414
if (!checkInternalApiKey(request).success) throw new InternalUnauthenticatedError()
1515
const subjectUserId = request.headers.get(FILE_DOC_INTERNAL_HEADERS.userId)
@@ -25,7 +25,7 @@ export const realtimeProjectFileListAuth: InternalAuthPolicy<ResourceDelegatedPr
2525
}
2626
const now = Date.now()
2727
return {
28-
kind: 'resource_delegated',
28+
kind: 'delegated',
2929
serviceId: 'realtime',
3030
subjectUserId,
3131
invocation: { kind: 'realtime', connectionId },

‎apps/sim/lib/core/application/audit-source.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Principal } from '@sim/auth/principal'
1+
import { type Principal, toPrincipalActor } from '@sim/auth/principal'
22

33
/**
44
* The surface a semantic audit row attributes a change to.
@@ -13,5 +13,6 @@ import type { Principal } from '@sim/auth/principal'
1313
* names its own credential class, which is already what `actor` records.
1414
*/
1515
export function principalAuditSource(principal: Principal): string {
16-
return principal.kind === 'delegated' ? principal.serviceId : principal.kind
16+
const actor = toPrincipalActor(principal)
17+
return actor.kind === 'delegated' ? actor.serviceId : actor.kind
1718
}

‎apps/sim/lib/core/application/authorized-workspace-use-case.test.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {
2-
DelegatedPrincipal,
32
SessionPrincipal,
43
WorkspaceApiKeyPrincipal,
4+
WorkspaceDelegatedPrincipal,
55
} from '@sim/auth/principal'
66
import {
77
createPersonalApiKeyPrincipal,
@@ -317,15 +317,15 @@ describe('defineAuthorizedWorkspaceUseCase', () => {
317317

318318
it('resolves domain-specific delegation options against canonical context', async () => {
319319
const scopeCheck = vi.fn(
320-
(principal: DelegatedPrincipal, context: TestContext) =>
320+
(principal: WorkspaceDelegatedPrincipal, context: TestContext) =>
321321
principal.resourceScope?.fileId === context.canonicalResourceId
322322
)
323323
const useCase = defineAuthorizedWorkspaceUseCase({
324324
operation: delegatedOperation,
325-
resolveContext: async (_args: { principal: DelegatedPrincipal; input: TestInput }) =>
325+
resolveContext: async (_args: { principal: WorkspaceDelegatedPrincipal; input: TestInput }) =>
326326
canonicalContext,
327327
authorizationOptions: ({ principal }) => {
328-
expectTypeOf(principal).toMatchTypeOf<DelegatedPrincipal>()
328+
expectTypeOf(principal).toMatchTypeOf<WorkspaceDelegatedPrincipal>()
329329
expectTypeOf(principal.serviceId).toEqualTypeOf<'executor'>()
330330
return {
331331
delegation: {
@@ -335,12 +335,12 @@ describe('defineAuthorizedWorkspaceUseCase', () => {
335335
}
336336
},
337337
async execute({ principal }) {
338-
expectTypeOf(principal).toMatchTypeOf<DelegatedPrincipal>()
338+
expectTypeOf(principal).toMatchTypeOf<WorkspaceDelegatedPrincipal>()
339339
expectTypeOf(principal.serviceId).toEqualTypeOf<'executor'>()
340340
return { ok: true as const }
341341
},
342342
})
343-
const principal: DelegatedPrincipal = {
343+
const principal: WorkspaceDelegatedPrincipal = {
344344
kind: 'delegated',
345345
serviceId: 'executor',
346346
subjectUserId: 'user-1',
@@ -367,7 +367,8 @@ describe('defineAuthorizedWorkspaceUseCase', () => {
367367

368368
it('rejects a disallowed delegated service before canonical loading', async () => {
369369
const resolveContext = vi.fn(
370-
async (_args: { principal: DelegatedPrincipal; input: TestInput }) => canonicalContext
370+
async (_args: { principal: WorkspaceDelegatedPrincipal; input: TestInput }) =>
371+
canonicalContext
371372
)
372373
const useCase = defineAuthorizedWorkspaceUseCase({
373374
operation: delegatedOperation,

0 commit comments

Comments
 (0)