Skip to content

Commit 8e8e2e0

Browse files
committed
feat(dynamic-secrets): GCP IAM
1 parent 722067f commit 8e8e2e0

31 files changed

+844
-77
lines changed

backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,10 @@ export const dynamicSecretLeaseServiceFactory = ({
227227
const expireAt = new Date(dynamicSecretLease.expireAt.getTime() + ms(selectedTTL));
228228
if (maxTTL) {
229229
const maxExpiryDate = new Date(dynamicSecretLease.createdAt.getTime() + ms(maxTTL));
230-
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max ttl" });
230+
if (expireAt > maxExpiryDate)
231+
throw new BadRequestError({
232+
message: "The requested renewal would exceed the maximum allowed lease duration. Please choose a shorter TTL"
233+
});
231234
}
232235

233236
const { entityId } = await selectedProvider.renew(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { gaxios, Impersonated, JWT } from "google-auth-library";
2+
import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
3+
4+
import { getConfig } from "@app/lib/config/env";
5+
import { BadRequestError, InternalServerError } from "@app/lib/errors";
6+
import { alphaNumericNanoId } from "@app/lib/nanoid";
7+
8+
import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models";
9+
10+
export const GcpIamProvider = (): TDynamicProviderFns => {
11+
const validateProviderInputs = async (inputs: unknown) => {
12+
const providerInputs = await DynamicSecretGcpIamSchema.parseAsync(inputs);
13+
return providerInputs;
14+
};
15+
16+
const $getToken = async (serviceAccountEmail: string, ttl: number): Promise<string> => {
17+
const appCfg = getConfig();
18+
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
19+
throw new InternalServerError({
20+
message: "Environment variable has not been configured: INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL"
21+
});
22+
}
23+
24+
const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as {
25+
client_email: string;
26+
private_key: string;
27+
};
28+
29+
const sourceClient = new JWT({
30+
email: credJson.client_email,
31+
key: credJson.private_key,
32+
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
33+
});
34+
35+
const impersonatedCredentials = new Impersonated({
36+
sourceClient,
37+
targetPrincipal: serviceAccountEmail,
38+
lifetime: ttl,
39+
delegates: [],
40+
targetScopes: ["https://www.googleapis.com/auth/iam", "https://www.googleapis.com/auth/cloud-platform"]
41+
});
42+
43+
let tokenResponse: GetAccessTokenResponse | undefined;
44+
try {
45+
tokenResponse = await impersonatedCredentials.getAccessToken();
46+
} catch (error) {
47+
let message = "Unable to validate connection";
48+
if (error instanceof gaxios.GaxiosError) {
49+
message = error.message;
50+
}
51+
52+
throw new BadRequestError({
53+
message
54+
});
55+
}
56+
57+
if (!tokenResponse || !tokenResponse.token) {
58+
throw new BadRequestError({
59+
message: "Unable to validate connection"
60+
});
61+
}
62+
63+
return tokenResponse.token;
64+
};
65+
66+
const validateConnection = async (inputs: unknown) => {
67+
const providerInputs = await validateProviderInputs(inputs);
68+
await $getToken(providerInputs.serviceAccountEmail, 10);
69+
return true;
70+
};
71+
72+
const create = async (inputs: unknown, expireAt: number) => {
73+
const providerInputs = await validateProviderInputs(inputs);
74+
75+
const now = Math.floor(Date.now() / 1000);
76+
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
77+
78+
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
79+
const entityId = alphaNumericNanoId(32);
80+
81+
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
82+
};
83+
84+
const revoke = async (_inputs: unknown, entityId: string) => {
85+
// There's no way to revoke GCP IAM access tokens
86+
return { entityId };
87+
};
88+
89+
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
90+
// To renew a token it must be re-created
91+
const data = await create(inputs, expireAt);
92+
93+
return { ...data, entityId };
94+
};
95+
96+
return {
97+
validateProviderInputs,
98+
validateConnection,
99+
create,
100+
revoke,
101+
renew
102+
};
103+
};

backend/src/ee/services/dynamic-secret/providers/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AwsIamProvider } from "./aws-iam";
66
import { AzureEntraIDProvider } from "./azure-entra-id";
77
import { CassandraProvider } from "./cassandra";
88
import { ElasticSearchProvider } from "./elastic-search";
9+
import { GcpIamProvider } from "./gcp-iam";
910
import { LdapProvider } from "./ldap";
1011
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
1112
import { MongoAtlasProvider } from "./mongo-atlas";
@@ -38,5 +39,6 @@ export const buildDynamicSecretProviders = ({
3839
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
3940
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
4041
[DynamicSecretProviders.Totp]: TotpProvider(),
41-
[DynamicSecretProviders.SapAse]: SapAseProvider()
42+
[DynamicSecretProviders.SapAse]: SapAseProvider(),
43+
[DynamicSecretProviders.GcpIam]: GcpIamProvider()
4244
});

backend/src/ee/services/dynamic-secret/providers/models.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,10 @@ export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
305305
})
306306
]);
307307

308+
export const DynamicSecretGcpIamSchema = z.object({
309+
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required")
310+
});
311+
308312
export enum DynamicSecretProviders {
309313
SqlDatabase = "sql-database",
310314
Cassandra = "cassandra",
@@ -320,7 +324,8 @@ export enum DynamicSecretProviders {
320324
SapHana = "sap-hana",
321325
Snowflake = "snowflake",
322326
Totp = "totp",
323-
SapAse = "sap-ase"
327+
SapAse = "sap-ase",
328+
GcpIam = "gcp-iam"
324329
}
325330

326331
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -338,7 +343,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
338343
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
339344
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
340345
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
341-
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
346+
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
347+
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema })
342348
]);
343349

344350
export type TDynamicProviderFns = {

docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx

+9-9
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,20 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
9999
![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png)
100100
</Step>
101101
<Step title="Click `Submit`">
102-
After submitting the form, you will see a dynamic secret created in the dashboard.
102+
After submitting the form, you will see a dynamic secret created in the dashboard.
103103

104104
<Note>
105-
If this step fails, you may have to add the CA certificate.
105+
If this step fails, you may have to add the CA certificate.
106106
</Note>
107107

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

114-
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
115-
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
114+
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
115+
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
116116

117117
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.
118118

@@ -123,14 +123,14 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
123123
</Tip>
124124

125125

126-
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
126+
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
127127

128128
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
129129
</Step>
130130
</Steps>
131131

132132
## Audit or Revoke Leases
133-
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.
133+
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.
134134
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
135135

136136
![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
141141

142142
<Warning>
143143
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
144-
</Warning>
144+
</Warning>

docs/documentation/platform/dynamic-secrets/azure-entra-id.mdx

+7-7
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,16 @@ Click on Add assignments. Search for the application name you created and select
123123

124124
</Step>
125125
<Step title="Click `Submit`">
126-
After submitting the form, you will see a dynamic secrets for each user created in the dashboard.
126+
After submitting the form, you will see a dynamic secrets for each user created in the dashboard.
127127
</Step>
128128

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

134-
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
135-
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
134+
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
135+
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
136136

137137
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.
138138

@@ -143,14 +143,14 @@ Click on Add assignments. Search for the application name you created and select
143143
</Tip>
144144

145145

146-
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
146+
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
147147

148148
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ad-lease.png)
149149
</Step>
150150
</Steps>
151151

152152
## Audit or Revoke Leases
153-
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.
153+
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.
154154
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
155155

156156
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)

docs/documentation/platform/dynamic-secrets/elastic-search.mdx

+8-8
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,20 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
8282

8383
</Step>
8484
<Step title="Click `Submit`">
85-
After submitting the form, you will see a dynamic secret created in the dashboard.
85+
After submitting the form, you will see a dynamic secret created in the dashboard.
8686

8787
<Note>
88-
If this step fails, you may have to add the CA certificate.
88+
If this step fails, you may have to add the CA certificate.
8989
</Note>
9090

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

97-
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
98-
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
97+
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
98+
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
9999

100100
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.
101101

@@ -106,14 +106,14 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
106106
</Tip>
107107

108108

109-
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
109+
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
110110

111111
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
112112
</Step>
113113
</Steps>
114114

115115
## Audit or Revoke Leases
116-
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.
116+
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.
117117
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
118118

119119
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)

0 commit comments

Comments
 (0)