mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 19:26:19 +00:00
feature: snowflake dynamic secrets
This commit is contained in:
Generated
+2245
-255
File diff suppressed because it is too large
Load Diff
@@ -196,6 +196,7 @@
|
|||||||
"scim2-parse-filter": "^0.2.10",
|
"scim2-parse-filter": "^0.2.10",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
|
"snowflake-sdk": "^1.14.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||||
|
|
||||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||||
import { AwsIamProvider } from "./aws-iam";
|
import { AwsIamProvider } from "./aws-iam";
|
||||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||||
@@ -24,5 +26,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||||
[DynamicSecretProviders.SapHana]: SapHanaProvider()
|
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||||
|
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -177,6 +177,16 @@ export const DynamicSecretSapHanaSchema = z.object({
|
|||||||
ca: z.string().optional()
|
ca: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretSnowflakeSchema = z.object({
|
||||||
|
accountId: z.string().trim().min(1),
|
||||||
|
orgId: z.string().trim().min(1),
|
||||||
|
username: z.string().trim().min(1),
|
||||||
|
password: z.string().trim().min(1),
|
||||||
|
creationStatement: z.string().trim(),
|
||||||
|
revocationStatement: z.string().trim(),
|
||||||
|
renewStatement: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
export const AzureEntraIDSchema = z.object({
|
export const AzureEntraIDSchema = z.object({
|
||||||
tenantId: z.string().trim().min(1),
|
tenantId: z.string().trim().min(1),
|
||||||
userId: z.string().trim().min(1),
|
userId: z.string().trim().min(1),
|
||||||
@@ -208,7 +218,8 @@ export enum DynamicSecretProviders {
|
|||||||
RabbitMq = "rabbit-mq",
|
RabbitMq = "rabbit-mq",
|
||||||
AzureEntraID = "azure-entra-id",
|
AzureEntraID = "azure-entra-id",
|
||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana"
|
SapHana = "sap-hana",
|
||||||
|
Snowflake = "snowflake"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@@ -223,7 +234,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import handlebars from "handlebars";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import snowflake from "snowflake-sdk";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
// destroy client requires callback...
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
const generatePassword = (size = 48) => {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||||
|
return customAlphabet(charset, 48)(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateUsername = () => {
|
||||||
|
return `infisical_${alphaNumericNanoId(32)}`; // username must start with alpha character, hence prefix
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDaysToExpiry = (expiryDate: Date) => {
|
||||||
|
const start = new Date().getTime();
|
||||||
|
const end = new Date(expiryDate).getTime();
|
||||||
|
const diffTime = Math.abs(end - start);
|
||||||
|
|
||||||
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
||||||
|
const client = snowflake.createConnection({
|
||||||
|
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
|
||||||
|
username: providerInputs.username,
|
||||||
|
password: providerInputs.password,
|
||||||
|
application: "Infisical"
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
client.connect((err) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const isValidConnection = await client.isValidAsync();
|
||||||
|
|
||||||
|
client.destroy(noop);
|
||||||
|
|
||||||
|
return isValidConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
const expiration = getDaysToExpiry(new Date(expireAt));
|
||||||
|
|
||||||
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
expiration
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
client.execute({
|
||||||
|
sqlText: creationStatement,
|
||||||
|
complete(err) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
client.destroy(noop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
client.execute({
|
||||||
|
sqlText: revokeStatement,
|
||||||
|
complete(err) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
client.destroy(noop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, username: string, expireAt: number) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const client = await getClient(providerInputs);
|
||||||
|
|
||||||
|
const expiration = getDaysToExpiry(new Date(expireAt));
|
||||||
|
|
||||||
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
|
username,
|
||||||
|
expiration
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
client.execute({
|
||||||
|
sqlText: renewStatement,
|
||||||
|
complete(err) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
client.destroy(noop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityId: username };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
title: "Snowflake"
|
||||||
|
description: "Learn how to dynamically generate Snowflake user credentials."
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical's Snowflake dynamic secrets allow you to generate Snowflake user credentials on demand.
|
||||||
|
|
||||||
|
## Snowflake Prerequisites
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Infisical requires a Snowflake user in your account with the USERADMIN role. This user will act as a service account for Infisical and facilitate the creation of new users as needed.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to Snowflake's User Dashboard and press the '+ User' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Create a Snowflake user with the USERADMIN role for Infisical">
|
||||||
|
<Warning>
|
||||||
|
Be sure to uncheck "Force user to change password on first time login"
|
||||||
|
</Warning>
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the Account Menu in the bottom left and take note of your Account and Organization identifiers">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Set up Dynamic Secrets with Snowflake
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open the Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select the Snowflake option in the grid list">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the required parameters for the Snowflake dynamic secret">
|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
The name you want to reference this secret by
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Default TTL" type="string" required>
|
||||||
|
Default time-to-live for a generated secret (it is possible to modify this value when generating a secret)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
Maximum time-to-live for a generated secret
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Account Identifier" type="string" required>
|
||||||
|
Snowflake account identifier
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Organization Identifier" type="string" required>
|
||||||
|
Snowflake organization identifier
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="User" type="string" required>
|
||||||
|
Username of the Infisical Service User
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Password" type="string" required>
|
||||||
|
Password of the Infisical Service User
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="(Optional) Modify SQL Statements">
|
||||||
|
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL
|
||||||
|
statement to your needs.
|
||||||
|

|
||||||
|
</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 fall 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>
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
This will allow you see the lease details and delete the lease ahead of its expiration time.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||||
|
secret.
|
||||||
|
</Warning>
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 559 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 666 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 301 KiB |
+2
-1
@@ -179,7 +179,8 @@
|
|||||||
"documentation/platform/dynamic-secrets/mongo-db",
|
"documentation/platform/dynamic-secrets/mongo-db",
|
||||||
"documentation/platform/dynamic-secrets/azure-entra-id",
|
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||||
"documentation/platform/dynamic-secrets/ldap",
|
"documentation/platform/dynamic-secrets/ldap",
|
||||||
"documentation/platform/dynamic-secrets/sap-hana"
|
"documentation/platform/dynamic-secrets/sap-hana",
|
||||||
|
"documentation/platform/dynamic-secrets/snowflake"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export enum DynamicSecretProviders {
|
|||||||
RabbitMq = "rabbit-mq",
|
RabbitMq = "rabbit-mq",
|
||||||
AzureEntraId = "azure-entra-id",
|
AzureEntraId = "azure-entra-id",
|
||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana"
|
SapHana = "sap-hana",
|
||||||
|
Snowflake = "snowflake"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
@@ -215,6 +216,18 @@ export type TDynamicSecretProvider =
|
|||||||
renewStatement?: string;
|
renewStatement?: string;
|
||||||
ca?: string | undefined;
|
ca?: string | undefined;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: DynamicSecretProviders.Snowflake;
|
||||||
|
inputs: {
|
||||||
|
orgId: string;
|
||||||
|
accountId: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
// creationStatement: string;
|
||||||
|
// revocationStatement: string;
|
||||||
|
// renewStatement?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type TCreateDynamicSecretDTO = {
|
export type TCreateDynamicSecretDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
|||||||
+26
-1
@@ -7,7 +7,8 @@ import {
|
|||||||
SiMicrosoftazure,
|
SiMicrosoftazure,
|
||||||
SiMongodb,
|
SiMongodb,
|
||||||
SiRabbitmq,
|
SiRabbitmq,
|
||||||
SiSap
|
SiSap,
|
||||||
|
SiSnowflake
|
||||||
} from "react-icons/si";
|
} from "react-icons/si";
|
||||||
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -16,6 +17,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
|
|
||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
import { SnowflakeInputForm } from "@app/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm";
|
||||||
|
|
||||||
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
|
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
|
||||||
import { AwsIamInputForm } from "./AwsIamInputForm";
|
import { AwsIamInputForm } from "./AwsIamInputForm";
|
||||||
@@ -103,6 +105,11 @@ const DYNAMIC_SECRET_LIST = [
|
|||||||
icon: <SiSap size="1.5rem" />,
|
icon: <SiSap size="1.5rem" />,
|
||||||
provider: DynamicSecretProviders.SapHana,
|
provider: DynamicSecretProviders.SapHana,
|
||||||
title: "SAP HANA"
|
title: "SAP HANA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SiSnowflake size="1.5rem" />,
|
||||||
|
provider: DynamicSecretProviders.Snowflake,
|
||||||
|
title: "Snowflake"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -380,6 +387,24 @@ export const CreateDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
|
selectedProvider === DynamicSecretProviders.Snowflake && (
|
||||||
|
<motion.div
|
||||||
|
key="dynamic-snowflake-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<SnowflakeInputForm
|
||||||
|
onCompleted={handleFormReset}
|
||||||
|
onCancel={handleFormReset}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+329
@@ -0,0 +1,329 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
|
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
provider: z.object({
|
||||||
|
accountId: z.string().trim().min(1),
|
||||||
|
orgId: z.string().trim().min(1),
|
||||||
|
username: z.string().trim().min(1),
|
||||||
|
password: z.string().trim().min(1),
|
||||||
|
creationStatement: z.string().trim().min(1),
|
||||||
|
revocationStatement: z.string().trim().min(1),
|
||||||
|
renewStatement: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCompleted: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
secretPath: string;
|
||||||
|
projectSlug: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SnowflakeInputForm = ({
|
||||||
|
onCompleted,
|
||||||
|
onCancel,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
projectSlug
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
provider: {
|
||||||
|
creationStatement:
|
||||||
|
"CREATE USER {{username}} PASSWORD = '{{password}}' DEFAULT_ROLE = public DEFAULT_SECONDARY_ROLES = ('ALL') MUST_CHANGE_PASSWORD = FALSE DAYS_TO_EXPIRY = {{expiration}};",
|
||||||
|
revocationStatement: "DROP USER {{username}};",
|
||||||
|
renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
|
|
||||||
|
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||||
|
// wait till previous request is finished
|
||||||
|
if (createDynamicSecret.isLoading) return;
|
||||||
|
try {
|
||||||
|
await createDynamicSecret.mutateAsync({
|
||||||
|
provider: { type: DynamicSecretProviders.Snowflake, inputs: provider },
|
||||||
|
maxTTL,
|
||||||
|
name,
|
||||||
|
path: secretPath,
|
||||||
|
defaultTTL,
|
||||||
|
projectSlug,
|
||||||
|
environmentSlug: environment
|
||||||
|
});
|
||||||
|
onCompleted();
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create dynamic secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="snowflake-secret" // TODO: remove
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="defaultTTL"
|
||||||
|
defaultValue="1h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Default TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="maxTTL"
|
||||||
|
defaultValue="24h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Max TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
<Link
|
||||||
|
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/snowflake"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<a target="_blank" rel="noopener noreferrer">
|
||||||
|
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
Docs
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.accountId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Account Identifier"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.orgId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Organization Identifier"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.username"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="User"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} autoComplete="off" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.password"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Password"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||||
|
<AccordionItem value="advance-statements">
|
||||||
|
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.creationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Creation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username, password and expiration are dynamically provisioned"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.revocationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Revocation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username is dynamically provisioned"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.renewStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Renew Statement"
|
||||||
|
helperText="username and expiration are dynamically provisioned"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-1
@@ -221,7 +221,10 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === DynamicSecretProviders.SapHana) {
|
if (
|
||||||
|
provider === DynamicSecretProviders.SapHana ||
|
||||||
|
provider === DynamicSecretProviders.Snowflake
|
||||||
|
) {
|
||||||
const { DB_USERNAME, DB_PASSWORD } = data as {
|
const { DB_USERNAME, DB_PASSWORD } = data as {
|
||||||
DB_USERNAME: string;
|
DB_USERNAME: string;
|
||||||
DB_PASSWORD: string;
|
DB_PASSWORD: string;
|
||||||
|
|||||||
+18
@@ -15,6 +15,7 @@ import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm";
|
|||||||
import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm";
|
import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm";
|
||||||
import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm";
|
import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm";
|
||||||
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
||||||
|
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
||||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -258,6 +259,23 @@ export const EditDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{dynamicSecretDetails?.type === DynamicSecretProviders.Snowflake && (
|
||||||
|
<motion.div
|
||||||
|
key="snowflake-edit"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<EditDynamicSecretSnowflakeForm
|
||||||
|
onClose={onClose}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
dynamicSecret={dynamicSecretDetails}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+337
@@ -0,0 +1,337 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
|
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
inputs: z
|
||||||
|
.object({
|
||||||
|
accountId: z.string().min(1),
|
||||||
|
orgId: z.string().min(1),
|
||||||
|
username: z.string().min(1),
|
||||||
|
password: z.string().min(1),
|
||||||
|
creationStatement: z.string().min(1),
|
||||||
|
revocationStatement: z.string().min(1),
|
||||||
|
renewStatement: z.string().optional()
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||||
|
secretPath: string;
|
||||||
|
projectSlug: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditDynamicSecretSnowflakeForm = ({
|
||||||
|
onClose,
|
||||||
|
dynamicSecret,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
projectSlug
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: {
|
||||||
|
defaultTTL: dynamicSecret.defaultTTL,
|
||||||
|
maxTTL: dynamicSecret.maxTTL,
|
||||||
|
newName: dynamicSecret.name,
|
||||||
|
inputs: {
|
||||||
|
...(dynamicSecret.inputs as TForm["inputs"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||||
|
|
||||||
|
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||||
|
// wait till previous request is finished
|
||||||
|
if (updateDynamicSecret.isLoading) return;
|
||||||
|
try {
|
||||||
|
await updateDynamicSecret.mutateAsync({
|
||||||
|
name: dynamicSecret.name,
|
||||||
|
path: secretPath,
|
||||||
|
projectSlug,
|
||||||
|
environmentSlug: environment,
|
||||||
|
data: {
|
||||||
|
maxTTL: maxTTL || undefined,
|
||||||
|
defaultTTL,
|
||||||
|
inputs,
|
||||||
|
newName: newName === dynamicSecret.name ? undefined : newName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated dynamic secret"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update dynamic secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="newName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="defaultTTL"
|
||||||
|
defaultValue="1h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Default TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="maxTTL"
|
||||||
|
defaultValue="24h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Max TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
<Link
|
||||||
|
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/snowflake"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<a target="_blank" rel="noopener noreferrer">
|
||||||
|
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
Docs
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.accountId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Account Identifer"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.orgId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Organizaton Identifier"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.username"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="User"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} autoComplete="off" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.password"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Password"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||||
|
<AccordionItem value="advance-statements">
|
||||||
|
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.creationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Creation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username, password and expiration are dynamically provisioned"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.revocationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Revocation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username is dynamically provisioned"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.renewStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Renew Statement"
|
||||||
|
helperText="username and expiration are dynamically provisioned"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user