Skip to content

Commit 3e476e7

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(oci): bind requests to authorized credentials
1 parent c9eecff commit 3e476e7

11 files changed

Lines changed: 1110 additions & 742 deletions

File tree

‎apps/sim/lib/internal/oci/client.server.ts‎

Lines changed: 869 additions & 148 deletions
Large diffs are not rendered by default.

‎apps/sim/lib/internal/oci/endpoints.ts‎

Lines changed: 137 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isIpLiteral, unwrapIpv6Brackets } from '@sim/security/ssrf'
2+
import type { OAuthService } from '@/lib/oauth/types'
23

34
export type OciDestinationProvenance = 'static' | 'authenticated-discovery'
45

@@ -12,26 +13,43 @@ export interface OciRegion {
1213
readonly realm: OciRealm
1314
}
1415

15-
declare const validatedOciDestinationBrand: unique symbol
16+
declare const preparedOciEndpointBrand: unique symbol
1617

17-
/** An OCI origin that passed both structural and service-owned hostname validation. */
18-
export interface ValidatedOciDestination {
18+
/** An OCI endpoint prepared from a declarative product policy. */
19+
export interface OciPreparedEndpoint {
1920
readonly origin: string
2021
readonly hostname: string
21-
readonly service: string
22+
readonly serviceId: OAuthService
23+
readonly serviceName: string
2224
readonly region: OciRegion
2325
readonly provenance: OciDestinationProvenance
24-
readonly [validatedOciDestinationBrand]: true
26+
readonly [preparedOciEndpointBrand]: true
2527
}
2628

27-
declare const ociServiceHostnamePredicateBrand: unique symbol
29+
declare const ociEndpointPolicyBrand: unique symbol
2830

29-
export type OciServiceHostnamePredicate = ((params: {
30-
hostname: string
31-
service: string
32-
region: OciRegion
33-
provenance: OciDestinationProvenance
34-
}) => boolean) & { readonly [ociServiceHostnamePredicateBrand]: true }
31+
export interface OciStaticEndpointPolicy {
32+
readonly kind: 'static'
33+
readonly serviceId: OAuthService
34+
readonly serviceName: string
35+
readonly [ociEndpointPolicyBrand]: true
36+
}
37+
38+
export type OciDiscoverySource =
39+
| { readonly kind: 'header'; readonly name: string }
40+
| { readonly kind: 'json'; readonly path: readonly string[] }
41+
42+
export interface OciDiscoveredEndpointPolicy {
43+
readonly kind: 'authenticated-discovery'
44+
readonly serviceId: OAuthService
45+
readonly serviceName: string
46+
readonly responsePolicy: OciEndpointPolicy
47+
readonly source: OciDiscoverySource
48+
readonly allowRegionalHost: boolean
49+
readonly [ociEndpointPolicyBrand]: true
50+
}
51+
52+
export type OciEndpointPolicy = OciStaticEndpointPolicy | OciDiscoveredEndpointPolicy
3553

3654
/**
3755
* Realm and region snapshot copied from `oci-common@2.140.0` files
@@ -179,17 +197,83 @@ export function resolveEffectiveOciRegion(defaultRegion: string, override?: stri
179197
return effective
180198
}
181199

182-
export function objectStorageOciHostname(region: OciRegion): string {
183-
return `objectstorage.${region.id}.${region.realm.domain}`
200+
function assertServiceName(value: string): void {
201+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(value)) {
202+
throw new Error('OCI endpoint policy service name is invalid')
203+
}
204+
}
205+
206+
function assertDiscoverySource(source: OciDiscoverySource): void {
207+
if (source.kind === 'header') {
208+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(source.name)) {
209+
throw new Error('OCI discovery header name is invalid')
210+
}
211+
return
212+
}
213+
if (
214+
source.kind !== 'json' ||
215+
source.path.length === 0 ||
216+
source.path.length > 8 ||
217+
source.path.some(
218+
(segment) =>
219+
segment.length === 0 || segment.length > 128 || /[\u0000-\u001f\u007f]/.test(segment)
220+
)
221+
) {
222+
throw new Error('OCI discovery JSON path is invalid')
223+
}
224+
}
225+
226+
/** Creates a frozen exact regional-host policy owned by one registered service. */
227+
export function createOciStaticEndpointPolicy(params: {
228+
serviceId: OAuthService
229+
serviceName: string
230+
}): OciStaticEndpointPolicy {
231+
assertServiceName(params.serviceName)
232+
return Object.freeze({
233+
kind: 'static',
234+
serviceId: params.serviceId,
235+
serviceName: params.serviceName,
236+
}) as OciStaticEndpointPolicy
237+
}
238+
239+
/** Creates a frozen authenticated-discovery policy without executable hostname callbacks. */
240+
export function createOciDiscoveredEndpointPolicy(params: {
241+
serviceId: OAuthService
242+
serviceName: string
243+
responsePolicy: OciEndpointPolicy
244+
source: OciDiscoverySource
245+
allowRegionalHost?: boolean
246+
}): OciDiscoveredEndpointPolicy {
247+
assertServiceName(params.serviceName)
248+
assertDiscoverySource(params.source)
249+
if (params.responsePolicy.serviceId !== params.serviceId) {
250+
throw new Error('OCI discovery source policy must have the same owning service')
251+
}
252+
const source =
253+
params.source.kind === 'json'
254+
? Object.freeze({ ...params.source, path: Object.freeze([...params.source.path]) })
255+
: Object.freeze({ ...params.source, name: params.source.name.toLowerCase() })
256+
return Object.freeze({
257+
kind: 'authenticated-discovery',
258+
serviceId: params.serviceId,
259+
serviceName: params.serviceName,
260+
responsePolicy: params.responsePolicy,
261+
source,
262+
allowRegionalHost: params.allowRegionalHost ?? false,
263+
}) as OciDiscoveredEndpointPolicy
264+
}
265+
266+
export function regionalOciHostname(serviceName: string, region: OciRegion): string {
267+
assertServiceName(serviceName)
268+
return `${serviceName}.${region.id}.${region.realm.domain}`
184269
}
185270

186-
export function validateOciDestination(params: {
271+
function validateOciOrigin(params: {
187272
origin: string
188-
service: string
273+
policy: OciEndpointPolicy
189274
region: OciRegion
190275
provenance: OciDestinationProvenance
191-
isServiceHostname: OciServiceHostnamePredicate
192-
}): ValidatedOciDestination {
276+
}): OciPreparedEndpoint {
193277
const knownRegion = getOciRegion(params.region.id)
194278
if (
195279
knownRegion.realm.id !== params.region.realm.id ||
@@ -204,8 +288,7 @@ export function validateOciDestination(params: {
204288
throw new Error('OCI destination must be a valid HTTPS origin')
205289
}
206290
if (
207-
(params.provenance !== 'static' && params.provenance !== 'authenticated-discovery') ||
208-
!/^[a-z][a-z0-9-]{0,62}$/.test(params.service) ||
291+
params.policy.kind !== params.provenance ||
209292
url.protocol !== 'https:' ||
210293
url.port !== '' ||
211294
url.username !== '' ||
@@ -218,39 +301,51 @@ export function validateOciDestination(params: {
218301
) {
219302
throw new Error('OCI destination must be an exact HTTPS origin with the default port')
220303
}
221-
if (
222-
!params.isServiceHostname({
223-
hostname: url.hostname,
224-
service: params.service,
225-
region: knownRegion,
226-
provenance: params.provenance,
227-
})
228-
) {
304+
const regionalHostname = regionalOciHostname(params.policy.serviceName, knownRegion)
305+
const hostnameMatches =
306+
params.provenance === 'static'
307+
? url.hostname === regionalHostname
308+
: url.hostname.endsWith(`.${regionalHostname}`) ||
309+
(params.policy.kind === 'authenticated-discovery' &&
310+
params.policy.allowRegionalHost &&
311+
url.hostname === regionalHostname)
312+
if (!hostnameMatches) {
229313
throw new Error('OCI destination hostname is not owned by the requested service')
230314
}
231315
return {
232316
origin: url.origin,
233317
hostname: url.hostname,
234-
service: params.service,
318+
serviceId: params.policy.serviceId,
319+
serviceName: params.policy.serviceName,
235320
region: knownRegion,
236321
provenance: params.provenance,
237-
} as ValidatedOciDestination
322+
} as OciPreparedEndpoint
238323
}
239324

240-
export const isObjectStorageOciHostname = (({ hostname, service, region }) =>
241-
service === 'objectstorage' &&
242-
hostname === objectStorageOciHostname(region)) as OciServiceHostnamePredicate
325+
/** Resolves a static policy exclusively from its service and validated region. */
326+
export function resolveStaticOciEndpoint(
327+
policy: OciStaticEndpointPolicy,
328+
region: OciRegion
329+
): OciPreparedEndpoint {
330+
const hostname = regionalOciHostname(policy.serviceName, region)
331+
return validateOciOrigin({
332+
origin: `https://${hostname}`,
333+
policy,
334+
region,
335+
provenance: 'static',
336+
})
337+
}
243338

244-
export function objectStorageOciDestination(
339+
/** Structurally validates an origin extracted from an authenticated response. */
340+
export function resolveDiscoveredOciEndpoint(
341+
policy: OciDiscoveredEndpointPolicy,
245342
region: OciRegion,
246-
provenance: OciDestinationProvenance = 'static'
247-
): ValidatedOciDestination {
248-
const hostname = objectStorageOciHostname(region)
249-
return validateOciDestination({
250-
origin: `https://${hostname}`,
251-
service: 'objectstorage',
343+
origin: string
344+
): OciPreparedEndpoint {
345+
return validateOciOrigin({
346+
origin,
347+
policy,
252348
region,
253-
provenance,
254-
isServiceHostname: isObjectStorageOciHostname,
349+
provenance: 'authenticated-discovery',
255350
})
256351
}

0 commit comments

Comments
 (0)