import { ApiException } from "@components/api-exceptions"
import type { FlowcoreAuthenticatedUser } from "@components/jwks-guard"
import type { Logger } from "@components/logger"
import type * as xxHash from "@jabr/xxhash64"
import { type Static, Type } from "@sinclair/typebox"
import type { Redis } from "ioredis"
* Represents an API forbidden exception. This exception is thrown when the server refuses to authorize the request.
* @property {number} status - The HTTP status code for the exception, which is 403.
* @property {string} code - The error code for the exception, which is "FORBIDDEN".
export class IamForbiddenException extends ApiException {
public code = "FORBIDDEN"
public readonly validPolicies?: { policyFrn: string; statementId: string }[],
public readonly invalidRequest?: RequestedAccess,
* Defines the structure of requested access for IAM validation.
* @property {action} - The action or actions to validate against.
* @property {resource} - The resource to validate, in order of granularity. First match wins.
export const requestedAccess = Type.Array(
action: Type.Union([Type.String(), Type.Array(Type.String())], {
description: "The action or actions to validate against",
examples: ["read", "write", "fetch", "ingest"],
resource: Type.Array(Type.String({ pattern: "^frn::" }), {
description: "The resource to validate, in order of granularity. First match wins.",
"frn::tenant:data-core/24005dde-7476-4692-aec4-175d2e5a9b9e",
"frn::tenant:flow-type/6a394228-e1a1-4491-b280-0117f481be32",
"frn::tenant:event-type/ffff7fe3-45c7-4a96-afe2-2519ee342d69",
"frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda",
"frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda:data-core/24005dde-7476-4692-aec4-175d2e5a9b9e",
"frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda:flow-type/6a394228-e1a1-4491-b280-0117f481be32",
"frn::13ea0ce0-2f5c-46c4-b4f2-e0e15c5d1cda:event-type/ffff7fe3-45c7-4a96-afe2-2519ee342d69",
* Represents the type of requested access for IAM validation.
export type RequestedAccess = Static<typeof requestedAccess>
* Represents the response from IAM validation.
* @property {valid} - Indicates if the validation was successful.
* @property {validPolicies} - The policies that were validated.
* @property {checksum} - The checksum of the validation request.
* @property {invalidRequest} - The invalid request details if validation failed.
export type IamValidationResponse =
validPolicies: { policyFrn: string; statementId: string }[]
invalidRequest: RequestedAccess
validPolicies: { policyFrn: string; statementId: string }[]
export class IamValidateBuilder {
private redisClient: Redis | undefined
private hash!: xxHash.Hasher
private type: "users" | "keys" = "users"
private mode: "tenant" | "organization" = "tenant"
private hashMap: Map<string, boolean> = new Map()
private logger: Logger | undefined
constructor(private readonly iamApiUrl: string) {}
* Sets the Redis client for caching.
* @param {Redis} redisClient - The Redis client instance.
* @returns {IamValidateBuilder} - This instance for method chaining.
public withRedisClient(redisClient: Redis): IamValidateBuilder {
this.redisClient = redisClient
* Sets the hash function for generating checksums.
* @param {xxHash.Hasher} hash - The hash function instance.
* @returns {IamValidateBuilder} - This instance for method chaining.
public withHash(hash: xxHash.Hasher): IamValidateBuilder {
* Sets the type of validation to perform.
* @param {"users" | "keys"} type - The type of validation.
* @returns {IamValidateBuilder} - This instance for method chaining.
public withType(type: "users" | "keys"): IamValidateBuilder {
* Sets the mode of validation to perform.
* @param {"tenant" | "organization"} mode - The mode of validation.
* @returns {IamValidateBuilder} - This instance for method chaining.
public withMode(mode: "tenant" | "organization"): IamValidateBuilder {
* Sets the logger for logging validation events.
* @param {Logger} logger - The logger instance.
* @returns {IamValidateBuilder} - This instance for method chaining.
public withLogger(logger: Logger): IamValidateBuilder {
* Builds the IAM validation process.
* @param {RequestedAccess} requestedAccess - The requested access details.
* @returns {(flowcoreUser: FlowcoreAuthenticatedUser) => Promise<void>} - A promise that resolves to void.
requestedAccess: RequestedAccess,
): (options: { flowcoreUser: FlowcoreAuthenticatedUser }) => Promise<void> {
const checksum = this.hash.hash(JSON.stringify(requestedAccess), "hex").toString()
return async ({ flowcoreUser }: { flowcoreUser: FlowcoreAuthenticatedUser }): Promise<void> => {
const { id } = flowcoreUser
const localChecksum = this.hashMap.has(`${id}-${checksum}`)
const cacheKey = `iam:${this.type}-validation:${checksum}:${id}`
const cached = await this.redisClient.get(cacheKey)
const validation = await fetch(`${this.iamApiUrl}/api/v1/validate/${this.type}/${id}`, {
"Content-Type": "application/json",
const data = (await validation.json()) as IamValidationResponse
this.hashMap.set(`${id}-${data.checksum}`, true)
this.logger?.debug(`IAM validation passed for ${this.type} ${id} with checksum ${data.checksum}`)
this.logger?.error("IAM validation failed", data)
throw new IamForbiddenException("IAM validation failed", data.validPolicies, data.invalidRequest)
* Enum for defining the types of FRNs.
ORGANIZATION = "organization",
EVENT_TYPE = "event-type",
* Enum for defining the actions that can be performed on FRNs.
* Builds a fully qualified FRN string.
* @param {string} tenantOrOrganizationId - The tenant or organization ID.
* @param {FrnType} type - The type of FRN.
* @param {string} [id] - The ID of the FRN.
* @returns {string} - The fully qualified FRN string.
export const buildFrn = (tenantOrOrganizationId: string, type: FrnType, id?: string): string => {
return `frn::${tenantOrOrganizationId}:${type}${id ? `/${id}` : ""}`
* Builds a list of fully qualified FRN strings.
* @param {{ type: FrnType; id?: string }[]} items - The list of items to build FRNs for.
* @param {string} tenantOrOrganizationId - The tenant or organization ID.
* @returns {string[]} - A list of fully qualified FRN strings.
export const buildListFrn = (items: { type: FrnType; id?: string }[], tenantOrOrganizationId: string): string[] => {
buildFrn(tenantOrOrganizationId, FrnType.ALL),
...items.map((item) => buildFrn(tenantOrOrganizationId, item.type, item.id)),