| ... | ... | @@ -40,15 +40,54 @@ import { FetchHttpClient, HttpClient } from "effect/unstable/http"; |
| 40 | 40 | import * as EnvironmentLinks from "./EnvironmentLinks.ts"; |
| 41 | 41 | import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; |
| 42 | 42 | import * as RelayConfiguration from "../Config.ts"; |
| 43 | import { isManagedEndpointHostname } from "../deploymentConfig.ts"; |
| 44 | |
| 45 | export const EnvironmentConnectNotAuthorizedReason = Schema.Literals([ |
| 46 | "client_proof_key_thumbprint_missing", |
| 47 | "environment_link_not_found", |
| 48 | "endpoint_provider_not_managed", |
| 49 | "managed_endpoint_allocation_not_found", |
| 50 | "managed_endpoint_base_domain_not_configured", |
| 51 | "managed_endpoint_allocation_not_ready", |
| 52 | "managed_endpoint_hostname_invalid", |
| 53 | "managed_endpoint_mismatch", |
| 54 | ]); |
| 55 | export type EnvironmentConnectNotAuthorizedReason = |
| 56 | typeof EnvironmentConnectNotAuthorizedReason.Type; |
| 57 | |
| 58 | function environmentConnectNotAuthorizedReasonMessage( |
| 59 | reason: EnvironmentConnectNotAuthorizedReason, |
| 60 | ): string { |
| 61 | switch (reason) { |
| 62 | case "client_proof_key_thumbprint_missing": |
| 63 | return "the client proof key thumbprint is missing"; |
| 64 | case "environment_link_not_found": |
| 65 | return "no active environment link was found"; |
| 66 | case "endpoint_provider_not_managed": |
| 67 | return "the linked endpoint is not relay-managed"; |
| 68 | case "managed_endpoint_allocation_not_found": |
| 69 | return "no managed endpoint allocation was found"; |
| 70 | case "managed_endpoint_base_domain_not_configured": |
| 71 | return "the managed endpoint base domain is not configured"; |
| 72 | case "managed_endpoint_allocation_not_ready": |
| 73 | return "the managed endpoint allocation is incomplete"; |
| 74 | case "managed_endpoint_hostname_invalid": |
| 75 | return "the managed endpoint hostname is invalid"; |
| 76 | case "managed_endpoint_mismatch": |
| 77 | return "the linked endpoint does not match its managed allocation"; |
| 78 | } |
| 79 | } |
| 43 | 80 | |
| 44 | 81 | export class EnvironmentConnectNotAuthorized extends Schema.TaggedErrorClass<EnvironmentConnectNotAuthorized>()( |
| 45 | 82 | "EnvironmentConnectNotAuthorized", |
| 46 | 83 | { |
| 47 | 84 | environmentId: Schema.String, |
| 85 | operation: Schema.Literals(["connect", "status"]), |
| 86 | reason: EnvironmentConnectNotAuthorizedReason, |
| 48 | 87 | }, |
| 49 | 88 | ) { |
| 50 | 89 | override get message(): string { |
| 51 | | return `Environment '${this.environmentId}' is not authorized to connect`; |
| 90 | return `Environment '${this.environmentId}' is not authorized for ${this.operation}: ${environmentConnectNotAuthorizedReasonMessage(this.reason)}`; |
| 52 | 91 | } |
| 53 | 92 | } |
| 54 | 93 | |
| ... | ... | @@ -257,30 +296,91 @@ const make = Effect.gen(function* () { |
| 257 | 296 | const resolveManagedEndpoint = Effect.fn("relay.environment_connector.resolve_managed_endpoint")( |
| 258 | 297 | function* (input: { |
| 259 | 298 | readonly userId: string; |
| 299 | readonly operation: "connect" | "status"; |
| 260 | 300 | readonly link: EnvironmentLinks.RelayLinkedEnvironmentRecord; |
| 261 | 301 | }) { |
| 262 | 302 | if (input.link.endpoint.providerKind !== "cloudflare_tunnel") { |
| 303 | yield* Effect.annotateCurrentSpan({ |
| 304 | "relay.authorization.endpoint_provider_kind": input.link.endpoint.providerKind, |
| 305 | }); |
| 263 | 306 | return yield* new EnvironmentConnectNotAuthorized({ |
| 264 | 307 | environmentId: input.link.environmentId, |
| 308 | operation: input.operation, |
| 309 | reason: "endpoint_provider_not_managed", |
| 265 | 310 | }); |
| 266 | 311 | } |
| 267 | 312 | const allocation = yield* allocations.get({ |
| 268 | 313 | userId: input.userId, |
| 269 | 314 | environmentId: input.link.environmentId, |
| 270 | 315 | }); |
| 271 | | const endpoint = allocation |
| 272 | | ? ManagedEndpointAllocations.resolveReadyManagedEndpoint({ |
| 273 | | allocation, |
| 274 | | baseDomain: settings.managedEndpointBaseDomain, |
| 275 | | }) |
| 276 | | : null; |
| 316 | if (!allocation) { |
| 317 | return yield* new EnvironmentConnectNotAuthorized({ |
| 318 | environmentId: input.link.environmentId, |
| 319 | operation: input.operation, |
| 320 | reason: "managed_endpoint_allocation_not_found", |
| 321 | }); |
| 322 | } |
| 323 | const allocationAttributes = { |
| 324 | "relay.authorization.allocation_hostname": allocation.hostname, |
| 325 | "relay.authorization.allocation_has_ready_at": allocation.readyAt !== null, |
| 326 | "relay.authorization.allocation_has_tunnel_id": allocation.tunnelId !== null, |
| 327 | "relay.authorization.allocation_has_dns_record_id": allocation.dnsRecordId !== null, |
| 328 | } as const; |
| 329 | if (!settings.managedEndpointBaseDomain) { |
| 330 | yield* Effect.annotateCurrentSpan(allocationAttributes); |
| 331 | return yield* new EnvironmentConnectNotAuthorized({ |
| 332 | environmentId: input.link.environmentId, |
| 333 | operation: input.operation, |
| 334 | reason: "managed_endpoint_base_domain_not_configured", |
| 335 | }); |
| 336 | } |
| 337 | if ( |
| 338 | allocation.readyAt === null || |
| 339 | allocation.tunnelId === null || |
| 340 | allocation.dnsRecordId === null |
| 341 | ) { |
| 342 | yield* Effect.annotateCurrentSpan(allocationAttributes); |
| 343 | return yield* new EnvironmentConnectNotAuthorized({ |
| 344 | environmentId: input.link.environmentId, |
| 345 | operation: input.operation, |
| 346 | reason: "managed_endpoint_allocation_not_ready", |
| 347 | }); |
| 348 | } |
| 349 | if (!isManagedEndpointHostname(allocation.hostname, settings.managedEndpointBaseDomain)) { |
| 350 | yield* Effect.annotateCurrentSpan({ |
| 351 | ...allocationAttributes, |
| 352 | "relay.authorization.managed_endpoint_base_domain": settings.managedEndpointBaseDomain, |
| 353 | }); |
| 354 | return yield* new EnvironmentConnectNotAuthorized({ |
| 355 | environmentId: input.link.environmentId, |
| 356 | operation: input.operation, |
| 357 | reason: "managed_endpoint_hostname_invalid", |
| 358 | }); |
| 359 | } |
| 360 | const endpoint = ManagedEndpointAllocations.resolveReadyManagedEndpoint({ |
| 361 | allocation, |
| 362 | baseDomain: settings.managedEndpointBaseDomain, |
| 363 | }); |
| 277 | 364 | if ( |
| 278 | 365 | endpoint === null || |
| 279 | 366 | endpoint.httpBaseUrl !== input.link.endpoint.httpBaseUrl || |
| 280 | 367 | endpoint.wsBaseUrl !== input.link.endpoint.wsBaseUrl |
| 281 | 368 | ) { |
| 369 | yield* Effect.annotateCurrentSpan({ |
| 370 | ...allocationAttributes, |
| 371 | "relay.authorization.linked_http_base_url": input.link.endpoint.httpBaseUrl, |
| 372 | "relay.authorization.linked_ws_base_url": input.link.endpoint.wsBaseUrl, |
| 373 | ...(endpoint |
| 374 | ? { |
| 375 | "relay.authorization.resolved_http_base_url": endpoint.httpBaseUrl, |
| 376 | "relay.authorization.resolved_ws_base_url": endpoint.wsBaseUrl, |
| 377 | } |
| 378 | : {}), |
| 379 | }); |
| 282 | 380 | return yield* new EnvironmentConnectNotAuthorized({ |
| 283 | 381 | environmentId: input.link.environmentId, |
| 382 | operation: input.operation, |
| 383 | reason: "managed_endpoint_mismatch", |
| 284 | 384 | }); |
| 285 | 385 | } |
| 286 | 386 | return endpoint; |
| ... | ... | @@ -295,9 +395,17 @@ const make = Effect.gen(function* () { |
| 295 | 395 | }); |
| 296 | 396 | const link = yield* links.getForUser(input); |
| 297 | 397 | if (!link) { |
| 298 | | return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.environmentId }); |
| 398 | return yield* new EnvironmentConnectNotAuthorized({ |
| 399 | environmentId: input.environmentId, |
| 400 | operation: "status", |
| 401 | reason: "environment_link_not_found", |
| 402 | }); |
| 299 | 403 | } |
| 300 | | const endpoint = yield* resolveManagedEndpoint({ userId: input.userId, link }); |
| 404 | const endpoint = yield* resolveManagedEndpoint({ |
| 405 | userId: input.userId, |
| 406 | operation: "status", |
| 407 | link, |
| 408 | }); |
| 301 | 409 | const now = yield* DateTime.now; |
| 302 | 410 | const expiresAt = DateTime.add(now, { minutes: 2 }); |
| 303 | 411 | const nonce = yield* crypto.randomUUIDv4.pipe( |
| ... | ... | @@ -404,13 +512,25 @@ const make = Effect.gen(function* () { |
| 404 | 512 | ...(input.deviceId ? { "relay.mobile.device_id": input.deviceId } : {}), |
| 405 | 513 | }); |
| 406 | 514 | if (input.clientProofKeyThumbprint.trim().length === 0) { |
| 407 | | return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.environmentId }); |
| 515 | return yield* new EnvironmentConnectNotAuthorized({ |
| 516 | environmentId: input.environmentId, |
| 517 | operation: "connect", |
| 518 | reason: "client_proof_key_thumbprint_missing", |
| 519 | }); |
| 408 | 520 | } |
| 409 | 521 | const link = yield* links.getForUser(input); |
| 410 | 522 | if (!link) { |
| 411 | | return yield* new EnvironmentConnectNotAuthorized({ environmentId: input.environmentId }); |
| 523 | return yield* new EnvironmentConnectNotAuthorized({ |
| 524 | environmentId: input.environmentId, |
| 525 | operation: "connect", |
| 526 | reason: "environment_link_not_found", |
| 527 | }); |
| 412 | 528 | } |
| 413 | | const endpoint = yield* resolveManagedEndpoint({ userId: input.userId, link }); |
| 529 | const endpoint = yield* resolveManagedEndpoint({ |
| 530 | userId: input.userId, |
| 531 | operation: "connect", |
| 532 | link, |
| 533 | }); |
| 414 | 534 | const now = yield* DateTime.now; |
| 415 | 535 | const expiresAt = DateTime.add(now, { minutes: 2 }); |
| 416 | 536 | const nonce = yield* crypto.randomUUIDv4.pipe( |