Compare commits

...

56 Commits

Author SHA1 Message Date
Scott Wilson
3328e0850f improvements: revise descriptions 2025-01-29 09:44:46 -08:00
Scott Wilson
a5945204ad improvements: import behavior clarification and minor integration layout adjustments 2025-01-28 19:09:43 -08:00
Vlad Matsiiako
e99eb47cf4 Merge pull request #3059 from Infisical/minor-doc-adjustments
Improvements: Integration Docs Nav Bar Reorder & Azure Integration Logo fix
2025-01-28 14:14:54 -08:00
Scott Wilson
cf107c0c0d improvements: change integration nav bar order and correct azure integrations image references 2025-01-28 12:51:24 -08:00
Daniel Hougaard
70515a1ca2 Merge pull request #3045 from Infisical/daniel/auditlogs-secret-path-query
feat(audit-logs): query by secret path
2025-01-28 21:17:42 +01:00
Scott Wilson
955cf9303a Merge pull request #3052 from Infisical/set-password-feature
Feature: Setup Password
2025-01-28 12:08:24 -08:00
Daniel Hougaard
a24ef46d7d requested changes 2025-01-28 20:44:45 +01:00
Daniel Hougaard
657aca516f Merge pull request #3049 from Infisical/daniel/vercel-custom-envs
feat(integrations/vercel): custom environments support
2025-01-28 20:38:40 +01:00
Sheen
c3d515bb95 Merge pull request #3039 from Infisical/feat/gcp-secret-sync
feat: gcp app connections and secret sync
2025-01-29 02:23:22 +08:00
Sheen Capadngan
7f89a7c860 Merge remote-tracking branch 'origin/main' into feat/gcp-secret-sync 2025-01-29 01:57:54 +08:00
Sheen Capadngan
23cb05c16d misc: added support for copy suffix 2025-01-29 01:55:15 +08:00
Scott Wilson
d74b819f57 improvements: make logged in status disclaimer in email more prominent and only add email auth method if not already present 2025-01-28 09:53:40 -08:00
Sheen Capadngan
457056b600 misc: added handling for empty values 2025-01-29 01:41:59 +08:00
Maidul Islam
7dc9ea4f6a update notice 2025-01-28 11:48:21 -05:00
Maidul Islam
3b4b520d42 Merge pull request #3055 from Quintasan/patch-1
Update Docker .env examples to reflect `SMTP_FROM` changes
2025-01-28 11:29:07 -05:00
Sheen Capadngan
23f605bda7 misc: added credential hash 2025-01-28 22:37:27 +08:00
Michał Zając
1c3c8dbdce Update Docker .env files to reflect SMT_FROM split 2025-01-28 10:57:09 +00:00
Sheen Capadngan
317c95384e misc: added secondary text 2025-01-28 16:48:06 +08:00
Sheen Capadngan
7dd959e124 misc: readded file 2025-01-28 16:40:17 +08:00
Sheen Capadngan
2049e5668f misc: deleted file 2025-01-28 16:39:05 +08:00
Sheen Capadngan
0a3e99b334 misc: added import support and a few ui/ux updates 2025-01-28 16:36:56 +08:00
Maidul Islam
c4ad0aa163 Merge pull request #3054 from Infisical/infisicalk8s-ha
K8s HA reference docs
2025-01-28 02:56:22 -05:00
Maidul Islam
5bb0b7a508 K8s HA reference docs
A complete guide to k8s HA reference docs
2025-01-28 02:53:02 -05:00
Akhil Mohan
96bcd42753 Merge pull request #3029 from akhilmhdh/feat/min-ttl
Resolved ttl and max ttl to be zero
2025-01-28 12:00:28 +05:30
Scott Wilson
6af7c5c371 improvements: remove removed property reference and remove excess padding/margin on secret sync pages 2025-01-27 19:12:05 -08:00
Scott Wilson
72468d5428 feature: setup password 2025-01-27 18:51:35 -08:00
Daniel Hougaard
939ee892e0 chore: cleanup 2025-01-28 01:02:18 +01:00
Daniel Hougaard
c7ec9ff816 Merge pull request #3050 from Infisical/daniel/k8-logs
feat(k8-operator): better error status
2025-01-27 23:53:23 +01:00
Daniel Hougaard
554e268f88 chore: update helm 2025-01-27 23:51:08 +01:00
Daniel Hougaard
a8a27c3045 feat(k8-operator): better error status 2025-01-27 23:48:20 +01:00
=
3e0f04273c feat: resolved merge conflict 2025-01-28 02:01:24 +05:30
=
91f2d0384e feat: updated router to validate max ttl and ttl 2025-01-28 01:57:15 +05:30
=
811dc8dd75 fix: changed accessTokenMaxTTL in expireAt to accessTokenTTL 2025-01-28 01:57:15 +05:30
=
4ee9375a8d fix: resolved min and max ttl to be zero 2025-01-28 01:57:15 +05:30
Maidul Islam
1181c684db Merge pull request #3036 from Infisical/identity-auth-ui-improvements
Improvement: Overhaul Identity Auth UI Section
2025-01-27 10:51:39 -05:00
Akhil Mohan
dda436bcd9 Merge pull request #3046 from akhilmhdh/fix/breadcrumb-bug-github
fix: resolved github breadcrumb issue
2025-01-27 20:36:06 +05:30
=
89124b18d2 fix: resolved github breadcrumb issue 2025-01-27 20:29:06 +05:30
Sheen Capadngan
effd88c4bd misc: improved doc wording 2025-01-27 22:57:16 +08:00
Daniel Hougaard
27efc908e2 feat(audit-logs): query by secret path 2025-01-27 15:53:07 +01:00
Sheen Capadngan
8e4226038b doc: add api enablement to docs 2025-01-27 22:51:49 +08:00
Sheen Capadngan
27425a1a64 fix: addressed hover effect for secret path input 2025-01-27 22:03:46 +08:00
Sheen Capadngan
18cf3c89c1 misc: renamed enum 2025-01-27 21:47:27 +08:00
Sheen Capadngan
49e6d7a861 misc: finalized endpoint and doc 2025-01-27 21:33:48 +08:00
Sheen Capadngan
c4446389b0 doc: add docs for gcp secret manager secret sync 2025-01-27 20:47:47 +08:00
Sheen Capadngan
7c21dec54d doc: add docs for gcp app connection 2025-01-27 19:32:02 +08:00
Sheen Capadngan
2ea5710896 misc: addressed lint issues 2025-01-27 17:33:01 +08:00
Sheen Capadngan
f9ac7442df misc: added validation against confused deputy 2025-01-27 17:30:26 +08:00
Scott Wilson
a534a4975c chore: revert license 2025-01-24 20:50:54 -08:00
Scott Wilson
79a616dc1c improvements: address feedback 2025-01-24 20:21:21 -08:00
Scott Wilson
598d14fc54 improvement: move edit/delete identity buttons to dropdown 2025-01-24 19:34:03 -08:00
Scott Wilson
99c9b644df improvements: address feedback 2025-01-24 12:55:56 -08:00
Sheen Capadngan
d0d5556bd0 feat: gcp integration sync and removal 2025-01-25 04:04:38 +08:00
Sheen Capadngan
753c28a2d3 feat: gcp secret sync management 2025-01-25 03:01:10 +08:00
Sheen Capadngan
58f51411c0 feat: gcp secret sync 2025-01-24 22:33:56 +08:00
Scott Wilson
94aed485a5 chore: optimize imports 2025-01-23 12:22:40 -08:00
Scott Wilson
e382941424 improvement: overhaul identity auth ui section 2025-01-23 12:18:09 -08:00
186 changed files with 6846 additions and 3649 deletions

View File

@@ -26,7 +26,8 @@ SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST=
SMTP_PORT=
SMTP_NAME=
SMTP_FROM_ADDRESS=
SMTP_FROM_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=
@@ -104,4 +105,7 @@ INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
INF_APP_CONNECTION_GITHUB_APP_SLUG=
INF_APP_CONNECTION_GITHUB_APP_ID=
INF_APP_CONNECTION_GITHUB_APP_ID=
#gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=

View File

@@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
offset = 0,
actorId,
actorType,
secretPath,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
secretPath?: string;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
@@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
});
}
if (projectId && secretPath) {
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]);
}
// Filter by actor type
if (actorType) {
void sqlQuery.where("actor", actorType);

View File

@@ -46,10 +46,6 @@ export const auditLogServiceFactory = ({
actorOrgId
);
/**
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level ✅
*/
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
}
@@ -64,6 +60,7 @@ export const auditLogServiceFactory = ({
actorId: filter.auditLogActorId,
actorType: filter.actorType,
eventMetadata: filter.eventMetadata,
secretPath: filter.secretPath,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
});

View File

@@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = {
projectId?: string;
auditLogActorId?: string;
actorType?: ActorType;
secretPath?: string;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">;

View File

@@ -828,6 +828,8 @@ export const AUDIT_LOGS = {
projectId:
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
eventType: "The type of the event to export.",
secretPath:
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
userAgentType: "Choose which consuming application to export audit logs for.",
eventMetadata:
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",

View File

@@ -201,6 +201,9 @@ const envSchema = z
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
// gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
/* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr(

View File

@@ -116,7 +116,7 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
export const generateHash = (value: string | Buffer) => crypto.createHash("sha256").update(value).digest("hex");
export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair();

View File

@@ -4,18 +4,21 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options
...SanitizedGitHubConnectionSchema.options,
...SanitizedGcpConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema,
GitHubConnectionListItemSchema
GitHubConnectionListItemSchema,
GcpConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,48 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateGcpConnectionSchema,
SanitizedGcpConnectionSchema,
UpdateGcpConnectionSchema
} from "@app/services/app-connection/gcp";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.GCP,
server,
sanitizedResponseSchema: SanitizedGcpConnectionSchema,
createSchema: CreateGcpConnectionSchema,
updateSchema: UpdateGcpConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/secret-manager-projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({ id: z.string(), name: z.string() }).array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.gcp.listSecretManagerProjects(connectionId, req.permission);
return projects;
}
});
};

View File

@@ -1,6 +1,7 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
export * from "./app-connection-router";
@@ -8,5 +9,6 @@ export * from "./app-connection-router";
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
{
[AppConnection.AWS]: registerAwsConnectionRouter,
[AppConnection.GitHub]: registerGitHubConnectionRouter
[AppConnection.GitHub]: registerGitHubConnectionRouter,
[AppConnection.GCP]: registerGcpConnectionRouter
};

View File

@@ -79,44 +79,44 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().trim().describe(AWS_AUTH.ATTACH.identityId)
}),
body: z.object({
stsEndpoint: z
.string()
.trim()
.min(1)
.default("https://sts.amazonaws.com/")
.describe(AWS_AUTH.ATTACH.stsEndpoint),
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns),
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(AWS_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(AWS_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(AWS_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
stsEndpoint: z
.string()
.trim()
.min(1)
.default("https://sts.amazonaws.com/")
.describe(AWS_AUTH.ATTACH.stsEndpoint),
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns),
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(AWS_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(AWS_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(AWS_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityAwsAuth: IdentityAwsAuthsSchema
@@ -172,30 +172,33 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().describe(AWS_AUTH.UPDATE.identityId)
}),
body: z.object({
stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint),
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns),
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(AWS_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
.describe(AWS_AUTH.UPDATE.accessTokenMaxTTL)
}),
body: z
.object({
stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint),
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns),
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(AWS_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.min(0)
.optional()
.describe(AWS_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityAwsAuth: IdentityAwsAuthsSchema

View File

@@ -76,39 +76,44 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
params: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId)
}),
body: z.object({
tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId),
resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource),
allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(AZURE_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId),
resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource),
allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(AZURE_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityAzureAuth: IdentityAzureAuthsSchema
@@ -163,32 +168,40 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
params: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.UPDATE.identityId)
}),
body: z.object({
tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId),
resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource),
allowedServicePrincipalIds: validateAzureAuthField
.optional()
.describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
.describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL)
}),
body: z
.object({
tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId),
resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource),
allowedServicePrincipalIds: validateAzureAuthField
.optional()
.describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.min(0)
.optional()
.describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityAzureAuth: IdentityAzureAuthsSchema

View File

@@ -74,40 +74,40 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().trim().describe(GCP_AUTH.ATTACH.identityId)
}),
body: z.object({
type: z.enum(["iam", "gce"]),
allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts),
allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects),
allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(GCP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(GCP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(GCP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
type: z.enum(["iam", "gce"]),
allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts),
allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects),
allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(GCP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(GCP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(GCP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityGcpAuth: IdentityGcpAuthsSchema
@@ -164,31 +164,34 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().trim().describe(GCP_AUTH.UPDATE.identityId)
}),
body: z.object({
type: z.enum(["iam", "gce"]).optional(),
allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts),
allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects),
allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(GCP_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
.describe(GCP_AUTH.UPDATE.accessTokenMaxTTL)
}),
body: z
.object({
type: z.enum(["iam", "gce"]).optional(),
allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts),
allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects),
allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(GCP_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(GCP_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityGcpAuth: IdentityGcpAuthsSchema

View File

@@ -34,23 +34,12 @@ const CreateBaseSchema = z.object({
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(JWT_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.ATTACH.accessTokenTTL),
accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit)
@@ -70,23 +59,12 @@ const UpdateBaseSchema = z
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(JWT_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.UPDATE.accessTokenTTL),
accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.UPDATE.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(JWT_AUTH.UPDATE.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit)

View File

@@ -87,47 +87,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
params: z.object({
identityId: z.string().trim().describe(KUBERNETES_AUTH.ATTACH.identityId)
}),
body: z.object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
@@ -183,44 +183,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
params: z.object({
identityId: z.string().describe(KUBERNETES_AUTH.UPDATE.identityId)
}),
body: z.object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL)
}),
body: z
.object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema

View File

@@ -87,42 +87,42 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().trim().describe(OIDC_AUTH.ATTACH.identityId)
}),
body: z.object({
oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl),
caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert),
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(OIDC_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl),
caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert),
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(OIDC_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOidcAuth: IdentityOidcAuthResponseSchema
@@ -202,26 +202,24 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
accessTokenTTL: z
.number()
.int()
.min(1)
.min(0)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(OIDC_AUTH.UPDATE.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(OIDC_AUTH.UPDATE.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.UPDATE.accessTokenNumUsesLimit)
})
.partial(),
.partial()
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOidcAuth: IdentityOidcAuthResponseSchema

View File

@@ -26,36 +26,41 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
params: z.object({
identityId: z.string().trim().describe(TOKEN_AUTH.ATTACH.identityId)
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(TOKEN_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(TOKEN_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
@@ -110,27 +115,35 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
params: z.object({
identityId: z.string().trim().describe(TOKEN_AUTH.UPDATE.identityId)
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
.describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL)
}),
body: z
.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema

View File

@@ -86,49 +86,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
params: z.object({
identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId)
}),
body: z.object({
clientSecretTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(1)
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000)
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000)
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit)
}),
body: z
.object({
clientSecretTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityUniversalAuth: IdentityUniversalAuthsSchema
@@ -181,46 +181,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
params: z.object({
identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId)
}),
body: z.object({
clientSecretTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL)
}),
body: z
.object({
clientSecretTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.min(0)
.max(315360000)
.optional()
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityUniversalAuth: IdentityUniversalAuthsSchema

View File

@@ -11,7 +11,7 @@ import {
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
actorType: z.nativeEnum(ActorType).optional(),
secretPath: z
.string()
.optional()
.transform((val) => (!val ? val : removeTrailingSlash(val)))
.describe(AUDIT_LOGS.EXPORT.secretPath),
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()

View File

@@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({
@@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
userId: token.userId
});
return { message: "Successfully updated backup private key" };
return { message: "Successfully reset password" };
}
});
server.route({
method: "POST",
url: "/email/password-setup",
config: {
rateLimit: authRateLimit
},
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.password.sendPasswordSetupEmail(req.permission);
return {
message: "A password setup link has been sent"
};
}
});
server.route({
method: "POST",
url: "/password-setup",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim(),
token: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req, res) => {
await server.services.password.setupPassword(req.body, req.permission);
const appCfg = getConfig();
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully setup password" };
}
});
};

View File

@@ -0,0 +1,13 @@
import { CreateGcpSyncSchema, GcpSyncSchema, UpdateGcpSyncSchema } from "@app/services/secret-sync/gcp";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerGcpSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.GCPSecretManager,
server,
responseSchema: GcpSyncSchema,
createSchema: CreateGcpSyncSchema,
updateSchema: UpdateGcpSyncSchema
});

View File

@@ -1,11 +1,13 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
export * from "./secret-sync-router";
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
[SecretSync.GitHub]: registerGitHubSyncRouter
[SecretSync.GitHub]: registerGitHubSyncRouter,
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
};

View File

@@ -9,13 +9,19 @@ import {
AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema
} from "@app/services/secret-sync/aws-parameter-store";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
const SecretSyncSchema = z.discriminatedUnion("destination", [AwsParameterStoreSyncSchema, GitHubSyncSchema]);
const SecretSyncSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncSchema,
GitHubSyncSchema,
GcpSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncListItemSchema,
GitHubSyncListItemSchema
GitHubSyncListItemSchema,
GcpSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -1,6 +1,7 @@
export enum AppConnection {
GitHub = "github",
AWS = "aws"
AWS = "aws",
GCP = "gcp"
}
export enum AWSRegion {

View File

@@ -1,4 +1,5 @@
import { TAppConnections } from "@app/db/schemas/app-connections";
import { generateHash } from "@app/lib/crypto/encryption";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
@@ -7,6 +8,11 @@ import {
getAwsAppConnectionListItem,
validateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
GcpConnectionMethod,
getGcpAppConnectionListItem,
validateGcpConnectionCredentials
} from "@app/services/app-connection/gcp";
import {
getGitHubConnectionListItem,
GitHubConnectionMethod,
@@ -15,7 +21,9 @@ import {
import { KmsDataKey } from "@app/services/kms/kms-types";
export const listAppConnectionOptions = () => {
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
a.name.localeCompare(b.name)
);
};
export const encryptAppConnectionCredentials = async ({
@@ -69,6 +77,8 @@ export const validateAppConnectionCredentials = async (
return validateAwsConnectionCredentials(appConnection);
case AppConnection.GitHub:
return validateGitHubConnectionCredentials(appConnection);
case AppConnection.GCP:
return validateGcpConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
@@ -85,6 +95,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Access Key";
case AwsConnectionMethod.AssumeRole:
return "Assume Role";
case GcpConnectionMethod.ServiceAccountImpersonation:
return "Service Account Impersonation";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -101,6 +113,7 @@ export const decryptAppConnection = async (
encryptedCredentials: appConnection.encryptedCredentials,
orgId: appConnection.orgId,
kmsService
})
}),
credentialsHash: generateHash(appConnection.encryptedCredentials)
} as TAppConnection;
};

View File

@@ -2,5 +2,6 @@ import { AppConnection } from "./app-connection-enums";
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AWS]: "AWS",
[AppConnection.GitHub]: "GitHub"
[AppConnection.GitHub]: "GitHub",
[AppConnection.GCP]: "GCP"
};

View File

@@ -10,6 +10,8 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
encryptedCredentials: true,
app: true,
method: true
}).extend({
credentialsHash: z.string().optional()
});
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>

View File

@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@@ -26,6 +27,8 @@ import { githubConnectionService } from "@app/services/app-connection/github/git
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@@ -37,7 +40,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -182,6 +186,7 @@ export const appConnectionServiceFactory = ({
return {
...connection,
credentialsHash: generateHash(connection.encryptedCredentials),
credentials: validatedCredentials
};
});
@@ -382,6 +387,7 @@ export const appConnectionServiceFactory = ({
deleteAppConnection,
connectAppConnectionById,
listAvailableAppConnectionsForUser,
github: githubConnectionService(connectAppConnectionById)
github: githubConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById)
};
};

View File

@@ -11,9 +11,11 @@ import {
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
@@ -24,8 +26,9 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
connectionId: string;
};
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
| TValidateGitHubConnectionCredentials;
| TValidateGitHubConnectionCredentials
| TValidateGcpConnectionCredentials;

View File

@@ -0,0 +1,3 @@
export enum GcpConnectionMethod {
ServiceAccountImpersonation = "service-account-impersonation"
}

View File

@@ -0,0 +1,164 @@
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 { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { getAppConnectionMethodName } from "../app-connection-fns";
import { GcpConnectionMethod } from "./gcp-connection-enums";
import {
GCPApp,
GCPGetProjectsRes,
GCPGetServiceRes,
TGcpConnection,
TGcpConnectionConfig
} from "./gcp-connection-types";
export const getGcpAppConnectionListItem = () => {
return {
name: "GCP" as const,
app: AppConnection.GCP as const,
methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation]
};
};
export const getGcpConnectionAuthToken = async (appConnection: TGcpConnectionConfig) => {
const appCfg = getConfig();
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
throw new InternalServerError({
message: `Environment variables have not been configured for GCP ${getAppConnectionMethodName(
GcpConnectionMethod.ServiceAccountImpersonation
)}`
});
}
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: appConnection.credentials.serviceAccountEmail,
lifetime: 3600,
delegates: [],
targetScopes: ["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;
};
export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => {
const accessToken = await getGcpConnectionAuthToken(appConnection);
let gcpApps: GCPApp[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
const projects: {
name: string;
id: string;
}[] = [];
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
// eslint-disable-next-line no-await-in-loop
const { data } = await request.get<GCPGetProjectsRes>(`${IntegrationUrls.GCP_API_URL}/v1/projects`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
gcpApps = gcpApps.concat(data.projects);
if (!data.nextPageToken) {
hasMorePages = false;
}
pageToken = data.nextPageToken;
}
// eslint-disable-next-line
for await (const gcpApp of gcpApps) {
try {
const res = (
await request.get<GCPGetServiceRes>(
`${IntegrationUrls.GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${IntegrationUrls.GCP_SECRET_MANAGER_SERVICE_NAME}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
)
).data;
if (res.state === "ENABLED") {
projects.push({
name: gcpApp.name,
id: gcpApp.projectId
});
}
} catch {
// eslint-disable-next-line
continue;
}
}
return projects;
};
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
// Check if provided service account email suffix matches organization ID.
// We do this to mitigate confused deputy attacks in multi-tenant instances
if (appConnection.credentials.serviceAccountEmail) {
const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-");
const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0];
if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) {
throw new BadRequestError({
message: `GCP service account ID (the part of the email before '@') must have a suffix of "${expectedAccountIdSuffix}"`
});
}
}
await getGcpConnectionAuthToken(appConnection);
return appConnection.credentials;
};

View File

@@ -0,0 +1,65 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { GcpConnectionMethod } from "./gcp-connection-enums";
export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required")
});
const BaseGcpConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GCP) });
export const GcpConnectionSchema = z.intersection(
BaseGcpConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema
})
])
);
export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [
BaseGcpConnectionSchema.extend({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({})
})
]);
export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GCP).credentials
)
})
]);
export const CreateGcpConnectionSchema = ValidateGcpConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.GCP)
);
export const UpdateGcpConnectionSchema = z
.object({
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.GCP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP));
export const GcpConnectionListItemSchema = z.object({
name: z.literal("GCP"),
app: z.literal(AppConnection.GCP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
methods: z.nativeEnum(GcpConnectionMethod).array()
});

View File

@@ -0,0 +1,29 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { getGcpSecretManagerProjects } from "./gcp-connection-fns";
import { TGcpConnection } from "./gcp-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TGcpConnection>;
export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listSecretManagerProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor);
try {
const projects = await getGcpSecretManagerProjects(appConnection);
return projects;
} catch (error) {
return [];
}
};
return {
listSecretManagerProjects
};
};

View File

@@ -0,0 +1,45 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateGcpConnectionSchema,
GcpConnectionSchema,
ValidateGcpConnectionCredentialsSchema
} from "./gcp-connection-schemas";
export type TGcpConnection = z.infer<typeof GcpConnectionSchema>;
export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
app: AppConnection.GCP;
};
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type GCPApp = {
projectNumber: string;
projectId: string;
lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS";
name: string;
createTime: string;
parent: {
type: "organization" | "folder" | "project";
id: string;
};
};
export type GCPGetProjectsRes = {
projects: GCPApp[];
nextPageToken?: string;
};
export type GCPGetServiceRes = {
name: string;
parent: string;
state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED";
};

View File

@@ -0,0 +1,4 @@
export * from "./gcp-connection-enums";
export * from "./gcp-connection-fns";
export * from "./gcp-connection-schemas";
export * from "./gcp-connection-types";

View File

@@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: {
// generate random hex
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_USER_UNLOCK: {
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 259200000);

View File

@@ -6,6 +6,7 @@ export enum TokenType {
TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
TOKEN_USER_UNLOCK = "userUnlock"
}

View File

@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
@@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal";
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
import { AuthTokenType } from "./auth-type";
import {
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;
@@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
userId
userId,
password
}: TResetPasswordViaBackupKeyDTO) => {
const cfg = getConfig();
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
@@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
verifier,
hashedPassword
});
await userDAL.updateById(userId, {
@@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({
return backupKey;
};
const sendPasswordSetupEmail = async (actor: OrgServiceActor) => {
if (actor.type !== ActorType.USER)
throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` });
const user = await userDAL.findById(actor.id);
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
if (!user.isAccepted || !user.authMethods)
throw new BadRequestError({ message: `You must complete signup to set a password` });
const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
userId: user.id
});
const email = user.email ?? user.username;
await smtpService.sendMail({
template: SmtpTemplates.SetupPassword,
recipients: [email],
subjectLine: "Infisical Password Setup",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : ""
}
});
};
const setupPassword = async (
{
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
password,
token
}: TSetupPasswordViaBackupKeyDTO,
actor: OrgServiceActor
) => {
try {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
userId: actor.id,
code: token
});
} catch (e) {
throw new BadRequestError({ message: "Expired or invalid token. Please try again." });
}
await userDAL.transaction(async (tx) => {
const user = await userDAL.findById(actor.id, tx);
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
if (!user.isAccepted || !user.authMethods)
throw new BadRequestError({ message: `You must complete signup to set a password` });
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await userDAL.updateById(
actor.id,
{
authMethods: [...user.authMethods, AuthMethod.EMAIL]
},
tx
);
}
const cfg = getConfig();
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(
actor.id,
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
hashedPassword,
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
});
await tokenService.revokeAllMySessions(actor.id);
};
return {
generateServerPubKey,
changePassword,
@@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({
sendPasswordResetEmail,
verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
setupPassword
};
};

View File

@@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
};
export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
token: string;
};
export type TCreateBackupPrivateKeyDTO = {

View File

@@ -126,12 +126,12 @@ export const identityAwsAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -99,12 +99,12 @@ export const identityAzureAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -138,12 +138,12 @@ export const identityGcpAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -212,12 +212,12 @@ export const identityJwtAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -229,12 +229,12 @@ export const identityKubernetesAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -194,12 +194,12 @@ export const identityOidcAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -328,12 +328,12 @@ export const identityTokenAuthServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg };

View File

@@ -129,12 +129,12 @@ export const identityUaServiceFactory = ({
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "GCP Secret Manager",
destination: SecretSync.GCPSecretManager,
connection: AppConnection.GCP,
canImportSecrets: true
};

View File

@@ -0,0 +1,3 @@
export enum GcpSyncScope {
Global = "global"
}

View File

@@ -0,0 +1,218 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "../secret-sync-errors";
import { TSecretMap } from "../secret-sync-types";
import {
GCPLatestSecretVersionAccess,
GCPSecret,
GCPSMListSecretsRes,
TGcpSyncWithCredentials
} from "./gcp-sync-types";
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
const { destinationConfig } = secretSync;
let gcpSecrets: GCPSecret[] = [];
const pageSize = 100;
let pageToken: string | undefined;
let hasMorePages = true;
while (hasMorePages) {
const params = new URLSearchParams({
pageSize: String(pageSize),
...(pageToken ? { pageToken } : {})
});
// eslint-disable-next-line no-await-in-loop
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
if (secretsRes.secrets) {
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
}
if (!secretsRes.nextPageToken) {
hasMorePages = false;
}
pageToken = secretsRes.nextPageToken;
}
const res: { [key: string]: string } = {};
for await (const gcpSecret of gcpSecrets) {
const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1];
try {
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} catch (error) {
// when a secret in GCP has no versions, we treat it as if it's a blank value
if (error instanceof AxiosError && error.response?.status === 404) {
res[key] = "";
} else {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
return res;
};
export const GcpSyncFns = {
syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig, connection } = secretSync;
const accessToken = await getGcpConnectionAuthToken(connection);
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
for await (const key of Object.keys(secretMap)) {
try {
// we do not process secrets with no value because GCP secret manager does not allow it
if (!secretMap[key].value) {
// eslint-disable-next-line no-continue
continue;
}
if (!(key in gcpSecrets)) {
// case: create secret
await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
{
replication: {
automatic: {}
}
},
{
params: {
secretId: key
},
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secretMap[key].value).toString("base64")
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
for await (const key of Object.keys(gcpSecrets)) {
try {
if (!(key in secretMap) || !secretMap[key].value) {
// case: delete secret
await request.delete(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
} else if (secretMap[key].value !== gcpSecrets[key]) {
if (!secretMap[key].value) {
logger.warn(
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]`
);
}
await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
{
payload: {
data: Buffer.from(secretMap[key].value).toString("base64")
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
},
getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise<TSecretMap> => {
const { connection } = secretSync;
const accessToken = await getGcpConnectionAuthToken(connection);
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
return Object.fromEntries(Object.entries(gcpSecrets).map(([key, value]) => [key, { value: value ?? "" }]));
},
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
const { destinationConfig, connection } = secretSync;
const accessToken = await getGcpConnectionAuthToken(connection);
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
for await (const [key] of Object.entries(gcpSecrets)) {
if (key in secretMap) {
await request.delete(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
}
}
};

View File

@@ -0,0 +1,45 @@
import z from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
import { SecretSync } from "../secret-sync-enums";
import { GcpSyncScope } from "./gcp-sync-enums";
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
const GcpSyncDestinationConfigSchema = z.object({
scope: z.literal(GcpSyncScope.Global),
projectId: z.string().min(1, "Project ID is required")
});
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
destination: z.literal(SecretSync.GCPSecretManager),
destinationConfig: GcpSyncDestinationConfigSchema
});
export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.GCPSecretManager,
GcpSyncOptionsConfig
).extend({
destinationConfig: GcpSyncDestinationConfigSchema
});
export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.GCPSecretManager,
GcpSyncOptionsConfig
).extend({
destinationConfig: GcpSyncDestinationConfigSchema.optional()
});
export const GcpSyncListItemSchema = z.object({
name: z.literal("GCP Secret Manager"),
connection: z.literal(AppConnection.GCP),
destination: z.literal(SecretSync.GCPSecretManager),
canImportSecrets: z.literal(true)
});

View File

@@ -0,0 +1,33 @@
import z from "zod";
import { TGcpConnection } from "@app/services/app-connection/gcp";
import { CreateGcpSyncSchema, GcpSyncListItemSchema, GcpSyncSchema } from "./gcp-sync-schemas";
export type TGcpSyncListItem = z.infer<typeof GcpSyncListItemSchema>;
export type TGcpSync = z.infer<typeof GcpSyncSchema>;
export type TGcpSyncInput = z.infer<typeof CreateGcpSyncSchema>;
export type TGcpSyncWithCredentials = TGcpSync & {
connection: TGcpConnection;
};
export type GCPSecret = {
name: string;
createTime: string;
};
export type GCPSMListSecretsRes = {
secrets?: GCPSecret[];
totalSize?: number;
nextPageToken?: string;
};
export type GCPLatestSecretVersionAccess = {
name: string;
payload: {
data: string;
};
};

View File

@@ -0,0 +1,4 @@
export * from "./gcp-sync-constants";
export * from "./gcp-sync-enums";
export * from "./gcp-sync-schemas";
export * from "./gcp-sync-types";

View File

@@ -1,6 +1,7 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store",
GitHub = "github"
GitHub = "github",
GCPSecretManager = "gcp-secret-manager"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -13,9 +13,13 @@ import {
TSecretSyncWithCredentials
} from "@app/services/secret-sync/secret-sync-types";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -71,6 +75,8 @@ export const SecretSyncFns = {
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -86,6 +92,9 @@ export const SecretSyncFns = {
case SecretSync.GitHub:
secretMap = await GithubSyncFns.getSecrets(secretSync);
break;
case SecretSync.GCPSecretManager:
secretMap = await GcpSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -103,6 +112,8 @@ export const SecretSyncFns = {
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -115,7 +126,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
if (err instanceof SecretSyncError) {
return JSON.stringify({
secretKey: err.secretKey,
error: err.message ?? parseSyncErrorMessage(err.error)
error: err.message || parseSyncErrorMessage(err.error)
});
}

View File

@@ -3,10 +3,12 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
[SecretSync.GitHub]: "GitHub"
[SecretSync.GitHub]: "GitHub",
[SecretSync.GCPSecretManager]: "GCP Secret Manager"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AWSParameterStore]: AppConnection.AWS,
[SecretSync.GitHub]: AppConnection.GitHub
[SecretSync.GitHub]: AppConnection.GitHub,
[SecretSync.GCPSecretManager]: AppConnection.GCP
};

View File

@@ -17,14 +17,18 @@ import {
TAwsParameterStoreSyncListItem,
TAwsParameterStoreSyncWithCredentials
} from "./aws-parameter-store";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
export type TSecretSyncWithCredentials = TAwsParameterStoreSyncWithCredentials | TGitHubSyncWithCredentials;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
| TGitHubSyncWithCredentials
| TGcpSyncWithCredentials;
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput;
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput;
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem;
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@@ -30,6 +30,7 @@ export enum SmtpTemplates {
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",
ResetPassword = "passwordReset.handlebars",
SetupPassword = "passwordSetup.handlebars",
SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars",

View File

@@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Password Setup</title>
</head>
<body>
<h2>Setup your password</h2>
<p>Someone requested to set up a password for your account.</p>
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
<p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body>
</html>

View File

@@ -20,7 +20,8 @@ SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST=
SMTP_PORT=
SMTP_NAME=
SMTP_FROM_ADDRESS=
SMTP_FROM_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/gcp/available"
---

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/gcp"
---
<Note>
Check out the configuration docs for [GCP
Connections](/integrations/app-connections/gcp) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/gcp/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/gcp/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/gcp/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/gcp"
---

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/gcp/{connectionId}"
---
<Note>
Check out the configuration docs for [GCP
Connections](/integrations/app-connections/gcp) to learn how to obtain the
required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

View File

@@ -0,0 +1,88 @@
---
title: "GCP Connection"
description: "Learn how to configure a GCP Connection for Infisical."
---
Infisical supports [service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation) to connect with your GCP projects.
<Accordion title="Self-Hosted Instance">
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.
<Steps>
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
![Service Account Page](/images/app-connections/gcp/service-account-overview.png)
</Step>
<Step title="Create a Service Account">
Create a new service account that will be used to impersonate other GCP service accounts for your app connections.
![Service Account Page](/images/app-connections/gcp/create-instance-service-account.png)
</Step>
<Step title="Generate Service Account Key">
Download the JSON key file for your service account. This will be used to authenticate your instance with GCP.
![Service Account Page](/images/app-connections/gcp/create-service-account-credential.png)
</Step>
<Step title="Configure Your Instance">
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.
</Step>
</Steps>
</Accordion>
## Configure Service Account for Infisical
<Steps>
<Step title="Navigate to IAM & Admin > Service Accounts in Google Cloud Console">
![Service Account Page](/images/app-connections/gcp/service-account-overview.png)
</Step>
<Step title="Create Service Account">
Create a new service account with an ID that follows this requirement:
Your service account ID must end with the first two sections of your Infisical organization ID.
Example:
- Infisical organization ID: `df92581a-0fe9-42b5-b526-0a1e88ec8085`
- Required service account ID suffix: `df92581a-0fe9`
![Create Service Account](/images/app-connections/gcp/create-service-account.png)
</Step>
<Step title="Configure Service Account Permissions">
<Tabs>
<Tab title="Secret Sync">
Add the required permissions for secret syncs:
![Assign Service Account Permission](/images/app-connections/gcp/service-account-secret-sync-permission.png)
</Tab>
</Tabs>
</Step>
<Step title="Enable Service Account Impersonation">
On the new service account, assign the `Service Account Token Creator` role to the Infisical instance's service account. This allows Infisical to impersonate the new service account.
![Service Account Page](/images/app-connections/gcp/service-account-grant-access.png)
</Step>
</Steps>
## Setup GCP Connection in Infisical
<Steps>
<Step title="Navigate to the App Connections">
Navigate to the **App Connections** tab on the **Organization Settings**
page. ![App Connections
Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **GCP Connection** option from the connection options modal.
![Select GCP
Connection](/images/app-connections/gcp/select-gcp-connection.png)
</Step>
<Step title="Authorize Connection">
Select the **Service Account Impersonation** method and click **Connect to
GCP**. ![Connect via GCP
impersonation](/images/app-connections/gcp/create-gcp-impersonation-method.png)
</Step>
<Step title="Connection Created">
Your **GCP Connection** is now available for use. ![Impersonation GCP
Connection](/images/app-connections/gcp/gcp-app-impersonation-connection.png)
</Step>
</Steps>

View File

@@ -38,8 +38,8 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict.
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.

View File

@@ -0,0 +1,141 @@
---
title: "GCP Secret Manager Sync"
description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
---
**Prerequisites:**
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions
- Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png)
![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png)
<Tabs>
<Tab title="Infisical UI">
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png)
2. Select the **GCP Secret Manager** option.
![Select GCP Secret Manager](/images/secret-syncs/gcp-secret-manager/select-gcp-secret-manager-option.png)
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
![Configure Destination](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-destination.png)
- **GCP Connection**: The GCP Connection to authenticate with.
- **Project**: The GCP project to sync with.
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Options](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-options.png)
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.
![Configure Details](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
7. Review your Secret Manager Sync configuration, then click **Create Sync**.
![Confirm Configuration](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-review.png)
8. If enabled, your GCP Secret Manager Sync will begin syncing your secrets to the destination endpoint.
![Sync Secrets](/images/secret-syncs/gcp-secret-manager/gcp-secret-manager-created.png)
</Tab>
<Tab title="API">
To create a **GCP Secret Manager Sync**, make an API request to the [Create GCP
Secret Manager Sync](/api-reference/endpoints/secret-syncs/gcp-secret-manager/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/gcp-secret-manager \
--header 'Content-Type: application/json' \
--data '{
"destinationConfig": {
"scope": "global",
"projectId": "infisical-test-playground"
},
"name": "my-gcp-sync",
"description": "this is an example secret sync",
"secretPath": "/",
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"isAutoSyncEnabled": true,
"connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d",
"environment": "dev",
"projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36"
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "aee02c4a-4a5f-488c-82dd-0b3164772871",
"name": "my-gcp-sync",
"description": "this is an example secret sync",
"isAutoSyncEnabled": true,
"version": 1,
"projectId": "09eda1f8-85a3-47a9-8a6f-e27f133b2a36",
"folderId": "1447389e-16fb-49ba-96fd-361b5a2522af",
"connectionId": "eec83609-5eb4-4d8d-9f6e-ded016984f0d",
"createdAt": "2025-01-27T12:28:59.408Z",
"updatedAt": "2025-01-27T12:28:59.408Z",
"syncStatus": "pending",
"lastSyncJobId": null,
"lastSyncMessage": null,
"lastSyncedAt": null,
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"connection": {
"app": "gcp",
"name": "my-gcp-connection",
"id": "eec83609-5eb4-4d8d-9f6e-ded016984f0d"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "124e0392-4070-4b1c-900e-ced30cd55bf3"
},
"folder": {
"id": "1447389e-16fb-49ba-96fd-361b5a2522af",
"path": "/"
},
"destination": "gcp-secret-manager",
"destinationConfig": {
"projectId": "infisical-test-playground"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -301,7 +301,8 @@
"group": "Reference architectures",
"pages": [
"self-hosting/reference-architectures/aws-ecs",
"self-hosting/reference-architectures/linux-deployment-ha"
"self-hosting/reference-architectures/linux-deployment-ha",
"self-hosting/reference-architectures/on-prem-k8s-ha"
]
},
"self-hosting/ee",
@@ -343,32 +344,6 @@
"cli/faq"
]
},
{
"group": "App Connections",
"pages": [
"integrations/app-connections/overview",
{
"group": "Connections",
"pages": [
"integrations/app-connections/aws",
"integrations/app-connections/github"
]
}
]
},
{
"group": "Secret Syncs",
"pages": [
"integrations/secret-syncs/overview",
{
"group": "Syncs",
"pages": [
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/github"
]
}
]
},
{
"group": "Infrastructure Integrations",
"pages": [
@@ -403,6 +378,34 @@
"integrations/platforms/ansible"
]
},
{
"group": "App Connections",
"pages": [
"integrations/app-connections/overview",
{
"group": "Connections",
"pages": [
"integrations/app-connections/aws",
"integrations/app-connections/github",
"integrations/app-connections/gcp"
]
}
]
},
{
"group": "Secret Syncs",
"pages": [
"integrations/secret-syncs/overview",
{
"group": "Syncs",
"pages": [
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/github",
"integrations/secret-syncs/gcp-secret-manager"
]
}
]
},
{
"group": "Native Integrations",
"pages": [
@@ -798,7 +801,8 @@
"pages": [
"api-reference/endpoints/app-connections/list",
"api-reference/endpoints/app-connections/options",
{ "group": "AWS",
{
"group": "AWS",
"pages": [
"api-reference/endpoints/app-connections/aws/list",
"api-reference/endpoints/app-connections/aws/available",
@@ -809,7 +813,8 @@
"api-reference/endpoints/app-connections/aws/delete"
]
},
{ "group": "GitHub",
{
"group": "GitHub",
"pages": [
"api-reference/endpoints/app-connections/github/list",
"api-reference/endpoints/app-connections/github/available",
@@ -819,6 +824,18 @@
"api-reference/endpoints/app-connections/github/update",
"api-reference/endpoints/app-connections/github/delete"
]
},
{
"group": "GCP",
"pages": [
"api-reference/endpoints/app-connections/gcp/list",
"api-reference/endpoints/app-connections/gcp/available",
"api-reference/endpoints/app-connections/gcp/get-by-id",
"api-reference/endpoints/app-connections/gcp/get-by-name",
"api-reference/endpoints/app-connections/gcp/create",
"api-reference/endpoints/app-connections/gcp/update",
"api-reference/endpoints/app-connections/gcp/delete"
]
}
]
},
@@ -827,7 +844,8 @@
"pages": [
"api-reference/endpoints/secret-syncs/list",
"api-reference/endpoints/secret-syncs/options",
{ "group": "AWS Parameter Store",
{
"group": "AWS Parameter Store",
"pages": [
"api-reference/endpoints/secret-syncs/aws-parameter-store/list",
"api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id",
@@ -840,7 +858,8 @@
"api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets"
]
},
{ "group": "GitHub",
{
"group": "GitHub",
"pages": [
"api-reference/endpoints/secret-syncs/github/list",
"api-reference/endpoints/secret-syncs/github/get-by-id",
@@ -851,6 +870,20 @@
"api-reference/endpoints/secret-syncs/github/sync-secrets",
"api-reference/endpoints/secret-syncs/github/remove-secrets"
]
},
{
"group": "GCP Secret Manager",
"pages": [
"api-reference/endpoints/secret-syncs/gcp-secret-manager/list",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/create",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/update",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/delete",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets",
"api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets"
]
}
]
},

View File

@@ -0,0 +1,231 @@
---
title: "Kubernetes (HA)"
description: "Reference architecture for self-hosting Infisical on Kubernetes (HA)"
---
Deploying Infisical on-premise with high availability requires expertise in networking, container orchestration, and database management.
This guide serves as a reference architecture and a starting point. Actual deployments may vary depending on your organization's existing infrastructure and capabilities.
## Architecture Overview
{/* ![On premise architecture](/images/self-hosting/reference-architectures/on-premise-architecture.png) */}
```mermaid
flowchart TB
subgraph GLB["Global LB (HAProxy/NGINX)"]
end
subgraph OS["Object Storage"]
direction LR
store["S3/MinIO/Enterprise Storage"]
subgraph store_contents["Storage Contents"]
wal["PostgreSQL WAL"]
pgbackup["PostgreSQL Backups"]
redisbackup["Redis Backups"]
end
end
subgraph DC1["Active Data Center"]
direction TB
subgraph k8s1["Kubernetes Cluster"]
ing1["Ingress Controller"]
app1["Infisical Deployment"]
subgraph db1["CloudNativePG"]
pg1p["PostgreSQL Primary"]
pg1r["PostgreSQL Replicas"]
end
subgraph red1["Redis (Bitnami)"]
rp1["Redis Primary"]
end
end
end
subgraph DC2["Passive Data Center"]
direction TB
subgraph k8s2["Kubernetes Cluster"]
ing2["Ingress Controller"]
app2["Infisical Deployment"]
subgraph db2["CloudNativePG"]
pg2["PostgreSQL Replicas"]
end
subgraph red2["Redis (Bitnami)"]
r2["Redis Standby"]
end
end
end
%% Connections
GLB --> ing1
GLB -.-> ing2
%% Database connections
pg1p --> store
store --> pg2
%% Redis backup flow
rp1 --> store
store -.-> r2
%% Intra-DC connections
ing1 --> app1
app1 --> db1
app1 --> red1
ing2 --> app2
app2 --> db2
app2 --> red2
classDef primary fill:#f96,stroke:#333
classDef replica fill:#69f,stroke:#333
classDef storage fill:#9c6,stroke:#333
classDef lb fill:#c9f,stroke:#333
class pg1p,rp1 primary
class pg1r,pg2,r2 replica
class store,wal,pgbackup,redisbackup storage
class GLB,ing1,ing2 lb
```
The architecture above makes use of Kubernetes for orchestrating both stateless and stateful components.
The architecture spans multiple data centers for increased redundancy, availability and disaster recovery capabilities using an active-passive configuration.
### Stateful vs stateless workloads
While managing databases within Kubernetes has typically been complex, modern operators like [CloudNativePG](https://cloudnative-pg.io/) simplify this process by handling storage provisioning, persistent volume management, and backup/recovery processes.
However, if you lack deep expertise in Kubernetes operators or database management, we recommend a hybrid approach where the database is on a managed service for production deployments.
<Warning>
Managing stateful components like databases can be challenging without deep expertise or a dedicated in-house database management team.
To simplify operations and reduce complexity, we recommend offloading databases to managed services from AWS/GCP.
These managed services automatically handle provisioning, scaling, failover, backups and rollbacks.
</Warning>
## Core Components
### Kubernetes Cluster
Infisical is deployed on a Kubernetes cluster, which allows for container management, auto-scaling, and self-healing capabilities.
A load balancer sits in front of the Kubernetes cluster, directing traffic and making sure there is an even load distribution across the application nodes.
This is the entry point where all other services will interact with Infisical.
### Object Storage
The architecture requires S3-compatible object storage for database backups and cross-datacenter replication. This can be provided by:
- Existing enterprise object storage solution
- Dedicated MinIO deployment
- In-cluster MinIO deployment if neither option above is available
The object storage must be accessible from all Kubernetes clusters and provides:
- Storage for PostgreSQL WAL archiving and backups
- Storage for Redis backups
### CloudNativePG for High Availability PostgreSQL
The database layer is powered by PostgreSQL, managed by CloudNativePG operator for high availability:
- **Redundancy:** CloudNativePG manages a primary-replica setup where the primary handles write operations and replicas handle read operations
- **Failover:** The operator automatically handles failover within a cluster by promoting a replica to primary when needed
- **Backup and Recovery:** Built-in support for backup to S3-compatible storage with point-in-time recovery capabilities
### Redis High Availability
Redis is deployed using the [Bitnami Helm chart](https://github.com/bitnami/charts/tree/main/bitnami/redis) in a simple primary configuration:
- Single Redis instance per cluster without streaming replication
- Regular backups to object storage
- Restore from backup during failover
<Note>
Infisical does not support Redis cluster mode, and since this is an active-passive setup, we use a simple Redis deployment with backup/restore for failover.
</Note>
#### PostgreSQL Backup and Restore
PostgreSQL is the single source of truth for nearly all application data on Infisical.
CloudNativePG provides well defined backup and restore capabilities:
- **Continuous Backup:** The operator continuously archives WAL files to object storage
- **Point-in-Time Recovery:** Supports restoring to any point in time using WAL archiving
- **Regular Testing:** Periodically test backup restoration to exercise the full lifecycle of this process
#### Redis Backup and Restore
Each Redis instance is backed up through a Kubernetes CronJob that:
1. Executes the Redis `SAVE` command
2. Copies the resulting `dump.rdb` to object storage
3. Manages backup retention
<Accordion title="Example Redis backup CronJob">
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: redis-backup
spec:
schedule: "0 * * * *" # Every hour
jobTemplate:
spec:
template:
spec:
containers:
- name: redis-backup
image: bitnami/redis
command:
- /bin/sh
- -c
- |
redis-cli -a $REDIS_PASSWORD save
mc cp /data/dump.rdb object-store/redis-backups/
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-data
```
</Accordion>
During failover, the latest Redis backup is restored from object storage to the passive data center. This process is manual and requires operator intervention.
## Multi Data Center Deployment
Infisical can be deployed across multiple data centers in an active-passive configuration for disaster recovery. In this setup, one data center serves as the active site while others remain as passive standbys.
### Active Data Center
The active data center contains:
- The primary PostgreSQL cluster managed by CloudNativePG handling all write operations
- The active Redis instance handling all traffic
- The active Infisical deployment serving all user traffic
### Passive Data Centers
Passive data centers act as disaster recovery sites. Each contains:
- A replica PostgreSQL cluster that replicates from the active site's primary cluster
- A standby Redis instance (not receiving traffic)
- A standby Infisical deployment (not receiving traffic)
### Traffic Management and Failover
Traffic routing between data centers requires:
1. A global load balancer for traffic management. For on-premises deployments, this can be implemented using:
- HAProxy or NGINX configured as a global load balancer
- Any enterprise network routing solutions you may already have in place
2. Each data center should have its own ingress or load balancer
The global load balancer should be deployed in a highly available configuration across multiple locations to avoid it becoming a single point of failure.
During normal operation:
- The global load balancer routes all traffic to the active data center
- Replica PostgreSQL clusters continuously replicate from the primary cluster
- Redis backups are regularly created and stored in object storage
During failover:
- A human operator must initiate the failover process
- The operator promotes a replica PostgreSQL cluster in the target passive data center to become primary using CloudNativePG's promotion process
- The latest Redis backup is restored from object storage to the passive data center's Redis instance
- Once database failover is complete, the global load balancer is updated to direct traffic to the new active data center
<Note>
This is an active-passive setup where failover must be initiated manually by an operator. Automatic failover between data centers is not recommended as it can lead to split-brain scenarios. The operator should verify the state of both data centers before initiating failover.
</Note>
## Data Replication Across Data Centers
### PostgreSQL Replication
CloudNativePG manages replication across data centers:
- **Replica Clusters:** Each data center runs a replica cluster that replicates from the primary cluster
- **WAL Shipping:** Changes are replicated via WAL shipping to object storage
- **Failover:** The operator can promote a replica cluster to primary during planned switchovers or failures
### Object Storage Configuration
If using MinIO for object storage, ensure:
- High availability deployment if running dedicated MinIO cluster
- Proper access controls and encryption for data at rest
- Regular monitoring of storage capacity and performance
- Backup of object storage data itself if running your own MinIO deployment

Some files were not shown because too many files have changed in this diff Show More