diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 4adf8b7e2..49c55de66 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -227,7 +227,10 @@ export const dynamicSecretLeaseServiceFactory = ({ const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL)); if (maxTTL) { const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL)); - if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max ttl" }); + if (expireAt > maxExpiryDate) + throw new BadRequestError({ + message: "The requested renewal would exceed the maximum allowed lease duration. Please choose a shorter TTL" + }); } const { entityId } = await selectedProvider.renew( diff --git a/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts b/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts new file mode 100644 index 000000000..cb5c26d89 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts @@ -0,0 +1,103 @@ +import { gaxios, Impersonated, JWT } from "google-auth-library"; +import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models"; + +export const GcpIamProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await DynamicSecretGcpIamSchema.parseAsync(inputs); + return providerInputs; + }; + + const $getToken = async (serviceAccountEmail: string, ttl: number): Promise => { + const appCfg = getConfig(); + if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) { + throw new InternalServerError({ + message: "Environment variable has not been configured: INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL" + }); + } + + const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as { + client_email: string; + private_key: string; + }; + + const sourceClient = new JWT({ + email: credJson.client_email, + key: credJson.private_key, + scopes: ["https://www.googleapis.com/auth/cloud-platform"] + }); + + const impersonatedCredentials = new Impersonated({ + sourceClient, + targetPrincipal: serviceAccountEmail, + lifetime: ttl, + delegates: [], + targetScopes: ["https://www.googleapis.com/auth/iam", "https://www.googleapis.com/auth/cloud-platform"] + }); + + let tokenResponse: GetAccessTokenResponse | undefined; + try { + tokenResponse = await impersonatedCredentials.getAccessToken(); + } catch (error) { + let message = "Unable to validate connection"; + if (error instanceof gaxios.GaxiosError) { + message = error.message; + } + + throw new BadRequestError({ + message + }); + } + + if (!tokenResponse || !tokenResponse.token) { + throw new BadRequestError({ + message: "Unable to validate connection" + }); + } + + return tokenResponse.token; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + await $getToken(providerInputs.serviceAccountEmail, 10); + return true; + }; + + const create = async (inputs: unknown, expireAt: number) => { + const providerInputs = await validateProviderInputs(inputs); + + const now = Math.floor(Date.now() / 1000); + const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0); + + const token = await $getToken(providerInputs.serviceAccountEmail, ttl); + const entityId = alphaNumericNanoId(32); + + return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } }; + }; + + const revoke = async (_inputs: unknown, entityId: string) => { + // There's no way to revoke GCP IAM access tokens + return { entityId }; + }; + + const renew = async (inputs: unknown, entityId: string, expireAt: number) => { + // To renew a token it must be re-created + const data = await create(inputs, expireAt); + + return { ...data, entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index faa671980..cf6a33690 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam"; import { AzureEntraIDProvider } from "./azure-entra-id"; import { CassandraProvider } from "./cassandra"; import { ElasticSearchProvider } from "./elastic-search"; +import { GcpIamProvider } from "./gcp-iam"; import { LdapProvider } from "./ldap"; import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; import { MongoAtlasProvider } from "./mongo-atlas"; @@ -38,5 +39,6 @@ export const buildDynamicSecretProviders = ({ [DynamicSecretProviders.SapHana]: SapHanaProvider(), [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), [DynamicSecretProviders.Totp]: TotpProvider(), - [DynamicSecretProviders.SapAse]: SapAseProvider() + [DynamicSecretProviders.SapAse]: SapAseProvider(), + [DynamicSecretProviders.GcpIam]: GcpIamProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 449f6d8f6..e999a65be 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -305,6 +305,10 @@ export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [ }) ]); +export const DynamicSecretGcpIamSchema = z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -320,7 +324,8 @@ export enum DynamicSecretProviders { SapHana = "sap-hana", Snowflake = "snowflake", Totp = "totp", - SapAse = "sap-ase" + SapAse = "sap-ase", + GcpIam = "gcp-iam" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -338,7 +343,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }), z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), - z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }) + z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), + z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }) ]); export type TDynamicProviderFns = { diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index 2cf4edc0e..67a277f32 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -99,20 +99,20 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa ![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png) - After submitting the form, you will see a dynamic secret created in the dashboard. + After submitting the form, you will see a dynamic secret created in the dashboard. - If this step fails, you may have to add the CA certificate. + If this step fails, you may have to add the CA certificate. - 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. + 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. @@ -123,14 +123,14 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa - Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) ## 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 expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) @@ -141,4 +141,4 @@ To extend the life of the generated dynamic secret leases past its initial time Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret - \ No newline at end of file + diff --git a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx index 515efabeb..da76cdee6 100644 --- a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx +++ b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx @@ -123,16 +123,16 @@ Click on Add assignments. Search for the application name you created and select - After submitting the form, you will see a dynamic secrets for each user created in the dashboard. + After submitting the form, you will see a dynamic secrets for each user created in the dashboard. - 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. + 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. @@ -143,14 +143,14 @@ Click on Add assignments. Search for the application name you created and select - Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. ![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ad-lease.png) ## 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 expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 0e1bc5104..9a637b270 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -82,20 +82,20 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch - After submitting the form, you will see a dynamic secret created in the dashboard. + After submitting the form, you will see a dynamic secret created in the dashboard. - If this step fails, you may have to add the CA certificate. + If this step fails, you may have to add the CA certificate. - 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. + 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. @@ -106,14 +106,14 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch - Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) ## 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 expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/gcp-iam.mdx b/docs/documentation/platform/dynamic-secrets/gcp-iam.mdx new file mode 100644 index 000000000..ed66a8ffb --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/gcp-iam.mdx @@ -0,0 +1,141 @@ +--- +title: "GCP IAM" +description: "Learn how to dynamically generate GCP service account tokens." +--- + +The Infisical GCP IAM dynamic secret allows you to generate GCP service account tokens on demand based on service account permissions. + + + You must enable the [IAM API](https://console.cloud.google.com/apis/library/iam.googleapis.com) and [IAM Credentials API](https://console.cloud.google.com/apis/library/iamcredentials.googleapis.com) in your GCP console as a prerequisite + + + + Using the GCP integration on a self-hosted instance of Infisical requires configuring a service account on GCP and + configuring your instance to use it. + + + + ![Service Account API](/images/app-connections/gcp/service-account-credentials-api.png) + + + ![Service Account IAM Page](/images/app-connections/gcp/service-account-overview.png) + + + Create a new service account that will be used to impersonate other GCP service accounts for your app connections. + ![Create Service Account Page](/images/app-connections/gcp/create-instance-service-account.png) + + Press "DONE" after creating the service account. + + + Download the JSON key file for your service account. This will be used to authenticate your instance with GCP. + ![Service Account Credential Page](/images/app-connections/gcp/create-service-account-credential.png) + + + 1. Copy the entire contents of the downloaded JSON key file. + 2. Set it as a string value for the `INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL` environment variable. + 3. Restart your Infisical instance to apply the changes. + 4. You can now use GCP integration with service account impersonation. + + + + +## Create GCP Service Account + + + + ![Service Account Page](/images/app-connections/gcp/service-account-overview.png) + + + ![Create Service Account](/images/app-connections/gcp/create-service-account.png) + + + When you assign specific roles and permissions to this service account, any tokens generated through Infisical's dynamic secrets functionality will inherit these exact permissions. This means that applications using these dynamically generated tokens will have the same access capabilities as defined by the service account's role assignments, ensuring proper access control while maintaining the principle of least privilege. + + After configuring the appropriate roles, press "DONE". + + + To enable service account impersonation, you'll need to grant the **Service Account Token Creator** role to the Infisical instance's service account. This configuration allows Infisical to securely impersonate the new service account. + - Navigate to the IAM & Admin > Service Accounts section in your Google Cloud Console + - Select the newly created service account + - Click on the "PERMISSIONS" tab + - Click "Grant Access" to add a new principal + + If you're using Infisical Cloud US, use the following service account: `infisical-us@infisical-us.iam.gserviceaccount.com` + + If you're using Infisical Cloud EU, use the following service account: `infisical-eu@infisical-eu.iam.gserviceaccount.com` + + If you're self-hosting, follow the "Self-Hosted Instance" guide at the top of the page and then use service account you created + + ![Service Account Page](/images/app-connections/gcp/service-account-grant-access.png) + + + +## Set up Dynamic Secrets with GCP IAM + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-gcp-iam-modal.png) + + + + Name by which you want the secret to be referenced + + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + + + Maximum time-to-live for a generated secret + + + The email tied to the service account created in earlier steps. + + + + After submitting the form, you will see a dynamic secrets for each user created in the dashboard. + + + + 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. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + 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. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Dynamic Secret Lease](/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-lease.png) + + + +## 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 to see the expiration time of the lease or delete a lease before it's set time to live. + +![Lease Data](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases + +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below. + +![Lease Renew](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + diff --git a/docs/documentation/platform/dynamic-secrets/ldap.mdx b/docs/documentation/platform/dynamic-secrets/ldap.mdx index a1731432c..863780cca 100644 --- a/docs/documentation/platform/dynamic-secrets/ldap.mdx +++ b/docs/documentation/platform/dynamic-secrets/ldap.mdx @@ -133,8 +133,8 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. @@ -236,8 +236,8 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx index 6ac5ac069..4190e516c 100644 --- a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -71,20 +71,20 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential - After submitting the form, you will see a dynamic secret created in the dashboard. + After submitting the form, you will see a dynamic secret created in the dashboard. - If this step fails, you may have to add the CA certificate. + If this step fails, you may have to add the CA certificate. - 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. + 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. @@ -95,14 +95,14 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential - Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) ## 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 expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index 43fbc6b61..65e6ee121 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -61,20 +61,20 @@ Create a user with the required permission in your Redis instance. This user wil ![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png) - After submitting the form, you will see a dynamic secret created in the dashboard. + After submitting the form, you will see a dynamic secret created in the dashboard. - If this step fails, you may have to add the CA certificate. + If this step fails, you may have to add the CA certificate. - 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. + 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. - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) - ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) 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. @@ -85,14 +85,14 @@ Create a user with the required permission in your Redis instance. This user wil - Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) ## 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 expiration time of the lease or delete a lease before it's set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) @@ -103,4 +103,4 @@ To extend the life of the generated dynamic secret leases past its initial time Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret - \ No newline at end of file + diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png index 537f20e73..d60f7cd39 100644 Binary files a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png and b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-lease.png b/docs/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-lease.png new file mode 100644 index 000000000..6dfc98229 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-lease.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-modal.png b/docs/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-modal.png new file mode 100644 index 000000000..e3fdfae7c Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-gcp-iam-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png deleted file mode 100644 index db7f8be35..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png b/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png index 4a816614a..769da8608 100644 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png and b/docs/images/platform/dynamic-secrets/dynamic-secret-generate.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png deleted file mode 100644 index a7842b298..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png index e6da94dcd..63d19aa86 100644 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png index 3d2e32e8a..aaacc9c35 100644 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-data.png b/docs/images/platform/dynamic-secrets/lease-data.png index 9562da1b5..646404e85 100644 Binary files a/docs/images/platform/dynamic-secrets/lease-data.png and b/docs/images/platform/dynamic-secrets/lease-data.png differ diff --git a/docs/images/platform/dynamic-secrets/provision-lease.png b/docs/images/platform/dynamic-secrets/provision-lease.png index 96b0505b9..f8652dc05 100644 Binary files a/docs/images/platform/dynamic-secrets/provision-lease.png and b/docs/images/platform/dynamic-secrets/provision-lease.png differ diff --git a/docs/mint.json b/docs/mint.json index a25a70124..52171243b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -197,20 +197,21 @@ "group": "Dynamic Secrets", "pages": [ "documentation/platform/dynamic-secrets/overview", - "documentation/platform/dynamic-secrets/postgresql", - "documentation/platform/dynamic-secrets/mysql", - "documentation/platform/dynamic-secrets/mssql", - "documentation/platform/dynamic-secrets/oracle", - "documentation/platform/dynamic-secrets/cassandra", - "documentation/platform/dynamic-secrets/redis", "documentation/platform/dynamic-secrets/aws-elasticache", - "documentation/platform/dynamic-secrets/elastic-search", - "documentation/platform/dynamic-secrets/rabbit-mq", "documentation/platform/dynamic-secrets/aws-iam", + "documentation/platform/dynamic-secrets/azure-entra-id", + "documentation/platform/dynamic-secrets/cassandra", + "documentation/platform/dynamic-secrets/elastic-search", + "documentation/platform/dynamic-secrets/gcp-iam", + "documentation/platform/dynamic-secrets/ldap", "documentation/platform/dynamic-secrets/mongo-atlas", "documentation/platform/dynamic-secrets/mongo-db", - "documentation/platform/dynamic-secrets/azure-entra-id", - "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/mssql", + "documentation/platform/dynamic-secrets/mysql", + "documentation/platform/dynamic-secrets/oracle", + "documentation/platform/dynamic-secrets/postgresql", + "documentation/platform/dynamic-secrets/rabbit-mq", + "documentation/platform/dynamic-secrets/redis", "documentation/platform/dynamic-secrets/sap-ase", "documentation/platform/dynamic-secrets/sap-hana", "documentation/platform/dynamic-secrets/snowflake", diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 1aedf264f..ad3e50ed2 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -31,7 +31,8 @@ export enum DynamicSecretProviders { SapHana = "sap-hana", Snowflake = "snowflake", Totp = "totp", - SapAse = "sap-ase" + SapAse = "sap-ase", + GcpIam = "gcp-iam" } export enum SqlProviders { @@ -261,6 +262,12 @@ export type TDynamicSecretProvider = algorithm?: string; digits?: number; }; + } + | { + type: DynamicSecretProviders.GcpIam; + inputs: { + serviceAccountEmail: string; + }; }; export type TCreateDynamicSecretDTO = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx index f2458bbf5..b09637a6b 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx @@ -121,7 +121,7 @@ export const AwsIamInputForm = ({ isError={Boolean(error)} errorText={error?.message} > - + )} /> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 106a658d4..b42d8798a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -10,7 +10,7 @@ import { SiSnowflake } from "react-icons/si"; import { VscAzure } from "react-icons/vsc"; -import { faAws } from "@fortawesome/free-brands-svg-icons"; +import { faAws, faGoogle } from "@fortawesome/free-brands-svg-icons"; import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { AnimatePresence, motion } from "framer-motion"; @@ -24,6 +24,7 @@ import { AwsIamInputForm } from "./AwsIamInputForm"; import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm"; import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; +import { GcpIamInputForm } from "./GcpIamInputForm"; import { LdapInputForm } from "./LdapInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; @@ -124,6 +125,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.Totp, title: "TOTP" + }, + { + icon: , + provider: DynamicSecretProviders.GcpIam, + title: "GCP IAM" } ]; @@ -472,6 +478,26 @@ export const CreateDynamicSecretForm = ({ /> )} + + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.GcpIam && ( + + + + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx new file mode 100644 index 000000000..8554a7392 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx @@ -0,0 +1,235 @@ +import { Controller, useForm } from "react-hook-form"; +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 { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; +import { WorkspaceEnv } from "@app/hooks/api/types"; + +const formSchema = z.object({ + provider: z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1 second" }); + if (valMs > 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 1 hour" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 1000) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "TTL must be a greater than 1 second" + }); + if (valMs > 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 1 hour" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environments: WorkspaceEnv[]; + isSingleEnvironmentMode?: boolean; +}; + +export const GcpIamInputForm = ({ + onCompleted, + onCancel, + environments, + secretPath, + projectSlug, + isSingleEnvironmentMode +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + environment: isSingleEnvironmentMode ? environments[0] : undefined + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { + console.log("handleCreateDynamicSecret called"); + + // wait till previous request is finished + if (createDynamicSecret.isPending) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.GcpIam, + inputs: { + ...provider + } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment.slug + }); + onCompleted(); + } catch { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+ +
+ ( + + Don't know where to get this value?{" "} + + Read our docs + + + } + > + + + )} + /> +
+ + {!isSingleEnvironmentMode && ( + ( + + option.name} + getOptionValue={(option) => option.slug} + menuPlacement="top" + /> + + )} + /> + )} +
+
+ +
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 101f41f0d..81908d434 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -335,6 +335,24 @@ const renderOutputForm = ( ); } + if (provider === DynamicSecretProviders.GcpIam) { + const { TOKEN, SERVICE_ACCOUNT_EMAIL } = data as { + SERVICE_ACCOUNT_EMAIL: string; + TOKEN: string; + }; + + return ( +
+ + +
+ ); + } + return null; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx index 78392ea14..49c38b2eb 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx @@ -252,6 +252,7 @@ export const DynamicSecretLease = ({ projectSlug={projectSlug} leaseId={(popUp.renewSecret?.data as { leaseId: string })?.leaseId} dynamicSecretName={dynamicSecretName} + dynamicSecret={dynamicSecret} secretPath={secretPath} environment={environment} /> diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index 971e89e44..435823b02 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -9,6 +9,7 @@ import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm"; import { EditDynamicSecretAzureEntraIdForm } from "./EditDynamicSecretAzureEntraIdForm"; import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm"; import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; +import { EditDynamicSecretGcpIamForm } from "./EditDynamicSecretGcpIamForm"; import { EditDynamicSecretLdapForm } from "./EditDynamicSecretLdapForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm"; @@ -312,6 +313,24 @@ export const EditDynamicSecretForm = ({ /> )} + + {dynamicSecretDetails?.type === DynamicSecretProviders.GcpIam && ( + + + + )} ); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx new file mode 100644 index 000000000..191e3b622 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretGcpIamForm.tsx @@ -0,0 +1,189 @@ +import { Controller, useForm } from "react-hook-form"; +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 { Button, FormControl, Input } 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({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1 second" }); + if (valMs > 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 1 hour" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 1000) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "TTL must be a greater than 1 second" + }); + if (valMs > 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 1 hour" }); + }), + newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; +export const EditDynamicSecretGcpIamForm = ({ + onClose, + dynamicSecret, + secretPath, + environment, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + 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.isPending) 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 { + createNotification({ + type: "error", + text: "Failed to update dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+ + ( + + + + )} + /> +
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx index d17b76cb4..1fa853d0e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx @@ -7,27 +7,23 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input } from "@app/components/v2"; import { useRenewDynamicSecretLease } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; -const formSchema = z.object({ - ttl: z.string().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" }); - }) -}); -type TForm = z.infer; +type TForm = { + ttl: string; +}; type Props = { onClose: () => void; leaseId: string; dynamicSecretName: string; + dynamicSecret: TDynamicSecret; projectSlug: string; environment: string; secretPath: string; + minTtl?: string; // Optional minimum TTL, defaults to 1min + maxTtl?: string; // Optional maximum TTL, defaults to 1day + defaultTtl?: string; // Optional default TTL, defaults to 1h }; export const RenewDynamicSecretLease = ({ @@ -36,8 +32,28 @@ export const RenewDynamicSecretLease = ({ dynamicSecretName, leaseId, secretPath, - environment + environment, + dynamicSecret }: Props) => { + const maxTtlMs = ms(dynamicSecret.maxTTL); + + const formSchema = z.object({ + ttl: z.string().superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "TTL must be greater than 1 second" + }); + if (valMs > maxTtlMs) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `TTL must be less than ${dynamicSecret.maxTTL}` + }); + }) + }); + const { control, formState: { isSubmitting }, @@ -45,7 +61,7 @@ export const RenewDynamicSecretLease = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - ttl: "1h" + ttl: dynamicSecret.defaultTTL } }); @@ -81,7 +97,7 @@ export const RenewDynamicSecretLease = ({ ( }