mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3750 from akhilmhdh/feat/dynamic-secret-aws
feat: assume role mode for aws dynamic secret iam
This commit is contained in:
@@ -99,7 +99,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
||||||
) as object;
|
) as object;
|
||||||
|
|
||||||
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, {
|
||||||
|
projectId: folder.projectId
|
||||||
|
});
|
||||||
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
dynamicSecretLeases.map(({ externalEntityId }) =>
|
||||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
|
selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
|
||||||
|
projectId: folder.projectId
|
||||||
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
result = await selectedProvider.create({
|
result = await selectedProvider.create({
|
||||||
inputs: decryptedStoredInput,
|
inputs: decryptedStoredInput,
|
||||||
expireAt: expireAt.getTime(),
|
expireAt: expireAt.getTime(),
|
||||||
usernameTemplate: dynamicSecretCfg.usernameTemplate
|
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
||||||
|
metadata: { projectId }
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||||
@@ -237,7 +238,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
const { entityId } = await selectedProvider.renew(
|
const { entityId } = await selectedProvider.renew(
|
||||||
decryptedStoredInput,
|
decryptedStoredInput,
|
||||||
dynamicSecretLease.externalEntityId,
|
dynamicSecretLease.externalEntityId,
|
||||||
expireAt.getTime()
|
expireAt.getTime(),
|
||||||
|
{ projectId }
|
||||||
);
|
);
|
||||||
|
|
||||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||||
@@ -313,7 +315,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
) as object;
|
) as object;
|
||||||
|
|
||||||
const revokeResponse = await selectedProvider
|
const revokeResponse = await selectedProvider
|
||||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
// only propogate this error if forced is false
|
// only propogate this error if forced is false
|
||||||
if (!isForced) return { error: err as Error };
|
if (!isForced) return { error: err as Error };
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
|
||||||
|
|
||||||
let selectedGatewayId: string | null = null;
|
let selectedGatewayId: string | null = null;
|
||||||
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
|
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
|
||||||
@@ -146,7 +146,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
selectedGatewayId = gateway.id;
|
selectedGatewayId = gateway.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId });
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@@ -272,7 +272,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
||||||
) as object;
|
) as object;
|
||||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
|
||||||
|
|
||||||
let selectedGatewayId: string | null = null;
|
let selectedGatewayId: string | null = null;
|
||||||
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
||||||
@@ -301,7 +301,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
selectedGatewayId = gateway.id;
|
selectedGatewayId = gateway.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput, { projectId });
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
@@ -472,7 +472,9 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
||||||
) as object;
|
) as object;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput, {
|
||||||
|
projectId
|
||||||
|
})) as object;
|
||||||
|
|
||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ import {
|
|||||||
PutUserPolicyCommand,
|
PutUserPolicyCommand,
|
||||||
RemoveUserFromGroupCommand
|
RemoveUserFromGroupCommand
|
||||||
} from "@aws-sdk/client-iam";
|
} from "@aws-sdk/client-iam";
|
||||||
|
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
const generateUsername = (usernameTemplate?: string | null) => {
|
const generateUsername = (usernameTemplate?: string | null) => {
|
||||||
const randomUsername = alphaNumericNanoId(32);
|
const randomUsername = alphaNumericNanoId(32);
|
||||||
@@ -40,7 +43,43 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>, projectId: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||||
|
const stsClient = new STSClient({
|
||||||
|
region: providerInputs.region,
|
||||||
|
credentials:
|
||||||
|
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||||
|
? {
|
||||||
|
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||||
|
}
|
||||||
|
: undefined // if hosting on AWS
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new AssumeRoleCommand({
|
||||||
|
RoleArn: providerInputs.roleArn,
|
||||||
|
RoleSessionName: `infisical-dynamic-secret-${randomUUID()}`,
|
||||||
|
DurationSeconds: 900, // 15 mins
|
||||||
|
ExternalId: projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const assumeRes = await stsClient.send(command);
|
||||||
|
|
||||||
|
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
|
||||||
|
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
||||||
|
}
|
||||||
|
const client = new IAMClient({
|
||||||
|
region: providerInputs.region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: assumeRes.Credentials?.AccessKeyId,
|
||||||
|
secretAccessKey: assumeRes.Credentials?.SecretAccessKey,
|
||||||
|
sessionToken: assumeRes.Credentials?.SessionToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
const client = new IAMClient({
|
const client = new IAMClient({
|
||||||
region: providerInputs.region,
|
region: providerInputs.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -52,19 +91,36 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await $getClient(providerInputs);
|
const client = await $getClient(providerInputs, projectId);
|
||||||
|
const isConnected = await client
|
||||||
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
.send(new GetUserCommand({}))
|
||||||
|
.then(() => true)
|
||||||
|
.catch((err) => {
|
||||||
|
const message = (err as Error)?.message;
|
||||||
|
if (
|
||||||
|
providerInputs.method === AwsIamAuthType.AssumeRole &&
|
||||||
|
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
||||||
|
message.includes("Must specify userName when calling with non-User credentials")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
return isConnected;
|
return isConnected;
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
const create = async (data: {
|
||||||
const { inputs, usernameTemplate } = data;
|
inputs: unknown;
|
||||||
|
expireAt: number;
|
||||||
|
usernameTemplate?: string | null;
|
||||||
|
metadata: { projectId: string };
|
||||||
|
}) => {
|
||||||
|
const { inputs, usernameTemplate, metadata } = data;
|
||||||
|
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await $getClient(providerInputs);
|
const client = await $getClient(providerInputs, metadata.projectId);
|
||||||
|
|
||||||
const username = generateUsername(usernameTemplate);
|
const username = generateUsername(usernameTemplate);
|
||||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||||
@@ -76,6 +132,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
UserName: username
|
UserName: username
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||||
if (userGroups) {
|
if (userGroups) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -125,9 +182,9 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await $getClient(providerInputs);
|
const client = await $getClient(providerInputs, metadata.projectId);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export enum SqlProviders {
|
|||||||
Vertica = "vertica"
|
Vertica = "vertica"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AwsIamAuthType {
|
||||||
|
AssumeRole = "assume-role",
|
||||||
|
AccessKey = "access-key"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ElasticSearchAuthTypes {
|
export enum ElasticSearchAuthTypes {
|
||||||
User = "user",
|
User = "user",
|
||||||
ApiKey = "api-key"
|
ApiKey = "api-key"
|
||||||
@@ -168,16 +173,38 @@ export const DynamicSecretSapAseSchema = z.object({
|
|||||||
revocationStatement: z.string().trim()
|
revocationStatement: z.string().trim()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DynamicSecretAwsIamSchema = z.object({
|
export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||||
accessKey: z.string().trim().min(1),
|
(val) => {
|
||||||
secretAccessKey: z.string().trim().min(1),
|
if (typeof val === "object" && val !== null && !Object.hasOwn(val, "method")) {
|
||||||
region: z.string().trim().min(1),
|
// eslint-disable-next-line no-param-reassign
|
||||||
awsPath: z.string().trim().optional(),
|
(val as { method: string }).method = AwsIamAuthType.AccessKey;
|
||||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
}
|
||||||
policyDocument: z.string().trim().optional(),
|
return val;
|
||||||
userGroups: z.string().trim().optional(),
|
},
|
||||||
policyArns: z.string().trim().optional()
|
z.discriminatedUnion("method", [
|
||||||
});
|
z.object({
|
||||||
|
method: z.literal(AwsIamAuthType.AccessKey),
|
||||||
|
accessKey: z.string().trim().min(1),
|
||||||
|
secretAccessKey: z.string().trim().min(1),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
awsPath: z.string().trim().optional(),
|
||||||
|
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||||
|
policyDocument: z.string().trim().optional(),
|
||||||
|
userGroups: z.string().trim().optional(),
|
||||||
|
policyArns: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsIamAuthType.AssumeRole),
|
||||||
|
roleArn: z.string().trim().min(1, "Role ARN required"),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
awsPath: z.string().trim().optional(),
|
||||||
|
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||||
|
policyDocument: z.string().trim().optional(),
|
||||||
|
userGroups: z.string().trim().optional(),
|
||||||
|
policyArns: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
export const DynamicSecretMongoAtlasSchema = z.object({
|
export const DynamicSecretMongoAtlasSchema = z.object({
|
||||||
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
|
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
|
||||||
@@ -400,9 +427,15 @@ export type TDynamicProviderFns = {
|
|||||||
inputs: unknown;
|
inputs: unknown;
|
||||||
expireAt: number;
|
expireAt: number;
|
||||||
usernameTemplate?: string | null;
|
usernameTemplate?: string | null;
|
||||||
|
metadata: { projectId: string };
|
||||||
}) => Promise<{ entityId: string; data: unknown }>;
|
}) => Promise<{ entityId: string; data: unknown }>;
|
||||||
validateConnection: (inputs: unknown) => Promise<boolean>;
|
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
||||||
validateProviderInputs: (inputs: object) => Promise<unknown>;
|
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
|
||||||
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
|
revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>;
|
||||||
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
|
renew: (
|
||||||
|
inputs: unknown,
|
||||||
|
entityId: string,
|
||||||
|
expireAt: number,
|
||||||
|
metadata: { projectId: string }
|
||||||
|
) => Promise<{ entityId: string }>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ const envSchema = z
|
|||||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||||
|
|
||||||
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"),
|
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"),
|
||||||
|
DYNAMIC_SECRET_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()).default(
|
||||||
|
process.env.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
|
||||||
|
),
|
||||||
|
DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default(
|
||||||
|
process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||||
|
),
|
||||||
/* ----------------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
|||||||
@@ -50,110 +50,281 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
|||||||
|
|
||||||
## Set up Dynamic Secrets with AWS IAM
|
## Set up Dynamic Secrets with AWS IAM
|
||||||
|
|
||||||
<Steps>
|
<Tabs>
|
||||||
<Step title="Secret Overview Dashboard">
|
<Tab title="Assume Role (Recommended)">
|
||||||
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
||||||
</Step>
|
<Accordion title="Self-Hosted Instance">
|
||||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role.
|
||||||

|
|
||||||
</Step>
|
|
||||||
<Step title="Select AWS IAM">
|
|
||||||

|
|
||||||
</Step>
|
|
||||||
<Step title="Provide the inputs for dynamic secret parameters">
|
|
||||||
<ParamField path="Secret Name" type="string" required>
|
|
||||||
Name by which you want the secret to be referenced
|
|
||||||
</ParamField>
|
|
||||||
|
|
||||||
<ParamField path="Default TTL" type="string" required>
|
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
|
||||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
|
||||||
</ParamField>
|
|
||||||
|
|
||||||
<ParamField path="Max TTL" type="string" required>
|
The following steps are for instances not deployed on AWS:
|
||||||
Maximum time-to-live for a generated secret
|
<Steps>
|
||||||
</ParamField>
|
<Step title="Create an IAM User">
|
||||||
|
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
|
||||||
|
</Step>
|
||||||
|
<Step title="Create an Inline Policy">
|
||||||
|
Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowAssumeAnyRole",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "sts:AssumeRole",
|
||||||
|
"Resource": "arn:aws:iam::*:role/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
<Step title="Obtain the IAM User Credentials">
|
||||||
|
Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**.
|
||||||
|
|
||||||
<ParamField path="AWS Access Key" type="string" required>
|

|
||||||
The managing AWS IAM User Access Key
|

|
||||||
</ParamField>
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Set Up Connection Keys">
|
||||||
|
1. Set the access key as **DYNAMIC_SECRET_AWS_ACCESS_KEY_ID**.
|
||||||
|
2. Set the secret key as **DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY**.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<ParamField path="AWS Secret Key" type="string" required>
|
<Steps>
|
||||||
The managing AWS IAM User Secret Key
|
<Step title="Create the Managing User IAM Role for Infisical">
|
||||||
</ParamField>
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||
|

|
||||||
|
|
||||||
<ParamField path="AWS IAM Path" type="string">
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
|
3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||||
</ParamField>
|
4. (Recommended) <strong>Enable "Require external ID"</strong> and input your **Project ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
|
||||||
|
5. Assign permission as shared in prerequisite.
|
||||||
|
|
||||||
<ParamField path="AWS Region" type="string" required>
|
<Warning type="warning" title="Security Best Practice: Use External ID to Prevent Confused Deputy Attacks">
|
||||||
The AWS data center region.
|
When configuring an IAM Role that Infisical will assume, it’s highly recommended to enable the **"Require external ID"** option and specify your **Project ID**.
|
||||||
</ParamField>
|
|
||||||
|
|
||||||
<ParamField path="IAM User Permission Boundary" type="string" required>
|
This precaution helps protect your AWS account against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), a potential security vulnerability where Infisical could be tricked into performing actions on your behalf by an unauthorized actor.
|
||||||
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
|
|
||||||
</ParamField>
|
|
||||||
|
|
||||||
<ParamField path="AWS IAM Groups" type="string">
|
<strong>Always enable "Require external ID" and use your Project ID when setting up the IAM Role.</strong>
|
||||||
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
|
</Warning>
|
||||||
</ParamField>
|
</Step>
|
||||||
|
<Step title="Copy the AWS IAM Role ARN">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Secret Overview Dashboard">
|
||||||
|
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select AWS IAM">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|

|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="AWS Policy ARNs" type="string">
|
<ParamField path="Default TTL" type="string" required>
|
||||||
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="AWS IAM Policy Document" type="string">
|
<ParamField path="Max TTL" type="string" required>
|
||||||
The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
<ParamField path="Method" type="string" required>
|
||||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
Select *Assume Role* method.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
Allowed template variables are
|
<ParamField path="Aws Role ARN" type="string" required>
|
||||||
- `{{randomUsername}}`: Random username string
|
The ARN of the AWS Role to assume.
|
||||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
</ParamField>
|
||||||
</ParamField>
|
|
||||||
|
|
||||||

|
<ParamField path="AWS IAM Path" type="string">
|
||||||
|
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
</Step>
|
<ParamField path="AWS Region" type="string" required>
|
||||||
<Step title="Click 'Submit'">
|
The AWS data center region.
|
||||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
</ParamField>
|
||||||
|
|
||||||

|
<ParamField path="IAM User Permission Boundary" type="string" required>
|
||||||
</Step>
|
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
|
||||||
<Step title="Generate dynamic secrets">
|
</ParamField>
|
||||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
|
||||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
|
||||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
|
||||||
|
|
||||||

|
<ParamField path="AWS IAM Groups" type="string">
|
||||||

|
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
<ParamField path="AWS Policy ARNs" type="string">
|
||||||
|
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
|
||||||
|
</ParamField>
|
||||||
|
|
||||||

|
<ParamField path="AWS IAM Policy Document" type="string">
|
||||||
|
The AWS IAM inline policy that should be attached to the created users.
|
||||||
|
Multiple values can be provided by separating them with commas
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<Tip>
|
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||||
</Tip>
|
|
||||||
|
|
||||||
|
Allowed template variables are
|
||||||
|
|
||||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
- `{{randomUsername}}`: Random username string
|
||||||
|
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
|
||||||

|
<Step title="Click 'Submit'">
|
||||||
</Step>
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
</Steps>
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Access Key">
|
||||||
|
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
|
||||||
|
<Steps>
|
||||||
|
<Step title="Secret Overview Dashboard">
|
||||||
|
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select AWS IAM">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|

|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Default TTL" type="string" required>
|
||||||
|
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
Maximum time-to-live for a generated secret
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Method" type="string" required>
|
||||||
|
Select *Access Key* method.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS Access Key" type="string" required>
|
||||||
|
The managing AWS IAM User Access Key
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS Secret Key" type="string" required>
|
||||||
|
The managing AWS IAM User Secret Key
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS IAM Path" type="string">
|
||||||
|
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS Region" type="string" required>
|
||||||
|
The AWS data center region.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="IAM User Permission Boundary" type="string" required>
|
||||||
|
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS IAM Groups" type="string">
|
||||||
|
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS Policy ARNs" type="string">
|
||||||
|
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="AWS IAM Policy Document" type="string">
|
||||||
|
The AWS IAM inline policy that should be attached to the created users.
|
||||||
|
Multiple values can be provided by separating them with commas
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||||
|
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||||
|
|
||||||
|
Allowed template variables are
|
||||||
|
|
||||||
|
- `{{randomUsername}}`: Random username string
|
||||||
|
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Click 'Submit'">
|
||||||
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Audit or Revoke Leases
|
## Audit or Revoke Leases
|
||||||
|
|
||||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||||
This will allow you to see the lease details and delete the lease ahead of its expiration time.
|
This will allow you to see the lease details and delete the lease ahead of its expiration time.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Renew Leases
|
## Renew Leases
|
||||||
|
|
||||||
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
|
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
|
||||||

|

|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||||
|
secret
|
||||||
</Warning>
|
</Warning>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 526 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 526 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 532 KiB |
@@ -44,6 +44,11 @@ export enum SqlProviders {
|
|||||||
MsSQL = "mssql"
|
MsSQL = "mssql"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DynamicSecretAwsIamAuth {
|
||||||
|
AssumeRole = "assume-role",
|
||||||
|
AccessKey = "access-key"
|
||||||
|
}
|
||||||
|
|
||||||
export type TDynamicSecretProvider =
|
export type TDynamicSecretProvider =
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.SqlDatabase;
|
type: DynamicSecretProviders.SqlDatabase;
|
||||||
@@ -78,15 +83,26 @@ export type TDynamicSecretProvider =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.AwsIam;
|
type: DynamicSecretProviders.AwsIam;
|
||||||
inputs: {
|
inputs:
|
||||||
accessKey: string;
|
| {
|
||||||
secretAccessKey: string;
|
method: DynamicSecretAwsIamAuth.AccessKey;
|
||||||
region: string;
|
accessKey: string;
|
||||||
awsPath?: string;
|
secretAccessKey: string;
|
||||||
policyDocument?: string;
|
region: string;
|
||||||
userGroups?: string;
|
awsPath?: string;
|
||||||
policyArns?: string;
|
policyDocument?: string;
|
||||||
};
|
userGroups?: string;
|
||||||
|
policyArns?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: DynamicSecretAwsIamAuth.AssumeRole;
|
||||||
|
roleArn: string;
|
||||||
|
region: string;
|
||||||
|
awsPath?: string;
|
||||||
|
policyDocument?: string;
|
||||||
|
userGroups?: string;
|
||||||
|
policyArns?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.Redis;
|
type: DynamicSecretProviders.Redis;
|
||||||
|
|||||||
@@ -5,22 +5,46 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
import {
|
||||||
|
DynamicSecretAwsIamAuth,
|
||||||
|
DynamicSecretProviders
|
||||||
|
} from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
provider: z.object({
|
provider: z.discriminatedUnion("method", [
|
||||||
accessKey: z.string().trim().min(1),
|
z.object({
|
||||||
secretAccessKey: z.string().trim().min(1),
|
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
|
||||||
region: z.string().trim().min(1),
|
accessKey: z.string().trim().min(1),
|
||||||
awsPath: z.string().trim().optional(),
|
secretAccessKey: z.string().trim().min(1),
|
||||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
region: z.string().trim().min(1),
|
||||||
policyDocument: z.string().trim().optional(),
|
awsPath: z.string().trim().optional(),
|
||||||
userGroups: z.string().trim().optional(),
|
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||||
policyArns: z.string().trim().optional()
|
policyDocument: z.string().trim().optional(),
|
||||||
}),
|
userGroups: z.string().trim().optional(),
|
||||||
|
policyArns: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||||
|
roleArn: z.string().trim().min(1),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
awsPath: z.string().trim().optional(),
|
||||||
|
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||||
|
policyDocument: z.string().trim().optional(),
|
||||||
|
userGroups: z.string().trim().optional(),
|
||||||
|
policyArns: z.string().trim().optional()
|
||||||
|
})
|
||||||
|
]),
|
||||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
@@ -67,16 +91,21 @@ export const AwsIamInputForm = ({
|
|||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
handleSubmit
|
handleSubmit,
|
||||||
|
watch
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
environment: isSingleEnvironmentMode ? environments[0] : undefined,
|
||||||
usernameTemplate: "{{randomUsername}}"
|
usernameTemplate: "{{randomUsername}}",
|
||||||
|
provider: {
|
||||||
|
method: DynamicSecretAwsIamAuth.AssumeRole
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const createDynamicSecret = useCreateDynamicSecret();
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
|
const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey;
|
||||||
|
|
||||||
const handleCreateDynamicSecret = async ({
|
const handleCreateDynamicSecret = async ({
|
||||||
name,
|
name,
|
||||||
@@ -127,7 +156,7 @@ export const AwsIamInputForm = ({
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="dynamic-postgres" />
|
<Input {...field} placeholder="dynamic-aws-iam" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -170,38 +199,82 @@ export const AwsIamInputForm = ({
|
|||||||
Configuration
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center space-x-2">
|
<Controller
|
||||||
<Controller
|
name="provider.method"
|
||||||
control={control}
|
control={control}
|
||||||
name="provider.accessKey"
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
defaultValue=""
|
<FormControl
|
||||||
render={({ field, fieldState: { error } }) => (
|
errorText={error?.message}
|
||||||
<FormControl
|
isError={Boolean(error?.message)}
|
||||||
label="AWS Access Key"
|
label="Method"
|
||||||
className="flex-grow"
|
>
|
||||||
isError={Boolean(error?.message)}
|
<Select
|
||||||
errorText={error?.message}
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
>
|
>
|
||||||
<Input {...field} />
|
<SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
|
||||||
</FormControl>
|
Assume Role (Recommended)
|
||||||
)}
|
</SelectItem>
|
||||||
/>
|
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
|
||||||
<Controller
|
</Select>
|
||||||
control={control}
|
</FormControl>
|
||||||
name="provider.secretAccessKey"
|
)}
|
||||||
defaultValue=""
|
/>
|
||||||
render={({ field, fieldState: { error } }) => (
|
{isAccessKeyMethod ? (
|
||||||
<FormControl
|
<div className="flex items-center space-x-2">
|
||||||
label="AWS Secret Key"
|
<Controller
|
||||||
className="flex-grow"
|
control={control}
|
||||||
isError={Boolean(error?.message)}
|
name="provider.accessKey"
|
||||||
errorText={error?.message}
|
defaultValue=""
|
||||||
>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Input {...field} type="password" />
|
<FormControl
|
||||||
</FormControl>
|
label="AWS Access Key"
|
||||||
)}
|
className="flex-grow"
|
||||||
/>
|
isError={Boolean(error?.message)}
|
||||||
</div>
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.secretAccessKey"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="AWS Secret Key"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.roleArn"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Assume Role ARN"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
inputs: z
|
inputs: z.discriminatedUnion("method", [
|
||||||
.object({
|
z.object({
|
||||||
|
method: z.literal(DynamicSecretAwsIamAuth.AccessKey),
|
||||||
accessKey: z.string().trim().min(1),
|
accessKey: z.string().trim().min(1),
|
||||||
secretAccessKey: z.string().trim().min(1),
|
secretAccessKey: z.string().trim().min(1),
|
||||||
region: z.string().trim().min(1),
|
region: z.string().trim().min(1),
|
||||||
@@ -21,8 +22,18 @@ const formSchema = z.object({
|
|||||||
policyDocument: z.string().trim().optional(),
|
policyDocument: z.string().trim().optional(),
|
||||||
userGroups: z.string().trim().optional(),
|
userGroups: z.string().trim().optional(),
|
||||||
policyArns: z.string().trim().optional()
|
policyArns: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||||
|
roleArn: z.string().trim().min(1),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
awsPath: z.string().trim().optional(),
|
||||||
|
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||||
|
policyDocument: z.string().trim().optional(),
|
||||||
|
userGroups: z.string().trim().optional(),
|
||||||
|
policyArns: z.string().trim().optional()
|
||||||
})
|
})
|
||||||
.partial(),
|
]),
|
||||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
@@ -66,6 +77,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
watch,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
handleSubmit
|
handleSubmit
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
@@ -80,6 +92,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
|
||||||
|
|
||||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||||
|
|
||||||
@@ -173,38 +186,82 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center space-x-2">
|
<Controller
|
||||||
<Controller
|
name="inputs.method"
|
||||||
control={control}
|
control={control}
|
||||||
name="inputs.accessKey"
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
defaultValue=""
|
<FormControl
|
||||||
render={({ field, fieldState: { error } }) => (
|
errorText={error?.message}
|
||||||
<FormControl
|
isError={Boolean(error?.message)}
|
||||||
label="AWS Access Key"
|
label="Method"
|
||||||
className="flex-grow"
|
>
|
||||||
isError={Boolean(error?.message)}
|
<Select
|
||||||
errorText={error?.message}
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
>
|
>
|
||||||
<Input {...field} />
|
<SelectItem value={DynamicSecretAwsIamAuth.AssumeRole}>
|
||||||
</FormControl>
|
Assume Role (Recommended)
|
||||||
)}
|
</SelectItem>
|
||||||
/>
|
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
|
||||||
<Controller
|
</Select>
|
||||||
control={control}
|
</FormControl>
|
||||||
name="inputs.secretAccessKey"
|
)}
|
||||||
defaultValue=""
|
/>
|
||||||
render={({ field, fieldState: { error } }) => (
|
{isAccessKeyMethod ? (
|
||||||
<FormControl
|
<div className="flex items-center space-x-2">
|
||||||
label="AWS Secret Key"
|
<Controller
|
||||||
className="flex-grow"
|
control={control}
|
||||||
isError={Boolean(error?.message)}
|
name="inputs.accessKey"
|
||||||
errorText={error?.message}
|
defaultValue=""
|
||||||
>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Input {...field} type="password" />
|
<FormControl
|
||||||
</FormControl>
|
label="AWS Access Key"
|
||||||
)}
|
className="flex-grow"
|
||||||
/>
|
isError={Boolean(error?.message)}
|
||||||
</div>
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.secretAccessKey"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="AWS Secret Key"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.roleArn"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Assume Role ARN"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
Reference in New Issue
Block a user