Files
floci-ui/packages/api/src/aws.ts
T
Zakaria 4156afccfe feat(api): add Cognito user pools to the cloud explorer
Adds a userpools catalog entry, schema and AwsUserPoolsAdapter, verified
against Floci core:

- list/get/create/delete user pools; create can set email or username
  sign-in, a relaxed password policy for local test users, and a public
  app client (on by default) whose id is surfaced in the pool metadata
- users as the pool's child items: list (paged), get, create and delete;
  passwords are set permanent by default so users can sign in at once
- map Cognito's HTTP-400 error names (UserNotFoundException,
  UsernameExistsException, AliasExistsException, InvalidPasswordException)
- link Floci's cognito-idp service to the new page on the diagnostics page
2026-10-23 12:12:40 -04:00

181 lines
7.1 KiB
TypeScript

import { S3Client } from "@aws-sdk/client-s3";
import { LambdaClient } from "@aws-sdk/client-lambda";
import { EKSClient } from "@aws-sdk/client-eks";
import { EC2Client } from "@aws-sdk/client-ec2";
import { RDSClient } from "@aws-sdk/client-rds";
import { RDSDataClient } from "@aws-sdk/client-rds-data";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
import { SSMClient } from "@aws-sdk/client-ssm";
import { KMSClient } from "@aws-sdk/client-kms";
import { SFNClient } from "@aws-sdk/client-sfn";
import { ElasticLoadBalancingV2Client } from "@aws-sdk/client-elastic-load-balancing-v2";
import { EventBridgeClient } from "@aws-sdk/client-eventbridge";
import { SQSClient } from "@aws-sdk/client-sqs";
import { IAMClient } from "@aws-sdk/client-iam";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { APIGatewayClient } from "@aws-sdk/client-api-gateway";
import { CloudFormationClient } from "@aws-sdk/client-cloudformation";
import { AppConfigClient } from "@aws-sdk/client-appconfig";
import { SNSClient } from "@aws-sdk/client-sns";
import { ECRClient } from "@aws-sdk/client-ecr";
import { CodeBuildClient } from "@aws-sdk/client-codebuild";
import { CodePipelineClient } from "@aws-sdk/client-codepipeline";
import { CodeDeployClient } from "@aws-sdk/client-codedeploy";
import { ECSClient } from "@aws-sdk/client-ecs";
import { KafkaClient } from "@aws-sdk/client-kafka";
import { KafkaConnectClient } from "@aws-sdk/client-kafkaconnect";
import { KinesisClient } from "@aws-sdk/client-kinesis";
import { SESv2Client } from "@aws-sdk/client-sesv2";
import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
import { ElastiCacheClient } from "@aws-sdk/client-elasticache";
import { LightsailClient } from "@aws-sdk/client-lightsail";
import { CognitoIdentityProviderClient } from "@aws-sdk/client-cognito-identity-provider";
const endpoint = process.env.FLOCI_ENDPOINT;
const region = process.env.AWS_REGION || "us-east-1";
// Floci derives the AWS account from AWS_ACCESS_KEY_ID: a value that is exactly
// 12 digits is used verbatim as the account id, and resources are isolated per
// account. Any other key (e.g. "test", "AKIA...") falls back to
// FLOCI_DEFAULT_ACCOUNT_ID (000000000000). We lean on that contract to scope the
// console to an account: the access key *is* the account id and the secret can
// be any non-empty string (Floci does not validate it).
const DEFAULT_ACCOUNT_ID = process.env.FLOCI_DEFAULT_ACCOUNT_ID || "000000000000";
const SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "floci";
export function isAccountId(value: string | null | undefined): value is string {
return typeof value === "string" && /^\d{12}$/.test(value);
}
/** The account used when a request does not (validly) specify one. */
export function defaultAccountId(): string {
const envKey = process.env.AWS_ACCESS_KEY_ID;
return isAccountId(envKey) ? envKey : DEFAULT_ACCOUNT_ID;
}
/** Normalize a caller-supplied account id, falling back to the default. */
export function resolveAccountId(raw?: string | null): string {
return isAccountId(raw) ? raw : defaultAccountId();
}
export type AwsClients = {
s3: S3Client;
lambda: LambdaClient;
eks: EKSClient;
ec2: EC2Client;
rds: RDSClient;
rdsData: RDSDataClient;
secretsManager: SecretsManagerClient;
logs: CloudWatchLogsClient;
ssm: SSMClient;
kms: KMSClient;
sfn: SFNClient;
elbv2: ElasticLoadBalancingV2Client;
eventbridge: EventBridgeClient;
sqs: SQSClient;
iam: IAMClient;
dynamodb: DynamoDBClient;
apiGateway: APIGatewayClient;
cloudformation: CloudFormationClient;
appConfig: AppConfigClient;
sns: SNSClient;
ecr: ECRClient;
codebuild: CodeBuildClient;
codepipeline: CodePipelineClient;
codedeploy: CodeDeployClient;
ecs: ECSClient;
kafka: KafkaClient;
kafkaConnect: KafkaConnectClient;
kinesis: KinesisClient;
sesv2: SESv2Client;
cloudwatch: CloudWatchClient;
elasticache: ElastiCacheClient;
lightsail: LightsailClient;
cognito: CognitoIdentityProviderClient;
};
export type AwsClientName = keyof AwsClients;
const clientCache = new Map<string, AwsClients>();
function buildClients(accountId: string): AwsClients {
const base = {
region,
credentials: { accessKeyId: accountId, secretAccessKey: SECRET_ACCESS_KEY },
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
};
return {
s3: new S3Client({ ...base, forcePathStyle: true }),
lambda: new LambdaClient(base),
eks: new EKSClient(base),
ec2: new EC2Client(base),
rds: new RDSClient(base),
rdsData: new RDSDataClient(base),
secretsManager: new SecretsManagerClient(base),
logs: new CloudWatchLogsClient(base),
ssm: new SSMClient(base),
kms: new KMSClient(base),
sfn: new SFNClient(base),
elbv2: new ElasticLoadBalancingV2Client(base),
eventbridge: new EventBridgeClient(base),
sqs: new SQSClient(base),
iam: new IAMClient(base),
dynamodb: new DynamoDBClient(base),
apiGateway: new APIGatewayClient(base),
cloudformation: new CloudFormationClient(base),
appConfig: new AppConfigClient(base),
sns: new SNSClient(base),
ecr: new ECRClient(base),
codebuild: new CodeBuildClient(base),
codepipeline: new CodePipelineClient(base),
codedeploy: new CodeDeployClient(base),
ecs: new ECSClient(base),
kafka: new KafkaClient(base),
kafkaConnect: new KafkaConnectClient(base),
kinesis: new KinesisClient(base),
sesv2: new SESv2Client(base),
cloudwatch: new CloudWatchClient(base),
elasticache: new ElastiCacheClient(base),
lightsail: new LightsailClient(base),
cognito: new CognitoIdentityProviderClient(base),
};
}
/**
* Return the cached AWS SDK client set bound to a given account. Clients are
* cheap to build but each holds connection state, so we memoize one set per
* resolved account id.
*/
export function awsClientsForAccount(accountId?: string | null): AwsClients {
const id = resolveAccountId(accountId);
let clients = clientCache.get(id);
if (!clients) {
clients = buildClients(id);
clientCache.set(id, clients);
}
return clients;
}
// Default-account singletons, kept for surfaces that are not yet account-aware:
// the dedicated EC2, EKS, RDS and Secrets Manager routes. These ignore the
// x-floci-account-id header and always serve the default account, so they do not
// follow the console's active account. The Cloud Explorer proxy resolves clients
// per request via awsClientsForAccount() instead.
export const awsClients = awsClientsForAccount(defaultAccountId());
export const s3 = awsClients.s3;
export const lambda = awsClients.lambda;
export const eks = awsClients.eks;
export const ec2 = awsClients.ec2;
export const rds = awsClients.rds;
export const secretsManager = awsClients.secretsManager;
export const eventbridge = awsClients.eventbridge;
export const sqs = awsClients.sqs;
export const iam = awsClients.iam;
export const dynamodb = awsClients.dynamodb;
export const apiGateway = awsClients.apiGateway;
export const cloudformation = awsClients.cloudformation;
export const ecr = awsClients.ecr;
export const kinesis = awsClients.kinesis;