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 4b72ff7e9..106f72334 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 @@ -264,7 +264,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..b5d34aa49 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/gcp-iam.ts @@ -0,0 +1,105 @@ +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 (data: { inputs: unknown; expireAt: number }) => { + const { inputs, expireAt } = data; + + 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 76ef7ef2a..7e14cf1ab 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 { KubernetesProvider } from "./kubernetes"; import { LdapProvider } from "./ldap"; import { DynamicSecretProviders, TDynamicProviderFns } from "./models"; @@ -42,5 +43,6 @@ export const buildDynamicSecretProviders = ({ [DynamicSecretProviders.Totp]: TotpProvider(), [DynamicSecretProviders.SapAse]: SapAseProvider(), [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }), - [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }) + [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }), + [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 32cc46d22..8f361e166 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -470,6 +470,10 @@ export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [ }) ]); +export const DynamicSecretGcpIamSchema = z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128) +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -487,7 +491,8 @@ export enum DynamicSecretProviders { Totp = "totp", SapAse = "sap-ase", Kubernetes = "kubernetes", - Vertica = "vertica" + Vertica = "vertica", + GcpIam = "gcp-iam" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -507,7 +512,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }), - z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }) + z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }), + 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 6c70612c8..92851cd03 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -135,15 +135,15 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa 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. + 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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx index 515efabeb..cedeb31c2 100644 --- a/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx +++ b/docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx @@ -123,35 +123,35 @@ 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 secret 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. + 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. + Ensure that the TTL for the lease falls 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. + 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. -This will allow you to see the expiration time of the lease or delete a lease before it's set time to live. +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 its set time to live. ![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) diff --git a/docs/documentation/platform/dynamic-secrets/cassandra.mdx b/docs/documentation/platform/dynamic-secrets/cassandra.mdx index 56b7ec336..16cca4dfe 100644 --- a/docs/documentation/platform/dynamic-secrets/cassandra.mdx +++ b/docs/documentation/platform/dynamic-secrets/cassandra.mdx @@ -128,7 +128,7 @@ The above configuration allows user creation and granting permissions. ![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 in step 4. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index 06d1102e2..f1c5f1128 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -127,15 +127,15 @@ The port that your Elasticsearch instance is running on. _(Example: 9200)_ 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. ![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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. 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..406021fa8 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/gcp-iam.mdx @@ -0,0 +1,145 @@ +--- +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. + + + GCP service account access tokens cannot be revoked. As such, revoking or regenerating a token does not invalidate the old one; it remains active until it expires. + + + + 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 secret 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 falls 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 its 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/kubernetes.mdx b/docs/documentation/platform/dynamic-secrets/kubernetes.mdx index 87c5b3e89..a7d16b111 100644 --- a/docs/documentation/platform/dynamic-secrets/kubernetes.mdx +++ b/docs/documentation/platform/dynamic-secrets/kubernetes.mdx @@ -492,7 +492,7 @@ When generating these secrets, it's important to specify a Time-to-Live (TTL) du ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) - Ensure that the TTL for the lease fall within the maximum TTL defined when + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/ldap.mdx b/docs/documentation/platform/dynamic-secrets/ldap.mdx index ddaaf9101..a113ec344 100644 --- a/docs/documentation/platform/dynamic-secrets/ldap.mdx +++ b/docs/documentation/platform/dynamic-secrets/ldap.mdx @@ -156,15 +156,15 @@ 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. + 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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. @@ -259,15 +259,15 @@ 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. + 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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx index 7c4ebb568..6753b8f0e 100644 --- a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx +++ b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx @@ -109,12 +109,12 @@ Create a user with the required permission in your MongoDB instance. This user w ![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. + 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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx index 6b6cef982..8bb942314 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -119,7 +119,7 @@ Create a user with the required permission in your SQL instance. This user will ![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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx index 33d11a4fc..c354dfe39 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -110,12 +110,12 @@ Create a user with the required permission in your SQL instance. This user will ![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. + 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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx index 3c83f3359..2d6193abd 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -117,7 +117,7 @@ Create a user with the required permission in your SQL instance. This user will ![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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index 974066e14..1f5c79f1e 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -121,7 +121,7 @@ Create a user with the required permission in your SQL instance. This user will ![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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx index be41901b7..cac5ac120 100644 --- a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -109,15 +109,15 @@ The port that the RabbitMQ management plugin is listening on. This is `15672` by 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. ![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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index 8e28c3cb2..583bd1d87 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -97,15 +97,15 @@ Create a user with the required permission in your Redis instance. This user wil 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. + 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. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret. diff --git a/docs/documentation/platform/dynamic-secrets/sap-ase.mdx b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx index 6da572f35..8009c779c 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-ase.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-ase.mdx @@ -112,7 +112,7 @@ Due to SAP ASE limitations, the attached SQL statements are not executed as a tr ![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 in step 4. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. diff --git a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx index 8ccd842f2..ce016e681 100644 --- a/docs/documentation/platform/dynamic-secrets/sap-hana.mdx +++ b/docs/documentation/platform/dynamic-secrets/sap-hana.mdx @@ -119,7 +119,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c ![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 in step 4. + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. diff --git a/docs/documentation/platform/dynamic-secrets/snowflake.mdx b/docs/documentation/platform/dynamic-secrets/snowflake.mdx index f0bbaa08c..10d7c182f 100644 --- a/docs/documentation/platform/dynamic-secrets/snowflake.mdx +++ b/docs/documentation/platform/dynamic-secrets/snowflake.mdx @@ -127,7 +127,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede ![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 in + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. 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 3382e371e..0596b8025 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -207,20 +207,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 2357a83c0..e8d80e632 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -34,7 +34,8 @@ export enum DynamicSecretProviders { Totp = "totp", SapAse = "sap-ase", Kubernetes = "kubernetes", - Vertica = "vertica" + Vertica = "vertica", + GcpIam = "gcp-iam" } export enum KubernetesDynamicSecretCredentialType { @@ -326,6 +327,12 @@ export type TDynamicSecretProvider = creationStatement: string; revocationStatement: string; }; + } + | { + type: DynamicSecretProviders.GcpIam; + inputs: { + serviceAccountEmail: string; + }; }; export type TCreateDynamicSecretDTO = { 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 5a73fa6d8..f67342190 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 @@ -11,7 +11,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"; @@ -25,6 +25,7 @@ import { AwsIamInputForm } from "./AwsIamInputForm"; import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm"; import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; +import { GcpIamInputForm } from "./GcpIamInputForm"; import { KubernetesInputForm } from "./KubernetesInputForm"; import { LdapInputForm } from "./LdapInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; @@ -137,6 +138,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.Kubernetes, title: "Kubernetes" + }, + { + icon: , + provider: DynamicSecretProviders.GcpIam, + title: "GCP IAM" } ]; @@ -523,6 +529,25 @@ 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..f47273aa1 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/GcpIamInputForm.tsx @@ -0,0 +1,237 @@ +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 validateTTL = (val: string, ctx: z.RefinementCtx) => { + if (!val) return; + const valMs = ms(val); + if (valMs === undefined) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid TTL format" }); + return; + } + 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" }); +}; + +const formSchema = z + .object({ + provider: z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") + }), + defaultTTL: z.string().superRefine(validateTTL), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (val) validateTTL(val, ctx); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"), + environment: z.object({ name: z.string(), slug: z.string() }) + }) + .refine((d) => !d.maxTTL || ms(d.maxTTL)! >= ms(d.defaultTTL)!, { + path: ["maxTTL"], + message: "Max TTL must be greater than or equal to Default TTL" + }); +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.length > 0 ? environments[0] : undefined + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ + name, + maxTTL, + provider, + defaultTTL, + environment + }: TForm) => { + // 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 0f56061f5..1bdf94665 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -350,6 +350,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 0f28e4565..11589d531 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 { EditDynamicSecretKubernetesForm } from "./EditDynamicSecretKubernetesForm"; import { EditDynamicSecretLdapForm } from "./EditDynamicSecretLdapForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; @@ -348,6 +349,23 @@ 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..780724a6b --- /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 validateTTL = (val: string, ctx: z.RefinementCtx) => { + if (!val) return; + const valMs = ms(val); + if (valMs === undefined) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid TTL format" }); + return; + } + 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" }); +}; + +const formSchema = z + .object({ + inputs: z.object({ + serviceAccountEmail: z.string().email().trim().min(1, "Service account email required") + }), + defaultTTL: z.string().superRefine(validateTTL), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (val) validateTTL(val, ctx); + }), + newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") + }) + .refine((d) => !d.maxTTL || ms(d.maxTTL)! >= ms(d.defaultTTL)!, { + path: ["maxTTL"], + message: "Max TTL must be greater than or equal to Default TTL" + }); +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..9cee0a671 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx @@ -7,24 +7,13 @@ 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"; - -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; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; type Props = { onClose: () => void; leaseId: string; dynamicSecretName: string; + dynamicSecret: TDynamicSecret; projectSlug: string; environment: string; secretPath: string; @@ -36,8 +25,30 @@ 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 < 1000) + 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}` + }); + }) + }); + + type TForm = z.infer; + const { control, formState: { isSubmitting }, @@ -45,7 +56,7 @@ export const RenewDynamicSecretLease = ({ } = useForm({ resolver: zodResolver(formSchema), defaultValues: { - ttl: "1h" + ttl: dynamicSecret.defaultTTL } }); @@ -81,7 +92,7 @@ export const RenewDynamicSecretLease = ({ ( }