Compare commits
56 Commits
daniel/ver
...
secret-syn
Author | SHA1 | Date | |
---|---|---|---|
|
3328e0850f | ||
|
a5945204ad | ||
|
e99eb47cf4 | ||
|
cf107c0c0d | ||
|
70515a1ca2 | ||
|
955cf9303a | ||
|
a24ef46d7d | ||
|
657aca516f | ||
|
c3d515bb95 | ||
|
7f89a7c860 | ||
|
23cb05c16d | ||
|
d74b819f57 | ||
|
457056b600 | ||
|
7dc9ea4f6a | ||
|
3b4b520d42 | ||
|
23f605bda7 | ||
|
1c3c8dbdce | ||
|
317c95384e | ||
|
7dd959e124 | ||
|
2049e5668f | ||
|
0a3e99b334 | ||
|
c4ad0aa163 | ||
|
5bb0b7a508 | ||
|
96bcd42753 | ||
|
6af7c5c371 | ||
|
72468d5428 | ||
|
939ee892e0 | ||
|
c7ec9ff816 | ||
|
554e268f88 | ||
|
a8a27c3045 | ||
|
3e0f04273c | ||
|
91f2d0384e | ||
|
811dc8dd75 | ||
|
4ee9375a8d | ||
|
1181c684db | ||
|
dda436bcd9 | ||
|
89124b18d2 | ||
|
effd88c4bd | ||
|
27efc908e2 | ||
|
8e4226038b | ||
|
27425a1a64 | ||
|
18cf3c89c1 | ||
|
49e6d7a861 | ||
|
c4446389b0 | ||
|
7c21dec54d | ||
|
2ea5710896 | ||
|
f9ac7442df | ||
|
a534a4975c | ||
|
79a616dc1c | ||
|
598d14fc54 | ||
|
99c9b644df | ||
|
d0d5556bd0 | ||
|
753c28a2d3 | ||
|
58f51411c0 | ||
|
94aed485a5 | ||
|
e382941424 |
@@ -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=
|
||||
|
@@ -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);
|
||||
|
@@ -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 })
|
||||
});
|
||||
|
||||
|
@@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = {
|
||||
projectId?: string;
|
||||
auditLogActorId?: string;
|
||||
actorType?: ActorType;
|
||||
secretPath?: string;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@@ -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.",
|
||||
|
@@ -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(
|
||||
|
@@ -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();
|
||||
|
@@ -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) => {
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
@@ -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
|
||||
};
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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" };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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
|
||||
});
|
@@ -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
|
||||
};
|
||||
|
@@ -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) => {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
export enum AppConnection {
|
||||
GitHub = "github",
|
||||
AWS = "aws"
|
||||
AWS = "aws",
|
||||
GCP = "gcp"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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"
|
||||
};
|
||||
|
@@ -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) =>
|
||||
|
@@ -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)
|
||||
};
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -0,0 +1,3 @@
|
||||
export enum GcpConnectionMethod {
|
||||
ServiceAccountImpersonation = "service-account-impersonation"
|
||||
}
|
164
backend/src/services/app-connection/gcp/gcp-connection-fns.ts
Normal 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;
|
||||
};
|
@@ -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()
|
||||
});
|
@@ -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
|
||||
};
|
||||
};
|
@@ -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";
|
||||
};
|
4
backend/src/services/app-connection/gcp/index.ts
Normal 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";
|
@@ -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);
|
||||
|
@@ -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"
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -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 = {
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
@@ -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 };
|
||||
|
10
backend/src/services/secret-sync/gcp/gcp-sync-constants.ts
Normal 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
|
||||
};
|
3
backend/src/services/secret-sync/gcp/gcp-sync-enums.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export enum GcpSyncScope {
|
||||
Global = "global"
|
||||
}
|
218
backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
45
backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts
Normal 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)
|
||||
});
|
33
backend/src/services/secret-sync/gcp/gcp-sync-types.ts
Normal 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;
|
||||
};
|
||||
};
|
4
backend/src/services/secret-sync/gcp/index.ts
Normal 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";
|
@@ -1,6 +1,7 @@
|
||||
export enum SecretSync {
|
||||
AWSParameterStore = "aws-parameter-store",
|
||||
GitHub = "github"
|
||||
GitHub = "github",
|
||||
GCPSecretManager = "gcp-secret-manager"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -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",
|
||||
|
17
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal 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>
|
@@ -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=
|
||||
|
||||
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/gcp/available"
|
||||
---
|
10
docs/api-reference/endpoints/app-connections/gcp/create.mdx
Normal 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>
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/gcp/{connectionId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/gcp/{connectionId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/gcp/connection-name/{connectionName}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/gcp"
|
||||
---
|
10
docs/api-reference/endpoints/app-connections/gcp/update.mdx
Normal 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>
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager/sync-name/{syncName}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Import Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/import-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/gcp-secret-manager"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/remove-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/gcp-secret-manager/{syncId}/sync-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/gcp-secret-manager/{syncId}"
|
||||
---
|
After Width: | Height: | Size: 632 KiB |
After Width: | Height: | Size: 411 KiB |
After Width: | Height: | Size: 519 KiB |
BIN
docs/images/app-connections/gcp/create-service-account.png
Normal file
After Width: | Height: | Size: 380 KiB |
After Width: | Height: | Size: 978 KiB |
BIN
docs/images/app-connections/gcp/select-gcp-connection.png
Normal file
After Width: | Height: | Size: 580 KiB |
BIN
docs/images/app-connections/gcp/service-account-grant-access.png
Normal file
After Width: | Height: | Size: 669 KiB |
BIN
docs/images/app-connections/gcp/service-account-overview.png
Normal file
After Width: | Height: | Size: 395 KiB |
After Width: | Height: | Size: 472 KiB |
After Width: | Height: | Size: 451 KiB |
After Width: | Height: | Size: 274 KiB |
After Width: | Height: | Size: 288 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 624 KiB |
After Width: | Height: | Size: 626 KiB |
After Width: | Height: | Size: 602 KiB |
After Width: | Height: | Size: 664 KiB |
After Width: | Height: | Size: 608 KiB |
After Width: | Height: | Size: 628 KiB |
88
docs/integrations/app-connections/gcp.mdx
Normal 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">
|
||||

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

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

|
||||
</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">
|
||||

|
||||
</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`
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Configure Service Account Permissions">
|
||||
<Tabs>
|
||||
<Tab title="Secret Sync">
|
||||
Add the required permissions for secret syncs:
|
||||

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

|
||||
</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. 
|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **GCP Connection** option from the connection options modal.
|
||||

|
||||
</Step>
|
||||
<Step title="Authorize Connection">
|
||||
Select the **Service Account Impersonation** method and click **Connect to
|
||||
GCP**. 
|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **GCP Connection** is now available for use. 
|
||||
</Step>
|
||||
</Steps>
|
@@ -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**.
|
||||
|
141
docs/integrations/secret-syncs/gcp-secret-manager.mdx
Normal 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
|
||||

|
||||

|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
|
||||

|
||||
|
||||
2. Select the **GCP Secret Manager** option.
|
||||

|
||||
|
||||
3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
|
||||

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

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

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

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

|
||||
|
||||
8. If enabled, your GCP Secret Manager Sync will begin syncing your secrets to the destination endpoint.
|
||||

|
||||
|
||||
</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>
|
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
231
docs/self-hosting/reference-architectures/on-prem-k8s-ha.mdx
Normal 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
|
||||
{/*  */}
|
||||
```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
|