Compare commits
89 Commits
daniel/upd
...
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 | |||
27af943ee1 | |||
9b772ad55a | |||
94a1fc2809 | |||
10c10642a1 | |||
3e0f04273c | |||
91f2d0384e | |||
811dc8dd75 | |||
4ee9375a8d | |||
1181c684db | |||
dda436bcd9 | |||
89124b18d2 | |||
effd88c4bd | |||
27efc908e2 | |||
8e4226038b | |||
27425a1a64 | |||
18cf3c89c1 | |||
49e6d7a861 | |||
c4446389b0 | |||
7c21dec54d | |||
2ea5710896 | |||
f9ac7442df | |||
a534a4975c | |||
79a616dc1c | |||
a93bfa69c9 | |||
598d14fc54 | |||
08a0550cd7 | |||
d7503573b1 | |||
b5a89edeed | |||
860eaae4c8 | |||
c7a4b6c4e9 | |||
c12c6dcc6e | |||
99c9b644df | |||
d0d5556bd0 | |||
753c28a2d3 | |||
8741414cfa | |||
b8d29793ec | |||
92013dbfbc | |||
c5319588fe | |||
9efb8eaf78 | |||
dfc973c7f7 | |||
3013d1977c | |||
f358e8942d | |||
58f51411c0 | |||
c3970d1ea2 | |||
2dc00a638a | |||
94aed485a5 | |||
e382941424 | |||
bab9c1f454 | |||
2bd4770fb4 | |||
31905fab6e | |||
784acf16d0 | |||
114b89c952 | |||
81420198cb | |||
0ff18e277f | |||
e093f70301 | |||
8e2ff18f35 | |||
3fbfecf7a9 | |||
9087def21c | |||
586dbd79b0 |
@ -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
|
||||
|
@ -1151,6 +1151,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationAuthId/vercel/custom-environments",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
teamId: z.string().trim()
|
||||
}),
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
environments: z
|
||||
.object({
|
||||
appId: z.string(),
|
||||
customEnvironments: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const environments = await server.services.integrationAuth.getVercelCustomEnvironments({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationAuthId,
|
||||
teamId: req.query.teamId
|
||||
});
|
||||
|
||||
return { environments };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationAuthId/octopus-deploy/spaces",
|
||||
|
@ -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;
|
||||
|
@ -81,11 +81,14 @@ export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig
|
||||
};
|
||||
|
||||
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
||||
const awsConfig = await getAwsConnectionConfig(appConnection);
|
||||
const sts = new AWS.STS(awsConfig);
|
||||
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
|
||||
let resp: AWS.STS.GetCallerIdentityResponse & {
|
||||
$response: AWS.Response<AWS.STS.GetCallerIdentityResponse, AWS.AWSError>;
|
||||
};
|
||||
|
||||
try {
|
||||
const awsConfig = await getAwsConnectionConfig(appConnection);
|
||||
const sts = new AWS.STS(awsConfig);
|
||||
|
||||
resp = await sts.getCallerIdentity().promise();
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
@ -93,7 +96,7 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
||||
});
|
||||
}
|
||||
|
||||
if (resp.$response.httpResponse.statusCode !== 200)
|
||||
if (resp?.$response.httpResponse.statusCode !== 200)
|
||||
throw new InternalServerError({
|
||||
message: `Unable to validate credentials: ${
|
||||
resp.$response.error?.message ??
|
||||
|
@ -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 };
|
||||
|
@ -132,16 +132,26 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
||||
|
||||
/**
|
||||
* Return list of names of apps for Vercel integration
|
||||
* This is re-used for getting custom environments for Vercel
|
||||
*/
|
||||
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
||||
const apps: Array<{ name: string; appId: string }> = [];
|
||||
export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
||||
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
|
||||
|
||||
const limit = "20";
|
||||
let hasMorePages = true;
|
||||
let next: number | null = null;
|
||||
|
||||
interface Response {
|
||||
projects: { name: string; id: string }[];
|
||||
projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
customEnvironments?: {
|
||||
id: string;
|
||||
type: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
}[];
|
||||
}[];
|
||||
pagination: {
|
||||
count: number;
|
||||
next: number | null;
|
||||
@ -173,7 +183,12 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null;
|
||||
data.projects.forEach((a) => {
|
||||
apps.push({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
appId: a.id,
|
||||
customEnvironments:
|
||||
a.customEnvironments?.map((env) => ({
|
||||
slug: env.slug,
|
||||
id: env.id
|
||||
})) ?? []
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -25,11 +25,12 @@ import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { getApps } from "./integration-app-list";
|
||||
import { getApps, getAppsVercel } from "./integration-app-list";
|
||||
import { TCircleCIContext } from "./integration-app-types";
|
||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import {
|
||||
GetVercelCustomEnvironmentsDTO,
|
||||
OctopusDeployScope,
|
||||
TBitbucketEnvironment,
|
||||
TBitbucketWorkspace,
|
||||
@ -1825,6 +1826,41 @@ export const integrationAuthServiceFactory = ({
|
||||
return integrationAuthDAL.create(newIntegrationAuth);
|
||||
};
|
||||
|
||||
const getVercelCustomEnvironments = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
teamId,
|
||||
id
|
||||
}: GetVercelCustomEnvironmentsDTO) => {
|
||||
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: integrationAuth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||
|
||||
const vercelApps = await getAppsVercel({
|
||||
accessToken,
|
||||
teamId
|
||||
});
|
||||
|
||||
return vercelApps.map((app) => ({
|
||||
customEnvironments: app.customEnvironments,
|
||||
appId: app.appId
|
||||
}));
|
||||
};
|
||||
|
||||
const getOctopusDeploySpaces = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@ -1944,6 +1980,7 @@ export const integrationAuthServiceFactory = ({
|
||||
getIntegrationAccessToken,
|
||||
duplicateIntegrationAuth,
|
||||
getOctopusDeploySpaces,
|
||||
getOctopusDeployScopeValues
|
||||
getOctopusDeployScopeValues,
|
||||
getVercelCustomEnvironments
|
||||
};
|
||||
};
|
||||
|
@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = {
|
||||
Self: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetVercelCustomEnvironmentsDTO = {
|
||||
teamId: string;
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({
|
||||
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
const isCustomEnvironment = !["development", "preview", "production"].includes(
|
||||
integration.targetEnvironment as string
|
||||
);
|
||||
interface VercelSecret {
|
||||
id?: string;
|
||||
type: string;
|
||||
customEnvironmentIds?: string[];
|
||||
key: string;
|
||||
value: string;
|
||||
target: string[];
|
||||
@ -1486,6 +1490,16 @@ const syncSecretsVercel = async ({
|
||||
}
|
||||
)
|
||||
).data.envs.filter((secret) => {
|
||||
if (isCustomEnvironment) {
|
||||
if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) {
|
||||
// case: secret does not have the same custom environment
|
||||
return false;
|
||||
}
|
||||
|
||||
// no need to check for preview environment, as custom environments are not available in preview
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!secret.target.includes(integration.targetEnvironment as string)) {
|
||||
// case: secret does not have the same target environment
|
||||
return false;
|
||||
@ -1583,7 +1597,13 @@ const syncSecretsVercel = async ({
|
||||
key,
|
||||
value: infisicalSecrets[key]?.value,
|
||||
type: "encrypted",
|
||||
target: [integration.targetEnvironment as string],
|
||||
...(isCustomEnvironment
|
||||
? {
|
||||
customEnvironmentIds: [integration.targetEnvironment as string]
|
||||
}
|
||||
: {
|
||||
target: [integration.targetEnvironment as string]
|
||||
}),
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
@ -1607,9 +1627,19 @@ const syncSecretsVercel = async ({
|
||||
key,
|
||||
value: infisicalSecrets[key]?.value,
|
||||
type: res[key].type,
|
||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||
? [...res[key].target]
|
||||
: [...res[key].target, integration.targetEnvironment as string],
|
||||
|
||||
...(!isCustomEnvironment
|
||||
? {
|
||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||
? [...res[key].target]
|
||||
: [...res[key].target, integration.targetEnvironment as string]
|
||||
}
|
||||
: {
|
||||
customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string)
|
||||
? [...(res[key].customEnvironmentIds || [])]
|
||||
: [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string]
|
||||
}),
|
||||
|
||||
...(integration.path
|
||||
? {
|
||||
gitBranch: integration.path
|
||||
|
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}"
|
||||
---
|
@ -5,7 +5,7 @@ title: "Node"
|
||||
This guide demonstrates how to use Infisical to manage secrets for your Node stack from local development to production. It uses:
|
||||
|
||||
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets.
|
||||
- The [@infisical/sdk](https://github.com/Infisical/sdk/tree/main/languages/node) Node.js client SDK to fetch secrets back to your Node application on demand.
|
||||
- The [@infisical/sdk](https://github.com/Infisical/node-sdk-v2) Node.js client SDK to fetch secrets back to your Node application on demand.
|
||||
|
||||
## Project Setup
|
||||
|
||||
@ -46,43 +46,57 @@ Finally, create an index.js file containing the application code.
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const { InfisicalClient } = require("@infisical/sdk");
|
||||
const { InfisicalSDK } = require("@infisical/sdk");
|
||||
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = 3000;
|
||||
|
||||
const client = new InfisicalClient({
|
||||
auth: {
|
||||
universalAuth: {
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
clientSecret: "YOUR_CLIENT_SECRET",
|
||||
}
|
||||
}
|
||||
});
|
||||
let client;
|
||||
|
||||
const setupClient = () => {
|
||||
|
||||
if (client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const infisicalSdk = new InfisicalSDK({
|
||||
siteUrl: "your-infisical-instance.com" // Optional, defaults to https://app.infisical.com
|
||||
});
|
||||
|
||||
await infisicalSdk.auth().universalAuth.login({
|
||||
clientId: "<machine-identity-client-id>",
|
||||
clientSecret: "<machine-identity-client-secret>"
|
||||
});
|
||||
|
||||
// If authentication was successful, assign the client
|
||||
client = infisicalSdk;
|
||||
}
|
||||
|
||||
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
// access value
|
||||
|
||||
|
||||
const name = await client.getSecret({
|
||||
environment: "dev",
|
||||
projectId: "PROJECT_ID",
|
||||
path: "/",
|
||||
type: "shared",
|
||||
secretName: "NAME"
|
||||
const name = await client.secrets().getSecret({
|
||||
environment: "dev", // dev, staging, prod, etc.
|
||||
projectId: "<project-id>",
|
||||
secretPath: "/",
|
||||
secretName: "NAME"
|
||||
});
|
||||
|
||||
|
||||
res.send(`Hello! My name is: ${name.secretValue}`);
|
||||
});
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
// initialize client
|
||||
|
||||
console.log(`App listening on port ${PORT}`);
|
||||
// initialize http server and Infisical
|
||||
await setupClient();
|
||||
console.log(`Server listening on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
Here, we initialized a `client` instance of the Infisical Node SDK with the Infisical Token
|
||||
Here, we initialized a `client` instance of the Infisical Node SDK with the [Machine Identity](/documentation/platform/identities/overview)
|
||||
that we created earlier, giving access to the secrets in the development environment of the
|
||||
project in Infisical that we created earlier.
|
||||
|
||||
@ -94,16 +108,12 @@ node index.js
|
||||
|
||||
The client fetched the secret with the key `NAME` from Infisical that we returned in the response of the endpoint.
|
||||
|
||||
At this stage, you know how to fetch secrets from Infisical back to your Node application. By using Infisical Tokens scoped to different environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production.
|
||||
At this stage, you know how to fetch secrets from Infisical back to your Node application.
|
||||
By using Machine Identities scoped to different projects and environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production.
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Isn't it inefficient if my app makes a request every time it needs a secret?">
|
||||
The client SDK caches every secret and implements a 5-minute waiting period before
|
||||
re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at
|
||||
the time of initializing the client.
|
||||
</Accordion>
|
||||
<Accordion title="What if a request for a secret fails?">
|
||||
The SDK caches every secret and falls back to the cached value if a request fails. If no cached
|
||||
value ever-existed, the SDK falls back to whatever value is on `process.env`.
|
||||
@ -124,4 +134,4 @@ At this stage, you know how to fetch secrets from Infisical back to your Node ap
|
||||
|
||||
See also:
|
||||
|
||||
- Explore the [Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node)
|
||||
- Explore the [Node SDK](https://github.com/Infisical/node-sdk-v2)
|
||||
|
@ -3,7 +3,7 @@ title: "Role-based Access Controls"
|
||||
description: "Learn how to use RBAC to manage user permissions."
|
||||
---
|
||||
|
||||
Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles male it possible to restrict access to resources and the range of actions that can be performed.
|
||||
Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles make it possible to restrict access to resources and the range of actions that can be performed.
|
||||
|
||||
In general, access controls can be split up across [projects](/documentation/platform/project) and [organizations](/documentation/platform/organization).
|
||||
|
||||
|
BIN
docs/images/app-connections/aws/access-key-create-policy.png
Normal file
After Width: | Height: | Size: 509 KiB |
BIN
docs/images/app-connections/aws/assume-role-create-policy.png
Normal file
After Width: | Height: | Size: 522 KiB |
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 |