mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-22 13:29:55 +00:00
Compare commits
96 Commits
org-access
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
f8c822eda7 | |||
ea5a5e0aa7 | |||
f20e4e189d | |||
c7ec6236e1 | |||
c4dea2d51f | |||
e89b0fdf3f | |||
d57f76d230 | |||
7ba79dec19 | |||
6ea8bff224 | |||
65f4e1bea1 | |||
73ce3b8bb7 | |||
e63af81e60 | |||
6c2c2b319b | |||
82c2be64a1 | |||
051d0780a8 | |||
5406871c30 | |||
8b89edc277 | |||
b394e191a8 | |||
92030884ec | |||
4583eb1732 | |||
4c8bf9bd92 | |||
a6554deb80 | |||
ae00e74c17 | |||
adfd5a1b59 | |||
d6c321d34d | |||
09a7346f32 | |||
e4abac91b4 | |||
b4f37193ac | |||
c8be5a637a | |||
45485f8bd3 | |||
766254c4e3 | |||
4c22024d13 | |||
4bd1eb6f70 | |||
6847e5bb89 | |||
022ecf75e1 | |||
5d35ce6c6c | |||
635f027752 | |||
ce170a6a47 | |||
cb8e36ae15 | |||
16ce1f441e | |||
8043b61c9f | |||
d374ff2093 | |||
eb7c533261 | |||
ac5bfbb6c9 | |||
1f80ff040d | |||
9a935c9177 | |||
f8939835e1 | |||
9d24eb15dc | |||
ed4882dfac | |||
7acd7fd522 | |||
2148b636f5 | |||
e40b4a0a4b | |||
d2b0ca94d8 | |||
5255f0ac17 | |||
311bf8b515 | |||
4f67834eaa | |||
78c4c3e847 | |||
b8aa36be99 | |||
594445814a | |||
a467b13069 | |||
c425c03939 | |||
9cc17452fa | |||
93ba6f7b58 | |||
0fcb66e9ab | |||
135f425fcf | |||
9c149cb4bf | |||
ce45c1a43d | |||
1a14c71564 | |||
e7fe2ea51e | |||
caa129b565 | |||
30d7e63a67 | |||
a4c21d85ac | |||
c34a139b19 | |||
f2a55da9b6 | |||
a3584d6a8a | |||
36f1559e5e | |||
07902f7db9 | |||
6fddecdf82 | |||
99e2c85f8f | |||
6e1504dc73 | |||
07d930f608 | |||
696bbcb072 | |||
54435d0ad9 | |||
952e60f08a | |||
6c52847dec | |||
698260cba6 | |||
5367d1ac2e | |||
92b9abb52b | |||
e2680d9aee | |||
aa049dc43b | |||
419e9ac755 | |||
b7b36a475d | |||
0fbf8efd3a | |||
9159a9fa36 | |||
6ae7b5e996 | |||
400157a468 |
@ -40,3 +40,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:578
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:579
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:581
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:582
|
||||
backend/src/services/smtp/smtp-service.ts:generic-api-key:79
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@ -119,6 +119,10 @@ declare module "@fastify/request-context" {
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
kubernetes?: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode");
|
||||
|
||||
if (!hasTokenReviewModeColumn) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.string("tokenReviewMode").notNullable().defaultTo("api");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasTokenReviewModeColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "tokenReviewMode");
|
||||
|
||||
if (hasTokenReviewModeColumn) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.dropColumn("tokenReviewMode");
|
||||
});
|
||||
}
|
||||
}
|
@ -31,7 +31,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
tokenReviewMode: z.string().default("api")
|
||||
});
|
||||
|
||||
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||
|
@ -23,7 +23,10 @@ const validateUsernameTemplateCharacters = characterValidator([
|
||||
CharacterType.CloseBrace,
|
||||
CharacterType.CloseBracket,
|
||||
CharacterType.OpenBracket,
|
||||
CharacterType.Fullstop
|
||||
CharacterType.Fullstop,
|
||||
CharacterType.SingleQuote,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Pipe
|
||||
]);
|
||||
|
||||
const userTemplateSchema = z
|
||||
@ -33,7 +36,7 @@ const userTemplateSchema = z
|
||||
.refine((el) => validateUsernameTemplateCharacters(el))
|
||||
.refine((el) =>
|
||||
isValidHandleBarTemplate(el, {
|
||||
allowedExpressions: (val) => ["randomUsername", "unixTimestamp"].includes(val)
|
||||
allowedExpressions: (val) => ["randomUsername", "unixTimestamp", "identity.name"].includes(val)
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -99,7 +99,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
||||
) as object;
|
||||
|
||||
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
||||
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, {
|
||||
projectId: folder.projectId
|
||||
});
|
||||
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||
return;
|
||||
}
|
||||
@ -133,7 +135,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||
await Promise.all(
|
||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId)
|
||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
|
||||
projectId: folder.projectId
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@ -11,10 +12,13 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||
@ -39,6 +43,8 @@ type TDynamicSecretLeaseServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||
@ -52,8 +58,16 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
userDAL,
|
||||
identityDAL
|
||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
||||
const extractEmailUsername = (email: string) => {
|
||||
const regex = new RE2(/^([^@]+)/);
|
||||
const match = email.match(regex);
|
||||
return match ? match[1] : email;
|
||||
};
|
||||
|
||||
const create = async ({
|
||||
environmentSlug,
|
||||
path,
|
||||
@ -132,10 +146,24 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
|
||||
let result;
|
||||
try {
|
||||
const identity: { name: string } = { name: "" };
|
||||
if (actor === ActorType.USER) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
if (user) {
|
||||
identity.name = extractEmailUsername(user.username);
|
||||
}
|
||||
} else if (actor === ActorType.Machine) {
|
||||
const machineIdentity = await identityDAL.findById(actorId);
|
||||
if (machineIdentity) {
|
||||
identity.name = machineIdentity.name;
|
||||
}
|
||||
}
|
||||
result = await selectedProvider.create({
|
||||
inputs: decryptedStoredInput,
|
||||
expireAt: expireAt.getTime(),
|
||||
usernameTemplate: dynamicSecretCfg.usernameTemplate
|
||||
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
||||
identity,
|
||||
metadata: { projectId }
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||
@ -237,7 +265,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
const { entityId } = await selectedProvider.renew(
|
||||
decryptedStoredInput,
|
||||
dynamicSecretLease.externalEntityId,
|
||||
expireAt.getTime()
|
||||
expireAt.getTime(),
|
||||
{ projectId }
|
||||
);
|
||||
|
||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||
@ -313,7 +342,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
) as object;
|
||||
|
||||
const revokeResponse = await selectedProvider
|
||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
|
||||
.catch(async (err) => {
|
||||
// only propogate this error if forced is false
|
||||
if (!isForced) return { error: err as Error };
|
||||
|
@ -116,7 +116,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
|
||||
@ -146,7 +146,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
selectedGatewayId = gateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId });
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
@ -272,7 +272,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
||||
) as object;
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
||||
@ -301,7 +301,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
selectedGatewayId = gateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
const isConnected = await selectedProvider.validateConnection(newInput, { projectId });
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
@ -472,7 +472,9 @@ export const dynamicSecretServiceFactory = ({
|
||||
secretManagerDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedInput }).toString()
|
||||
) as object;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput, {
|
||||
projectId
|
||||
})) as object;
|
||||
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const CreateElastiCacheUserSchema = z.object({
|
||||
UserId: z.string().trim().min(1),
|
||||
@ -132,14 +133,14 @@ const generatePassword = () => {
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
|
||||
const randomUsername = `inf-${customAlphabet(charset, 32)()}`;
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -174,14 +175,21 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: {
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!(await validateConnection(providerInputs))) {
|
||||
throw new BadRequestError({ message: "Failed to establish connection" });
|
||||
}
|
||||
|
||||
const leaseUsername = generateUsername(usernameTemplate);
|
||||
const leaseUsername = generateUsername(usernameTemplate, identity);
|
||||
const leasePassword = generatePassword();
|
||||
const leaseExpiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@ -16,21 +16,25 @@ import {
|
||||
PutUserPolicyCommand,
|
||||
RemoveUserFromGroupCommand
|
||||
} from "@aws-sdk/client-iam";
|
||||
import handlebars from "handlebars";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { randomUUID } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -40,7 +44,43 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>, projectId: string) => {
|
||||
const appCfg = getConfig();
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
credentials:
|
||||
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
: undefined // if hosting on AWS
|
||||
});
|
||||
|
||||
const command = new AssumeRoleCommand({
|
||||
RoleArn: providerInputs.roleArn,
|
||||
RoleSessionName: `infisical-dynamic-secret-${randomUUID()}`,
|
||||
DurationSeconds: 900, // 15 mins
|
||||
ExternalId: projectId
|
||||
});
|
||||
|
||||
const assumeRes = await stsClient.send(command);
|
||||
|
||||
if (!assumeRes.Credentials?.AccessKeyId || !assumeRes.Credentials?.SecretAccessKey) {
|
||||
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
||||
}
|
||||
const client = new IAMClient({
|
||||
region: providerInputs.region,
|
||||
credentials: {
|
||||
accessKeyId: assumeRes.Credentials?.AccessKeyId,
|
||||
secretAccessKey: assumeRes.Credentials?.SecretAccessKey,
|
||||
sessionToken: assumeRes.Credentials?.SessionToken
|
||||
}
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
const client = new IAMClient({
|
||||
region: providerInputs.region,
|
||||
credentials: {
|
||||
@ -52,21 +92,41 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
const message = (err as Error)?.message;
|
||||
if (
|
||||
providerInputs.method === AwsIamAuthType.AssumeRole &&
|
||||
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
||||
message.includes("Must specify userName when calling with non-User credentials")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: {
|
||||
name: string;
|
||||
};
|
||||
metadata: { projectId: string };
|
||||
}) => {
|
||||
const { inputs, usernameTemplate, metadata, identity } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
@ -76,6 +136,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
@ -125,9 +186,9 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
|
@ -8,19 +8,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -75,12 +76,17 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const { keyspace } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -7,19 +6,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -71,12 +71,12 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
return infoResponse;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
const { inputs, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
await connection.security.putUser({
|
||||
|
@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
import { InternalServerError } from "@app/lib/errors";
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||
|
||||
@ -43,6 +43,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
return res;
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: inputs.targetHost,
|
||||
targetPort: inputs.targetPort,
|
||||
relayHost,
|
||||
|
@ -9,6 +9,7 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
@ -22,13 +23,13 @@ const encodePassword = (password?: string) => {
|
||||
return base64Password;
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -196,8 +197,8 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
return dnArray;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
const { inputs, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
@ -224,7 +225,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
|
||||
|
||||
|
@ -20,6 +20,11 @@ export enum SqlProviders {
|
||||
Vertica = "vertica"
|
||||
}
|
||||
|
||||
export enum AwsIamAuthType {
|
||||
AssumeRole = "assume-role",
|
||||
AccessKey = "access-key"
|
||||
}
|
||||
|
||||
export enum ElasticSearchAuthTypes {
|
||||
User = "user",
|
||||
ApiKey = "api-key"
|
||||
@ -168,16 +173,38 @@ export const DynamicSecretSapAseSchema = z.object({
|
||||
revocationStatement: z.string().trim()
|
||||
});
|
||||
|
||||
export const DynamicSecretAwsIamSchema = z.object({
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
policyDocument: z.string().trim().optional(),
|
||||
userGroups: z.string().trim().optional(),
|
||||
policyArns: z.string().trim().optional()
|
||||
});
|
||||
export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
(val) => {
|
||||
if (typeof val === "object" && val !== null && !Object.hasOwn(val, "method")) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
(val as { method: string }).method = AwsIamAuthType.AccessKey;
|
||||
}
|
||||
return val;
|
||||
},
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.AccessKey),
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
policyDocument: z.string().trim().optional(),
|
||||
userGroups: z.string().trim().optional(),
|
||||
policyArns: z.string().trim().optional()
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.AssumeRole),
|
||||
roleArn: z.string().trim().min(1, "Role ARN required"),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
policyDocument: z.string().trim().optional(),
|
||||
userGroups: z.string().trim().optional(),
|
||||
policyArns: z.string().trim().optional()
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const DynamicSecretMongoAtlasSchema = z.object({
|
||||
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
|
||||
@ -400,9 +427,18 @@ export type TDynamicProviderFns = {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: {
|
||||
name: string;
|
||||
};
|
||||
metadata: { projectId: string };
|
||||
}) => Promise<{ entityId: string; data: unknown }>;
|
||||
validateConnection: (inputs: unknown) => Promise<boolean>;
|
||||
validateProviderInputs: (inputs: object) => Promise<unknown>;
|
||||
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
|
||||
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
|
||||
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
||||
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
|
||||
revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>;
|
||||
renew: (
|
||||
inputs: unknown,
|
||||
entityId: string,
|
||||
expireAt: number,
|
||||
metadata: { projectId: string }
|
||||
) => Promise<{ entityId: string }>;
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -7,19 +6,20 @@ import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -64,12 +64,17 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
await client({
|
||||
|
@ -1,4 +1,3 @@
|
||||
import handlebars from "handlebars";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@ -7,19 +6,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -60,12 +60,12 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
const { inputs, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import axios, { Axios } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@ -9,19 +8,20 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -117,12 +117,12 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
return infoResponse;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
const { inputs, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
await createRabbitMqUser({
|
||||
|
@ -9,19 +9,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = () => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*";
|
||||
return customAlphabet(charset, 64)();
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -121,12 +122,17 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return pingResponse;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@ -9,19 +9,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = `inf_${alphaNumericNanoId(25)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -87,11 +88,11 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
|
||||
const { inputs, usernameTemplate } = data;
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
const { inputs, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
|
@ -15,19 +15,20 @@ import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32); // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -97,11 +98,16 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
return testResult;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
// destroy client requires callback...
|
||||
const noop = () => {};
|
||||
@ -17,13 +18,13 @@ const generatePassword = (size = 48) => {
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null) => {
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = `infisical_${alphaNumericNanoId(32)}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
@ -88,13 +89,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
return isValidConnection;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(usernameTemplate);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
try {
|
||||
|
@ -3,13 +3,14 @@ import handlebars from "handlebars";
|
||||
import knex from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
@ -104,9 +105,8 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire
|
||||
}
|
||||
};
|
||||
|
||||
const generateUsername = (provider: SqlProviders, usernameTemplate?: string | null) => {
|
||||
const generateUsername = (provider: SqlProviders, usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
let randomUsername = "";
|
||||
|
||||
// For oracle, the client assumes everything is upper case when not using quotes around the password
|
||||
if (provider === SqlProviders.Oracle) {
|
||||
randomUsername = alphaNumericNanoId(32).toUpperCase();
|
||||
@ -114,10 +114,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu
|
||||
randomUsername = alphaNumericNanoId(32);
|
||||
}
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
|
||||
return handlebars.compile(usernameTemplate)({
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
unixTimestamp: Math.floor(Date.now() / 100)
|
||||
identity,
|
||||
options: {
|
||||
toUpperCase: provider === SqlProviders.Oracle
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -185,6 +188,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
@ -220,11 +224,16 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
|
||||
const { inputs, expireAt, usernameTemplate } = data;
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const username = generateUsername(providerInputs.client, usernameTemplate);
|
||||
const username = generateUsername(providerInputs.client, usernameTemplate, identity);
|
||||
|
||||
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
|
@ -0,0 +1,80 @@
|
||||
/* eslint-disable func-names */
|
||||
import handlebars from "handlebars";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
export const compileUsernameTemplate = ({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
identity,
|
||||
unixTimestamp,
|
||||
options
|
||||
}: {
|
||||
usernameTemplate: string;
|
||||
randomUsername: string;
|
||||
identity?: { name: string };
|
||||
unixTimestamp?: number;
|
||||
options?: {
|
||||
toUpperCase?: boolean;
|
||||
};
|
||||
}): string => {
|
||||
// Create isolated handlebars instance
|
||||
const hbs = handlebars.create();
|
||||
|
||||
// Register random helper on local instance
|
||||
hbs.registerHelper("random", function (length: number) {
|
||||
if (typeof length !== "number" || length <= 0 || length > 100) {
|
||||
return "";
|
||||
}
|
||||
return alphaNumericNanoId(length);
|
||||
});
|
||||
|
||||
// Register replace helper on local instance
|
||||
hbs.registerHelper("replace", function (text: string, searchValue: string, replaceValue: string) {
|
||||
// Convert to string if it's not already
|
||||
const textStr = String(text || "");
|
||||
if (!textStr) {
|
||||
return textStr;
|
||||
}
|
||||
|
||||
try {
|
||||
const re2Pattern = new RE2(searchValue, "g");
|
||||
// Replace all occurrences
|
||||
return re2Pattern.replace(textStr, replaceValue);
|
||||
} catch (error) {
|
||||
logger.error(error, "RE2 pattern failed, using original template");
|
||||
return textStr;
|
||||
}
|
||||
});
|
||||
|
||||
// Register truncate helper on local instance
|
||||
hbs.registerHelper("truncate", function (text: string, length: number) {
|
||||
// Convert to string if it's not already
|
||||
const textStr = String(text || "");
|
||||
if (!textStr) {
|
||||
return textStr;
|
||||
}
|
||||
|
||||
if (typeof length !== "number" || length <= 0) return textStr;
|
||||
return textStr.substring(0, length);
|
||||
});
|
||||
|
||||
// Compile template with context using local instance
|
||||
const context = {
|
||||
randomUsername,
|
||||
unixTimestamp: unixTimestamp || Math.floor(Date.now() / 100),
|
||||
identity: {
|
||||
name: identity?.name
|
||||
}
|
||||
};
|
||||
|
||||
const result = hbs.compile(usernameTemplate)(context);
|
||||
|
||||
if (options?.toUpperCase) {
|
||||
return result.toUpperCase();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
@ -4,7 +4,7 @@ import knex, { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
@ -196,6 +196,7 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
|
@ -42,6 +42,10 @@ export type TListGroupUsersDTO = {
|
||||
filter?: EFilterReturnedUsers;
|
||||
} & TGenericPermission;
|
||||
|
||||
export type TListProjectGroupUsersDTO = TListGroupUsersDTO & {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TAddUserToGroupDTO = {
|
||||
id: string;
|
||||
username: string;
|
||||
|
@ -709,6 +709,10 @@ export const licenseServiceFactory = ({
|
||||
return licenses;
|
||||
};
|
||||
|
||||
const invalidateGetPlan = async (orgId: string) => {
|
||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
||||
};
|
||||
|
||||
return {
|
||||
generateOrgCustomerId,
|
||||
removeOrgCustomer,
|
||||
@ -723,6 +727,7 @@ export const licenseServiceFactory = ({
|
||||
return onPremFeatures;
|
||||
},
|
||||
getPlan,
|
||||
invalidateGetPlan,
|
||||
updateSubscriptionOrgMemberCount,
|
||||
refreshPlan,
|
||||
getOrgPlan,
|
||||
|
@ -376,7 +376,8 @@ const DynamicSecretConditionV2Schema = z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
@ -404,6 +405,23 @@ const DynamicSecretConditionV2Schema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const SecretImportConditionSchema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
|
||||
})
|
||||
.partial();
|
||||
|
||||
const SecretConditionV2Schema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
@ -741,7 +759,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretConditionV1Schema.describe(
|
||||
conditions: SecretImportConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
|
@ -117,6 +117,7 @@ export const OCIVaultSyncFns = {
|
||||
syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
environment,
|
||||
destinationConfig: { compartmentOcid, vaultOcid, keyOcid }
|
||||
} = secretSync;
|
||||
|
||||
@ -213,7 +214,7 @@ export const OCIVaultSyncFns = {
|
||||
// Update and delete secrets
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
// Only update / delete active secrets
|
||||
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
|
||||
|
@ -10,7 +10,8 @@ export const PgSqlLock = {
|
||||
KmsRootKeyInit: 2025,
|
||||
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
|
||||
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`)
|
||||
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
|
||||
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`)
|
||||
} as const;
|
||||
|
||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
|
@ -89,6 +89,7 @@ export const GROUPS = {
|
||||
limit: "The number of users to return.",
|
||||
username: "The username to search for.",
|
||||
search: "The text string that user email or name will be filtered by.",
|
||||
projectId: "The ID of the project the group belongs to.",
|
||||
filterUsers:
|
||||
"Whether to filter the list of returned users. 'existingMembers' will only return existing users in the group, 'nonMembers' will only return users not in the group, undefined will return all users in the organization."
|
||||
},
|
||||
@ -400,6 +401,8 @@ export const KUBERNETES_AUTH = {
|
||||
caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
|
||||
tokenReviewerJwt:
|
||||
"Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
|
||||
tokenReviewMode:
|
||||
"The mode to use for token review. Must be one of: 'api', 'gateway'. If gateway is selected, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding.",
|
||||
allowedNamespaces:
|
||||
"The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
|
||||
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
|
||||
@ -417,6 +420,8 @@ export const KUBERNETES_AUTH = {
|
||||
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
|
||||
tokenReviewerJwt:
|
||||
"Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
|
||||
tokenReviewMode:
|
||||
"The mode to use for token review. Must be one of: 'api', 'gateway'. If gateway is selected, the gateway must be deployed in Kubernetes, and the gateway must have the system:auth-delegator ClusterRole binding.",
|
||||
allowedNamespaces:
|
||||
"The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
|
||||
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
|
||||
@ -2272,7 +2277,8 @@ export const SecretSyncs = {
|
||||
},
|
||||
GCP: {
|
||||
scope: "The Google project scope that secrets should be synced to.",
|
||||
projectId: "The ID of the Google project secrets should be synced to."
|
||||
projectId: "The ID of the Google project secrets should be synced to.",
|
||||
locationId: 'The ID of the Google project location secrets should be synced to (ie "us-west4").'
|
||||
},
|
||||
DATABRICKS: {
|
||||
scope: "The Databricks secret scope that secrets should be synced to."
|
||||
|
@ -213,6 +213,12 @@ const envSchema = z
|
||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"),
|
||||
DYNAMIC_SECRET_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()).default(
|
||||
process.env.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
|
||||
),
|
||||
DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default(
|
||||
process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||
),
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
|
411
backend/src/lib/gateway/gateway.ts
Normal file
411
backend/src/lib/gateway/gateway.ts
Normal file
@ -0,0 +1,411 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import crypto from "node:crypto";
|
||||
import net from "node:net";
|
||||
|
||||
import quicDefault, * as quicModule from "@infisical/quic";
|
||||
import axios from "axios";
|
||||
import https from "https";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
import {
|
||||
GatewayProxyProtocol,
|
||||
IGatewayProxyOptions,
|
||||
IGatewayProxyServer,
|
||||
TGatewayTlsOptions,
|
||||
TPingGatewayAndVerifyDTO
|
||||
} from "./types";
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
const quic = quicDefault || quicModule;
|
||||
|
||||
const parseSubjectDetails = (data: string) => {
|
||||
const values: Record<string, string> = {};
|
||||
data.split("\n").forEach((el) => {
|
||||
const [key, value] = el.split("=");
|
||||
values[key.trim()] = value.trim();
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
const createQuicConnection = async (
|
||||
relayHost: string,
|
||||
relayPort: number,
|
||||
tlsOptions: TGatewayTlsOptions,
|
||||
identityId: string,
|
||||
orgId: string
|
||||
) => {
|
||||
const client = await quic.QUICClient.createQUICClient({
|
||||
host: relayHost,
|
||||
port: relayPort,
|
||||
config: {
|
||||
ca: tlsOptions.ca,
|
||||
cert: tlsOptions.cert,
|
||||
key: tlsOptions.key,
|
||||
applicationProtos: ["infisical-gateway"],
|
||||
verifyPeer: true,
|
||||
verifyCallback: async (certs) => {
|
||||
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
|
||||
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
|
||||
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
|
||||
const isValidServerCertificate = serverCertificate.verify(caCertificate.publicKey);
|
||||
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
|
||||
|
||||
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
|
||||
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
|
||||
return quic.native.CryptoError.CertificateUnknown;
|
||||
}
|
||||
|
||||
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
|
||||
return quic.native.CryptoError.CertificateExpired;
|
||||
}
|
||||
|
||||
const formatedRelayHost =
|
||||
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
|
||||
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
|
||||
},
|
||||
maxIdleTimeout: 90000,
|
||||
keepAliveIntervalTime: 30000
|
||||
},
|
||||
crypto: {
|
||||
ops: {
|
||||
randomBytes: async (data) => {
|
||||
crypto.getRandomValues(new Uint8Array(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
export const pingGatewayAndVerify = async ({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions,
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
}: TPingGatewayAndVerifyDTO) => {
|
||||
let lastError: Error | null = null;
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
message: (err as Error)?.message,
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||
try {
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
const pingWriter = stream.writable.getWriter();
|
||||
await pingWriter.write(Buffer.from("PING\n"));
|
||||
pingWriter.releaseLock();
|
||||
|
||||
// Read PONG response
|
||||
const reader = stream.readable.getReader();
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
throw new Error("Gateway closed before receiving PONG");
|
||||
}
|
||||
|
||||
const response = Buffer.from(value).toString();
|
||||
|
||||
if (response !== "PONG\n" && response !== "PONG") {
|
||||
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
|
||||
}
|
||||
|
||||
reader.releaseLock();
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, DEFAULT_RETRY_DELAY);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await quicClient.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(lastError);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
|
||||
});
|
||||
};
|
||||
|
||||
const setupProxyServer = async ({
|
||||
targetPort,
|
||||
targetHost,
|
||||
tlsOptions,
|
||||
relayHost,
|
||||
relayPort,
|
||||
identityId,
|
||||
orgId,
|
||||
protocol = GatewayProxyProtocol.Tcp,
|
||||
httpsAgent
|
||||
}: {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayPort: number;
|
||||
relayHost: string;
|
||||
tlsOptions: TGatewayTlsOptions;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
protocol?: GatewayProxyProtocol;
|
||||
httpsAgent?: https.Agent;
|
||||
}): Promise<IGatewayProxyServer> => {
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
const proxyErrorMsg = [""];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
let streamClosed = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
server.on("connection", async (clientConn) => {
|
||||
try {
|
||||
clientConn.setKeepAlive(true, 30000); // 30 seconds
|
||||
clientConn.setNoDelay(true);
|
||||
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
|
||||
const forwardWriter = stream.writable.getWriter();
|
||||
let command: string;
|
||||
|
||||
if (protocol === GatewayProxyProtocol.Http) {
|
||||
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
||||
command = `FORWARD-HTTP ${targetUrl}`;
|
||||
logger.debug(`Using HTTP proxy mode: ${command.trim()}`);
|
||||
|
||||
// extract ca certificate from httpsAgent if present
|
||||
if (httpsAgent && targetHost.startsWith("https://")) {
|
||||
const agentOptions = httpsAgent.options;
|
||||
if (agentOptions && agentOptions.ca) {
|
||||
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
||||
const caB64 = Buffer.from(caCert as string).toString("base64");
|
||||
command += ` ca=${caB64}`;
|
||||
|
||||
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
||||
command += ` verify=${rejectUnauthorized}`;
|
||||
|
||||
logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`);
|
||||
}
|
||||
}
|
||||
|
||||
command += "\n";
|
||||
} else if (protocol === GatewayProxyProtocol.Tcp) {
|
||||
// For TCP mode, send FORWARD-TCP with host:port
|
||||
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
|
||||
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid protocol: ${protocol as string}`
|
||||
});
|
||||
}
|
||||
|
||||
await forwardWriter.write(Buffer.from(command));
|
||||
forwardWriter.releaseLock();
|
||||
|
||||
// Set up bidirectional copy
|
||||
const setupCopy = () => {
|
||||
// Client to QUIC
|
||||
// eslint-disable-next-line
|
||||
(async () => {
|
||||
const writer = stream.writable.getWriter();
|
||||
|
||||
// Create a handler for client data
|
||||
clientConn.on("data", (chunk) => {
|
||||
writer.write(chunk).catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle client connection close
|
||||
clientConn.on("end", () => {
|
||||
if (!streamClosed) {
|
||||
try {
|
||||
writer.close().catch((err) => {
|
||||
logger.debug(err, "Error closing writer (already closed)");
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug(error, "Error in writer close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
writer.abort(clientConnErr?.message).catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// QUIC to Client
|
||||
void (async () => {
|
||||
try {
|
||||
const reader = stream.readable.getReader();
|
||||
|
||||
let reading = true;
|
||||
while (reading) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
reading = false;
|
||||
clientConn.end(); // Close client connection when QUIC stream ends
|
||||
break;
|
||||
}
|
||||
|
||||
// Write data to TCP client
|
||||
const canContinue = clientConn.write(Buffer.from(value));
|
||||
|
||||
// Handle backpressure
|
||||
if (!canContinue) {
|
||||
await new Promise((res) => {
|
||||
clientConn.once("drain", res);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
clientConn.destroy();
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
setupCopy();
|
||||
// Handle connection closure
|
||||
clientConn.on("close", () => {
|
||||
if (!streamClosed) {
|
||||
streamClosed = true;
|
||||
stream.destroy().catch((err) => {
|
||||
logger.debug(err, "Stream already destroyed during close event");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
clientConn?.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying client connection");
|
||||
}
|
||||
|
||||
if (!streamClosed) {
|
||||
streamClosed = true;
|
||||
try {
|
||||
await stream.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying stream (might be already closed)");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
logger.error(clientConnErr, "Client socket error");
|
||||
cleanup().catch((err) => {
|
||||
logger.error(err, "Client conn cleanup");
|
||||
});
|
||||
});
|
||||
|
||||
clientConn.on("end", () => {
|
||||
cleanup().catch((err) => {
|
||||
logger.error(err, "Client conn end");
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to establish target connection:");
|
||||
clientConn.end();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.on("close", () => {
|
||||
quicClient?.destroy().catch((err) => {
|
||||
logger.error(err, "Failed to destroy quic client");
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("Failed to get server port"));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Gateway proxy started on port ${address.port} (${protocol} mode)`);
|
||||
resolve({
|
||||
server,
|
||||
port: address.port,
|
||||
cleanup: async () => {
|
||||
try {
|
||||
server.close();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error closing server");
|
||||
}
|
||||
|
||||
try {
|
||||
await quicClient?.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying QUIC client");
|
||||
}
|
||||
},
|
||||
getProxyError: () => proxyErrorMsg.join(",")
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const withGatewayProxy = async <T>(
|
||||
callback: (port: number, httpsAgent?: https.Agent) => Promise<T>,
|
||||
options: IGatewayProxyOptions
|
||||
): Promise<T> => {
|
||||
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId, protocol, httpsAgent } = options;
|
||||
|
||||
// Setup the proxy server
|
||||
const { port, cleanup, getProxyError } = await setupProxyServer({
|
||||
targetHost,
|
||||
targetPort,
|
||||
relayPort,
|
||||
relayHost,
|
||||
tlsOptions,
|
||||
identityId,
|
||||
orgId,
|
||||
protocol,
|
||||
httpsAgent
|
||||
});
|
||||
|
||||
try {
|
||||
// Execute the callback with the allocated port
|
||||
return await callback(port, httpsAgent);
|
||||
} catch (err) {
|
||||
const proxyErrorMessage = getProxyError();
|
||||
if (proxyErrorMessage) {
|
||||
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
|
||||
}
|
||||
logger.error(err, "Failed to do gateway");
|
||||
let errorMessage = proxyErrorMessage || (err as Error)?.message;
|
||||
if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) {
|
||||
errorMessage = (err.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: errorMessage });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
await cleanup();
|
||||
}
|
||||
};
|
@ -1,392 +1,2 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import crypto from "node:crypto";
|
||||
import net from "node:net";
|
||||
|
||||
import quicDefault, * as quicModule from "@infisical/quic";
|
||||
import axios from "axios";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
const quic = quicDefault || quicModule;
|
||||
|
||||
const parseSubjectDetails = (data: string) => {
|
||||
const values: Record<string, string> = {};
|
||||
data.split("\n").forEach((el) => {
|
||||
const [key, value] = el.split("=");
|
||||
values[key.trim()] = value.trim();
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
type TTlsOption = { ca: string; cert: string; key: string };
|
||||
|
||||
const createQuicConnection = async (
|
||||
relayHost: string,
|
||||
relayPort: number,
|
||||
tlsOptions: TTlsOption,
|
||||
identityId: string,
|
||||
orgId: string
|
||||
) => {
|
||||
const client = await quic.QUICClient.createQUICClient({
|
||||
host: relayHost,
|
||||
port: relayPort,
|
||||
config: {
|
||||
ca: tlsOptions.ca,
|
||||
cert: tlsOptions.cert,
|
||||
key: tlsOptions.key,
|
||||
applicationProtos: ["infisical-gateway"],
|
||||
verifyPeer: true,
|
||||
verifyCallback: async (certs) => {
|
||||
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
|
||||
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
|
||||
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
|
||||
const isValidServerCertificate = serverCertificate.verify(caCertificate.publicKey);
|
||||
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
|
||||
|
||||
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
|
||||
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
|
||||
return quic.native.CryptoError.CertificateUnknown;
|
||||
}
|
||||
|
||||
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
|
||||
return quic.native.CryptoError.CertificateExpired;
|
||||
}
|
||||
|
||||
const formatedRelayHost =
|
||||
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
|
||||
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
|
||||
},
|
||||
maxIdleTimeout: 90000,
|
||||
keepAliveIntervalTime: 30000
|
||||
},
|
||||
crypto: {
|
||||
ops: {
|
||||
randomBytes: async (data) => {
|
||||
crypto.getRandomValues(new Uint8Array(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
type TPingGatewayAndVerifyDTO = {
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TTlsOption;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export const pingGatewayAndVerify = async ({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions,
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
}: TPingGatewayAndVerifyDTO) => {
|
||||
let lastError: Error | null = null;
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
message: (err as Error)?.message,
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||
try {
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
const pingWriter = stream.writable.getWriter();
|
||||
await pingWriter.write(Buffer.from("PING\n"));
|
||||
pingWriter.releaseLock();
|
||||
|
||||
// Read PONG response
|
||||
const reader = stream.readable.getReader();
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
throw new Error("Gateway closed before receiving PONG");
|
||||
}
|
||||
|
||||
const response = Buffer.from(value).toString();
|
||||
|
||||
if (response !== "PONG\n" && response !== "PONG") {
|
||||
throw new Error(`Failed to Ping. Unexpected response: ${response}`);
|
||||
}
|
||||
|
||||
reader.releaseLock();
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, DEFAULT_RETRY_DELAY);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await quicClient.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(lastError);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
|
||||
});
|
||||
};
|
||||
|
||||
interface TProxyServer {
|
||||
server: net.Server;
|
||||
port: number;
|
||||
cleanup: () => Promise<void>;
|
||||
getProxyError: () => string;
|
||||
}
|
||||
|
||||
const setupProxyServer = async ({
|
||||
targetPort,
|
||||
targetHost,
|
||||
tlsOptions,
|
||||
relayHost,
|
||||
relayPort,
|
||||
identityId,
|
||||
orgId
|
||||
}: {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayPort: number;
|
||||
relayHost: string;
|
||||
tlsOptions: TTlsOption;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
}): Promise<TProxyServer> => {
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
const proxyErrorMsg = [""];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
let streamClosed = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
server.on("connection", async (clientConn) => {
|
||||
try {
|
||||
clientConn.setKeepAlive(true, 30000); // 30 seconds
|
||||
clientConn.setNoDelay(true);
|
||||
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
// Send FORWARD-TCP command
|
||||
const forwardWriter = stream.writable.getWriter();
|
||||
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
|
||||
forwardWriter.releaseLock();
|
||||
|
||||
// Set up bidirectional copy
|
||||
const setupCopy = () => {
|
||||
// Client to QUIC
|
||||
// eslint-disable-next-line
|
||||
(async () => {
|
||||
const writer = stream.writable.getWriter();
|
||||
|
||||
// Create a handler for client data
|
||||
clientConn.on("data", (chunk) => {
|
||||
writer.write(chunk).catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle client connection close
|
||||
clientConn.on("end", () => {
|
||||
if (!streamClosed) {
|
||||
try {
|
||||
writer.close().catch((err) => {
|
||||
logger.debug(err, "Error closing writer (already closed)");
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug(error, "Error in writer close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
writer.abort(clientConnErr?.message).catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// QUIC to Client
|
||||
void (async () => {
|
||||
try {
|
||||
const reader = stream.readable.getReader();
|
||||
|
||||
let reading = true;
|
||||
while (reading) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
reading = false;
|
||||
clientConn.end(); // Close client connection when QUIC stream ends
|
||||
break;
|
||||
}
|
||||
|
||||
// Write data to TCP client
|
||||
const canContinue = clientConn.write(Buffer.from(value));
|
||||
|
||||
// Handle backpressure
|
||||
if (!canContinue) {
|
||||
await new Promise((res) => {
|
||||
clientConn.once("drain", res);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
clientConn.destroy();
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
setupCopy();
|
||||
// Handle connection closure
|
||||
clientConn.on("close", () => {
|
||||
if (!streamClosed) {
|
||||
streamClosed = true;
|
||||
stream.destroy().catch((err) => {
|
||||
logger.debug(err, "Stream already destroyed during close event");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
clientConn?.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying client connection");
|
||||
}
|
||||
|
||||
if (!streamClosed) {
|
||||
streamClosed = true;
|
||||
try {
|
||||
await stream.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying stream (might be already closed)");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
logger.error(clientConnErr, "Client socket error");
|
||||
cleanup().catch((err) => {
|
||||
logger.error(err, "Client conn cleanup");
|
||||
});
|
||||
});
|
||||
|
||||
clientConn.on("end", () => {
|
||||
cleanup().catch((err) => {
|
||||
logger.error(err, "Client conn end");
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to establish target connection:");
|
||||
clientConn.end();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.on("close", () => {
|
||||
quicClient?.destroy().catch((err) => {
|
||||
logger.error(err, "Failed to destroy quic client");
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("Failed to get server port"));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Gateway proxy started");
|
||||
resolve({
|
||||
server,
|
||||
port: address.port,
|
||||
cleanup: async () => {
|
||||
try {
|
||||
server.close();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error closing server");
|
||||
}
|
||||
|
||||
try {
|
||||
await quicClient?.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying QUIC client");
|
||||
}
|
||||
},
|
||||
getProxyError: () => proxyErrorMsg.join(",")
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
interface ProxyOptions {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TTlsOption;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export const withGatewayProxy = async <T>(
|
||||
callback: (port: number) => Promise<T>,
|
||||
options: ProxyOptions
|
||||
): Promise<T> => {
|
||||
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
||||
|
||||
// Setup the proxy server
|
||||
const { port, cleanup, getProxyError } = await setupProxyServer({
|
||||
targetHost,
|
||||
targetPort,
|
||||
relayPort,
|
||||
relayHost,
|
||||
tlsOptions,
|
||||
identityId,
|
||||
orgId
|
||||
});
|
||||
|
||||
try {
|
||||
// Execute the callback with the allocated port
|
||||
return await callback(port);
|
||||
} catch (err) {
|
||||
const proxyErrorMessage = getProxyError();
|
||||
if (proxyErrorMessage) {
|
||||
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
|
||||
}
|
||||
logger.error(err, "Failed to do gateway");
|
||||
let errorMessage = proxyErrorMessage || (err as Error)?.message;
|
||||
if (axios.isAxiosError(err) && (err.response?.data as { message?: string })?.message) {
|
||||
errorMessage = (err.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: errorMessage });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
await cleanup();
|
||||
}
|
||||
};
|
||||
export { pingGatewayAndVerify, withGatewayProxy } from "./gateway";
|
||||
export { GatewayHttpProxyActions, GatewayProxyProtocol } from "./types";
|
||||
|
42
backend/src/lib/gateway/types.ts
Normal file
42
backend/src/lib/gateway/types.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import net from "node:net";
|
||||
|
||||
import https from "https";
|
||||
|
||||
export type TGatewayTlsOptions = { ca: string; cert: string; key: string };
|
||||
|
||||
export enum GatewayProxyProtocol {
|
||||
Http = "http",
|
||||
Tcp = "tcp"
|
||||
}
|
||||
|
||||
export enum GatewayHttpProxyActions {
|
||||
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token"
|
||||
}
|
||||
|
||||
export interface IGatewayProxyOptions {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TGatewayTlsOptions;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
protocol: GatewayProxyProtocol;
|
||||
httpsAgent?: https.Agent;
|
||||
}
|
||||
|
||||
export type TPingGatewayAndVerifyDTO = {
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TGatewayTlsOptions;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export interface IGatewayProxyServer {
|
||||
server: net.Server;
|
||||
port: number;
|
||||
cleanup: () => Promise<void>;
|
||||
getProxyError: () => string;
|
||||
}
|
@ -7,13 +7,24 @@ type SanitizationArg = {
|
||||
allowedExpressions?: (arg: string) => boolean;
|
||||
};
|
||||
|
||||
const isValidExpression = (expression: string, dto: SanitizationArg): boolean => {
|
||||
// Allow helper functions (replace, truncate)
|
||||
const allowedHelpers = ["replace", "truncate", "random"];
|
||||
if (allowedHelpers.includes(expression)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check regular allowed expressions
|
||||
return dto?.allowedExpressions?.(expression) || false;
|
||||
};
|
||||
|
||||
export const validateHandlebarTemplate = (templateName: string, template: string, dto: SanitizationArg) => {
|
||||
const parsedAst = handlebars.parse(template);
|
||||
parsedAst.body.forEach((el) => {
|
||||
if (el.type === "ContentStatement") return;
|
||||
if (el.type === "MustacheStatement" && "path" in el) {
|
||||
const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } };
|
||||
if (path.type === "PathExpression" && dto?.allowedExpressions?.(path.original)) return;
|
||||
if (path.type === "PathExpression" && isValidExpression(path.original, dto)) return;
|
||||
}
|
||||
logger.error(el, "Template sanitization failed");
|
||||
throw new BadRequestError({ message: `Template sanitization failed: ${templateName}` });
|
||||
@ -26,7 +37,7 @@ export const isValidHandleBarTemplate = (template: string, dto: SanitizationArg)
|
||||
if (el.type === "ContentStatement") return true;
|
||||
if (el.type === "MustacheStatement" && "path" in el) {
|
||||
const { path } = el as { type: "MustacheStatement"; path: { type: "PathExpression"; original: string } };
|
||||
if (path.type === "PathExpression" && dto?.allowedExpressions?.(path.original)) return true;
|
||||
if (path.type === "PathExpression" && isValidExpression(path.original, dto)) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
return {
|
||||
errorResponseBuilder: (_, context) => {
|
||||
throw new RateLimitError({
|
||||
message: `Rate limit exceeded. Please try again in ${context.after}`
|
||||
message: `Rate limit exceeded. Please try again in ${Math.ceil(context.ttl / 1000)} seconds`
|
||||
});
|
||||
},
|
||||
timeWindow: 60 * 1000,
|
||||
@ -113,3 +113,12 @@ export const requestAccessLimit: RateLimitOptions = {
|
||||
max: 10,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const smtpRateLimit = ({
|
||||
keyGenerator = (req) => req.realIp
|
||||
}: Pick<RateLimitOptions, "keyGenerator"> = {}): RateLimitOptions => ({
|
||||
timeWindow: 40 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 2,
|
||||
keyGenerator
|
||||
});
|
||||
|
@ -155,6 +155,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
oidc: token?.identityAuth?.oidc
|
||||
});
|
||||
}
|
||||
if (token?.identityAuth?.kubernetes) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
kubernetes: token?.identityAuth?.kubernetes
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
|
@ -1516,7 +1516,9 @@ export const registerRoutes = async (
|
||||
dynamicSecretProviders,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
userDAL,
|
||||
identityDAL
|
||||
});
|
||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||
auditLogDAL,
|
||||
|
@ -45,4 +45,37 @@ export const registerGcpConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/secret-manager-project-locations`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ displayName: z.string(), locationId: z.string() }).array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
params: { connectionId },
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const locations = await server.services.appConnection.gcp.listSecretManagerProjectLocations(
|
||||
{ connectionId, projectId },
|
||||
req.permission
|
||||
);
|
||||
|
||||
return locations;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { IdentityKubernetesAuthTokenReviewMode } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
|
||||
const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick({
|
||||
@ -18,6 +19,7 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
|
||||
accessTokenTrustedIps: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
tokenReviewMode: true,
|
||||
identityId: true,
|
||||
kubernetesHost: true,
|
||||
allowedNamespaces: true,
|
||||
@ -124,6 +126,10 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
),
|
||||
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
|
||||
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
|
||||
tokenReviewMode: z
|
||||
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
|
||||
.default(IdentityKubernetesAuthTokenReviewMode.Api)
|
||||
.describe(KUBERNETES_AUTH.ATTACH.tokenReviewMode),
|
||||
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),
|
||||
@ -157,10 +163,22 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.default(0)
|
||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
|
||||
ctx.addIssue({
|
||||
path: ["gatewayId"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "When token review mode is set to Gateway, a gateway must be selected"
|
||||
});
|
||||
}
|
||||
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
|
||||
ctx.addIssue({
|
||||
path: ["accessTokenTTL"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
});
|
||||
}
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||
@ -247,6 +265,10 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
),
|
||||
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
|
||||
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
|
||||
tokenReviewMode: z
|
||||
.nativeEnum(IdentityKubernetesAuthTokenReviewMode)
|
||||
.optional()
|
||||
.describe(KUBERNETES_AUTH.UPDATE.tokenReviewMode),
|
||||
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),
|
||||
@ -280,10 +302,26 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
.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."
|
||||
),
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.tokenReviewMode &&
|
||||
data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway &&
|
||||
!data.gatewayId
|
||||
) {
|
||||
ctx.addIssue({
|
||||
path: ["gatewayId"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "When token review mode is set to Gateway, a gateway must be selected"
|
||||
});
|
||||
}
|
||||
if (data.accessTokenMaxTTL && data.accessTokenTTL ? data.accessTokenTTL > data.accessTokenMaxTTL : false) {
|
||||
ctx.addIssue({
|
||||
path: ["accessTokenTTL"],
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
});
|
||||
}
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { inviteUserRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -11,7 +11,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/signup",
|
||||
config: {
|
||||
rateLimit: inviteUserRateLimit
|
||||
rateLimit: smtpRateLimit()
|
||||
},
|
||||
method: "POST",
|
||||
schema: {
|
||||
@ -81,7 +81,10 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/signup-resend",
|
||||
config: {
|
||||
rateLimit: inviteUserRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) =>
|
||||
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp
|
||||
})
|
||||
},
|
||||
method: "POST",
|
||||
schema: {
|
||||
|
@ -2,9 +2,9 @@ import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
|
||||
@ -47,7 +47,9 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/projects/:projectId/grant-admin-access",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp)
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
|
@ -2,10 +2,10 @@ import { z } from "zod";
|
||||
|
||||
import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { UserEncryption } from "@app/services/user/user-types";
|
||||
|
||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
@ -80,7 +80,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/email/password-reset",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
@ -224,7 +226,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/email/password-setup",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp)
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
@ -233,6 +237,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.password.sendPasswordSetupEmail(req.permission);
|
||||
|
||||
@ -267,6 +272,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req, res) => {
|
||||
await server.services.password.setupPassword(req.body, req.permission);
|
||||
|
||||
|
@ -4,9 +4,11 @@ import {
|
||||
GroupProjectMembershipsSchema,
|
||||
GroupsSchema,
|
||||
ProjectMembershipRole,
|
||||
ProjectUserMembershipRolesSchema
|
||||
ProjectUserMembershipRolesSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
|
||||
import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -301,4 +303,61 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
|
||||
return { groupMembership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/groups/:groupId/users",
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectGroups],
|
||||
description: "Return project group users",
|
||||
params: z.object({
|
||||
projectId: z.string().trim().describe(GROUPS.LIST_USERS.projectId),
|
||||
groupId: z.string().trim().describe(GROUPS.LIST_USERS.id)
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
|
||||
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
|
||||
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search),
|
||||
filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
users: UsersSchema.pick({
|
||||
email: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
isPartOfGroup: z.boolean(),
|
||||
joinedGroupAt: z.date().nullable()
|
||||
})
|
||||
)
|
||||
.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { users, totalCount } = await server.services.groupProject.listProjectGroupUsers({
|
||||
id: req.params.groupId,
|
||||
projectId: req.params.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return { users, totalCount };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||
@ -12,7 +12,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/me/emails/code",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
@ -13,7 +13,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
url: "/email/signup",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
rateLimit: smtpRateLimit({
|
||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
|
||||
})
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@ -11,8 +11,10 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||
import {
|
||||
GCPApp,
|
||||
GCPGetProjectLocationsRes,
|
||||
GCPGetProjectsRes,
|
||||
GCPGetServiceRes,
|
||||
GCPLocation,
|
||||
TGcpConnection,
|
||||
TGcpConnectionConfig
|
||||
} from "./gcp-connection-types";
|
||||
@ -145,6 +147,45 @@ export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection)
|
||||
return projects;
|
||||
};
|
||||
|
||||
export const getGcpSecretManagerProjectLocations = async (projectId: string, appConnection: TGcpConnection) => {
|
||||
const accessToken = await getGcpConnectionAuthToken(appConnection);
|
||||
|
||||
let gcpLocations: GCPLocation[] = [];
|
||||
|
||||
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 } = await request.get<GCPGetProjectLocationsRes>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${projectId}/locations`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
gcpLocations = gcpLocations.concat(data.locations);
|
||||
|
||||
if (!data.nextPageToken) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
pageToken = data.nextPageToken;
|
||||
}
|
||||
|
||||
return gcpLocations.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
};
|
||||
|
||||
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
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
||||
import { TGcpConnection } from "./gcp-connection-types";
|
||||
import { getGcpSecretManagerProjectLocations, getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
||||
import { TGcpConnection, TGetGCPProjectLocationsDTO } from "./gcp-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
@ -23,7 +23,23 @@ export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
}
|
||||
};
|
||||
|
||||
const listSecretManagerProjectLocations = async (
|
||||
{ connectionId, projectId }: TGetGCPProjectLocationsDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor);
|
||||
|
||||
try {
|
||||
const locations = await getGcpSecretManagerProjectLocations(projectId, appConnection);
|
||||
|
||||
return locations;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretManagerProjects
|
||||
listSecretManagerProjects,
|
||||
listSecretManagerProjectLocations
|
||||
};
|
||||
};
|
||||
|
@ -38,6 +38,22 @@ export type GCPGetProjectsRes = {
|
||||
nextPageToken?: string;
|
||||
};
|
||||
|
||||
export type GCPLocation = {
|
||||
name: string;
|
||||
locationId: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export type GCPGetProjectLocationsRes = {
|
||||
locations: GCPLocation[];
|
||||
nextPageToken?: string;
|
||||
};
|
||||
|
||||
export type TGetGCPProjectLocationsDTO = {
|
||||
projectId: string;
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type GCPGetServiceRes = {
|
||||
name: string;
|
||||
parent: string;
|
||||
|
@ -397,7 +397,7 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
// Check if the user actually has access to the specified organization.
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId);
|
||||
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
const selectedOrg = await orgDAL.findById(organizationId);
|
||||
|
||||
if (!hasOrganizationMembership) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
||||
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
@ -42,7 +43,7 @@ type TGroupProjectServiceFactoryDep = {
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
projectBotDAL: TProjectBotDALFactory;
|
||||
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
||||
groupDAL: Pick<TGroupDALFactory, "findOne" | "findAllGroupPossibleMembers">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||
};
|
||||
|
||||
@ -471,11 +472,54 @@ export const groupProjectServiceFactory = ({
|
||||
return groupMembership;
|
||||
};
|
||||
|
||||
const listProjectGroupUsers = async ({
|
||||
id,
|
||||
projectId,
|
||||
offset,
|
||||
limit,
|
||||
username,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
search,
|
||||
filter
|
||||
}: TListProjectGroupUsersDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
|
||||
const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({
|
||||
orgId: project.orgId,
|
||||
groupId: id,
|
||||
offset,
|
||||
limit,
|
||||
username,
|
||||
search,
|
||||
filter
|
||||
});
|
||||
|
||||
return { users: members, totalCount };
|
||||
};
|
||||
|
||||
return {
|
||||
addGroupToProject,
|
||||
updateGroupInProject,
|
||||
removeGroupFromProject,
|
||||
listGroupsInProject,
|
||||
getGroupInProject
|
||||
getGroupInProject,
|
||||
listProjectGroupUsers
|
||||
};
|
||||
};
|
||||
|
@ -11,5 +11,9 @@ export type TIdentityAccessTokenJwtPayload = {
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
kubernetes?: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -20,8 +20,9 @@ import {
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@ -33,6 +34,7 @@ import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/su
|
||||
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
|
||||
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
|
||||
import {
|
||||
IdentityKubernetesAuthTokenReviewMode,
|
||||
TAttachKubernetesAuthDTO,
|
||||
TCreateTokenReviewResponse,
|
||||
TGetKubernetesAuthDTO,
|
||||
@ -72,19 +74,25 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
gatewayId: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
caCert?: string;
|
||||
reviewTokenThroughGateway: boolean;
|
||||
},
|
||||
gatewayCallback: (host: string, port: number) => Promise<T>
|
||||
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
|
||||
const callbackResult = await withGatewayProxy(
|
||||
async (port) => {
|
||||
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
|
||||
const res = await gatewayCallback("https://localhost", port);
|
||||
async (port, httpsAgent) => {
|
||||
const res = await gatewayCallback(
|
||||
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
|
||||
port,
|
||||
httpsAgent
|
||||
);
|
||||
return res;
|
||||
},
|
||||
{
|
||||
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
|
||||
targetHost: inputs.targetHost,
|
||||
targetPort: inputs.targetPort,
|
||||
relayHost,
|
||||
@ -95,7 +103,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
}
|
||||
},
|
||||
// we always pass this, because its needed for both tcp and http protocol
|
||||
httpsAgent: new https.Agent({
|
||||
ca: inputs.caCert,
|
||||
rejectUnauthorized: Boolean(inputs.caCert)
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
@ -129,22 +142,29 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
||||
}
|
||||
|
||||
let tokenReviewerJwt = "";
|
||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||
tokenReviewerJwt = decryptor({
|
||||
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
}).toString();
|
||||
} else {
|
||||
// if no token reviewer is provided means the incoming token has to act as reviewer
|
||||
tokenReviewerJwt = serviceAccountJwt;
|
||||
}
|
||||
const tokenReviewCallbackRaw = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
||||
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
||||
let tokenReviewerJwt = "";
|
||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||
tokenReviewerJwt = decryptor({
|
||||
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
}).toString();
|
||||
} else {
|
||||
// if no token reviewer is provided means the incoming token has to act as reviewer
|
||||
tokenReviewerJwt = serviceAccountJwt;
|
||||
}
|
||||
|
||||
let { kubernetesHost } = identityKubernetesAuth;
|
||||
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
||||
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
||||
}
|
||||
let servername = identityKubernetesAuth.kubernetesHost;
|
||||
if (servername.startsWith("https://") || servername.startsWith("http://")) {
|
||||
servername = new RE2("^https?:\\/\\/").replace(servername, "");
|
||||
}
|
||||
|
||||
// get the last colon index, if it has a port, remove it, including the colon
|
||||
const lastColonIndex = servername.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
servername = servername.substring(0, lastColonIndex);
|
||||
}
|
||||
|
||||
const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
const res = await axios
|
||||
@ -165,11 +185,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
timeout: 10000,
|
||||
// if ca cert, rejectUnauthorized: true
|
||||
httpsAgent: new https.Agent({
|
||||
ca: caCert,
|
||||
rejectUnauthorized: Boolean(caCert),
|
||||
servername: kubernetesHost
|
||||
servername
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -192,18 +211,137 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const [k8sHost, k8sPort] = kubernetesHost.split(":");
|
||||
const tokenReviewCallbackThroughGateway = async (
|
||||
host: string = identityKubernetesAuth.kubernetesHost,
|
||||
port?: number,
|
||||
httpsAgent?: https.Agent
|
||||
) => {
|
||||
logger.info(
|
||||
{
|
||||
host,
|
||||
port
|
||||
},
|
||||
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
|
||||
);
|
||||
|
||||
const data = identityKubernetesAuth.gatewayId
|
||||
? await $gatewayProxyWrapper(
|
||||
const baseUrl = port ? `${host}:${port}` : host;
|
||||
|
||||
const res = await axios
|
||||
.post<TCreateTokenReviewResponse>(
|
||||
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||
{
|
||||
gatewayId: identityKubernetesAuth.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort ? Number(k8sPort) : 443
|
||||
apiVersion: "authentication.k8s.io/v1",
|
||||
kind: "TokenReview",
|
||||
spec: {
|
||||
token: serviceAccountJwt,
|
||||
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
|
||||
}
|
||||
},
|
||||
tokenReviewCallback
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
timeout: 10000,
|
||||
...(httpsAgent ? { httpsAgent } : {})
|
||||
}
|
||||
)
|
||||
: await tokenReviewCallback();
|
||||
.catch((err) => {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err.response) {
|
||||
let { message } = err?.response?.data as unknown as { message?: string };
|
||||
|
||||
if (!message && typeof err.response.data === "string") {
|
||||
message = err.response.data;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
throw new UnauthorizedError({
|
||||
message,
|
||||
name: "KubernetesTokenReviewRequestError"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
let data: TCreateTokenReviewResponse | undefined;
|
||||
|
||||
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
|
||||
const { kubernetesHost } = identityKubernetesAuth;
|
||||
|
||||
let urlString = kubernetesHost;
|
||||
if (!kubernetesHost.startsWith("http://") && !kubernetesHost.startsWith("https://")) {
|
||||
urlString = `https://${kubernetesHost}`;
|
||||
}
|
||||
|
||||
const url = new URL(urlString);
|
||||
let { port: k8sPort } = url;
|
||||
const { protocol, hostname: k8sHost } = url;
|
||||
|
||||
const cleanedProtocol = new RE2(/[^a-zA-Z0-9]/g).replace(protocol, "").toLowerCase();
|
||||
|
||||
if (!["https", "http"].includes(cleanedProtocol)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid Kubernetes host URL, must start with http:// or https://"
|
||||
});
|
||||
}
|
||||
|
||||
if (!k8sPort) {
|
||||
k8sPort = cleanedProtocol === "https" ? "443" : "80";
|
||||
}
|
||||
|
||||
if (!identityKubernetesAuth.gatewayId) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway ID is required when token review mode is set to Gateway"
|
||||
});
|
||||
}
|
||||
|
||||
data = await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: identityKubernetesAuth.gatewayId,
|
||||
targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http)
|
||||
targetPort: k8sPort ? Number(k8sPort) : 443,
|
||||
caCert,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
tokenReviewCallbackThroughGateway
|
||||
);
|
||||
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
|
||||
let { kubernetesHost } = identityKubernetesAuth;
|
||||
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
||||
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
||||
}
|
||||
|
||||
const [k8sHost, k8sPort] = kubernetesHost.split(":");
|
||||
|
||||
data = identityKubernetesAuth.gatewayId
|
||||
? await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: identityKubernetesAuth.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort ? Number(k8sPort) : 443,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
tokenReviewCallbackRaw
|
||||
)
|
||||
: await tokenReviewCallbackRaw();
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}`
|
||||
});
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to review token"
|
||||
});
|
||||
}
|
||||
|
||||
if ("error" in data.status)
|
||||
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
|
||||
@ -278,7 +416,13 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
{
|
||||
identityId: identityKubernetesAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
||||
identityAuth: {
|
||||
kubernetes: {
|
||||
namespace: targetNamespace,
|
||||
name: targetName
|
||||
}
|
||||
}
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
@ -298,6 +442,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
kubernetesHost,
|
||||
caCert,
|
||||
tokenReviewerJwt,
|
||||
tokenReviewMode,
|
||||
allowedNamespaces,
|
||||
allowedNames,
|
||||
allowedAudience,
|
||||
@ -384,6 +529,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
kubernetesHost,
|
||||
tokenReviewMode,
|
||||
allowedNamespaces,
|
||||
allowedNames,
|
||||
allowedAudience,
|
||||
@ -410,6 +556,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
kubernetesHost,
|
||||
caCert,
|
||||
tokenReviewerJwt,
|
||||
tokenReviewMode,
|
||||
allowedNamespaces,
|
||||
allowedNames,
|
||||
allowedAudience,
|
||||
@ -492,6 +639,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
|
||||
const updateQuery: TIdentityKubernetesAuthsUpdate = {
|
||||
kubernetesHost,
|
||||
tokenReviewMode,
|
||||
allowedNamespaces,
|
||||
allowedNames,
|
||||
allowedAudience,
|
||||
|
@ -5,11 +5,17 @@ export type TLoginKubernetesAuthDTO = {
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
export enum IdentityKubernetesAuthTokenReviewMode {
|
||||
Api = "api",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
export type TAttachKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
kubernetesHost: string;
|
||||
caCert: string;
|
||||
tokenReviewerJwt?: string;
|
||||
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
||||
allowedNamespaces: string;
|
||||
allowedNames: string;
|
||||
allowedAudience: string;
|
||||
@ -26,6 +32,7 @@ export type TUpdateKubernetesAuthDTO = {
|
||||
kubernetesHost?: string;
|
||||
caCert?: string;
|
||||
tokenReviewerJwt?: string | null;
|
||||
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
||||
allowedNamespaces?: string;
|
||||
allowedNames?: string;
|
||||
allowedAudience?: string;
|
||||
|
@ -212,7 +212,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
// special query
|
||||
const findAllOrgsByUserId = async (
|
||||
userId: string
|
||||
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => {
|
||||
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[]> => {
|
||||
try {
|
||||
const org = (await db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
@ -234,6 +234,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole"))
|
||||
.select(db.ref("status").withSchema(TableName.OrgMembership).as("userStatus"))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
@ -242,7 +243,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
ELSE ''
|
||||
END as "orgAuthMethod"
|
||||
`)
|
||||
)) as (TOrganizations & { orgAuthMethod: string; userRole: string })[];
|
||||
)) as (TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[];
|
||||
|
||||
return org;
|
||||
} catch (error) {
|
||||
|
@ -183,7 +183,9 @@ export const orgServiceFactory = ({
|
||||
* */
|
||||
const findAllOrganizationOfUser = async (userId: string) => {
|
||||
const orgs = await orgDAL.findAllOrgsByUserId(userId);
|
||||
return orgs;
|
||||
|
||||
// Filter out orgs where the membership object is an invitation
|
||||
return orgs.filter((org) => org.userStatus !== "invited");
|
||||
};
|
||||
/*
|
||||
* Get all workspace members
|
||||
@ -835,16 +837,22 @@ export const orgServiceFactory = ({
|
||||
|
||||
// if the user doesn't exist we create the user with the email
|
||||
if (!inviteeUser) {
|
||||
inviteeUser = await userDAL.create(
|
||||
{
|
||||
isAccepted: false,
|
||||
email: inviteeEmail,
|
||||
username: inviteeEmail,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
// TODO(carlos): will be removed once the function receives usernames instead of emails
|
||||
const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx);
|
||||
if (usersByEmail?.length === 1) {
|
||||
[inviteeUser] = usersByEmail;
|
||||
} else {
|
||||
inviteeUser = await userDAL.create(
|
||||
{
|
||||
isAccepted: false,
|
||||
email: inviteeEmail,
|
||||
username: inviteeEmail,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const inviteeUserId = inviteeUser?.id;
|
||||
|
@ -30,7 +30,7 @@ import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-
|
||||
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@ -165,7 +165,7 @@ type TProjectServiceFactoryDep = {
|
||||
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
|
||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
@ -259,16 +259,17 @@ export const projectServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
|
||||
const plan = await licenseService.getPlan(organization.id);
|
||||
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
|
||||
// case: limit imposed on number of workspaces allowed
|
||||
// case: number of workspaces used exceeds the number of workspaces allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
||||
});
|
||||
}
|
||||
|
||||
const results = await (trx || projectDAL).transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateProject(organization.id)]);
|
||||
|
||||
const plan = await licenseService.getPlan(organization.id);
|
||||
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
|
||||
// case: limit imposed on number of workspaces allowed
|
||||
// case: number of workspaces used exceeds the number of workspaces allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
||||
});
|
||||
}
|
||||
const ghostUser = await orgService.addGhostUser(organization.id, tx);
|
||||
|
||||
if (kmsKeyId) {
|
||||
@ -493,6 +494,10 @@ export const projectServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
// no need to invalidate if there was no limit
|
||||
if (plan.workspaceLimit) {
|
||||
await licenseService.invalidateGetPlan(organization.id);
|
||||
}
|
||||
return {
|
||||
...project,
|
||||
environments: envs,
|
||||
|
@ -127,6 +127,7 @@ export const OnePassSyncFns = {
|
||||
syncSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
environment,
|
||||
destinationConfig: { vaultId }
|
||||
} = secretSync;
|
||||
|
||||
@ -164,7 +165,7 @@ export const OnePassSyncFns = {
|
||||
|
||||
for await (const [key, variable] of Object.entries(items)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
if (!(key in secretMap)) {
|
||||
try {
|
||||
|
@ -294,7 +294,7 @@ const deleteParametersBatch = async (
|
||||
|
||||
export const AwsParameterStoreSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
@ -391,7 +391,7 @@ export const AwsParameterStoreSyncFns = {
|
||||
const [key, parameter] = entry;
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(key, environment?.slug || "", syncOptions.keySchema)) continue;
|
||||
|
||||
if (!(key in secretMap) || !secretMap[key].value) {
|
||||
parametersToDelete.push(parameter);
|
||||
|
@ -57,7 +57,11 @@ const sleep = async () =>
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string): Promise<TAwsSecretsRecord> => {
|
||||
const getSecretsRecord = async (
|
||||
client: SecretsManagerClient,
|
||||
environment: string,
|
||||
keySchema?: string
|
||||
): Promise<TAwsSecretsRecord> => {
|
||||
const awsSecretsRecord: TAwsSecretsRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
@ -72,7 +76,7 @@ const getSecretsRecord = async (client: SecretsManagerClient, keySchema?: string
|
||||
|
||||
if (output.SecretList) {
|
||||
output.SecretList.forEach((secretEntry) => {
|
||||
if (secretEntry.Name && matchesSchema(secretEntry.Name, keySchema)) {
|
||||
if (secretEntry.Name && matchesSchema(secretEntry.Name, environment, keySchema)) {
|
||||
awsSecretsRecord[secretEntry.Name] = secretEntry;
|
||||
}
|
||||
});
|
||||
@ -307,11 +311,11 @@ const processTags = ({
|
||||
|
||||
export const AwsSecretsManagerSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema);
|
||||
const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema);
|
||||
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
@ -401,7 +405,7 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(secretKey, syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(secretKey, environment?.slug || "", syncOptions.keySchema)) continue;
|
||||
|
||||
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
|
||||
try {
|
||||
@ -468,7 +472,11 @@ export const AwsSecretsManagerSyncFns = {
|
||||
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client, secretSync.syncOptions.keySchema);
|
||||
const awsSecretsRecord = await getSecretsRecord(
|
||||
client,
|
||||
secretSync.environment?.slug || "",
|
||||
secretSync.syncOptions.keySchema
|
||||
);
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
const { destinationConfig } = secretSync;
|
||||
@ -503,11 +511,11 @@ export const AwsSecretsManagerSyncFns = {
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
const { destinationConfig, syncOptions, environment } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client, syncOptions.keySchema);
|
||||
const awsSecretsRecord = await getSecretsRecord(client, environment?.slug || "", syncOptions.keySchema);
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
|
@ -141,7 +141,7 @@ export const azureAppConfigurationSyncFactory = ({
|
||||
|
||||
for await (const key of Object.keys(azureAppConfigSecrets)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
const azureSecret = azureAppConfigSecrets[key];
|
||||
if (
|
||||
|
@ -194,7 +194,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
|
||||
for await (const deleteSecretKey of deleteSecrets.filter(
|
||||
(secret) =>
|
||||
matchesSchema(secret, secretSync.syncOptions.keySchema) &&
|
||||
matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
|
||||
!setSecrets.find((setSecret) => setSecret.key === secret)
|
||||
)) {
|
||||
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, {
|
||||
|
@ -118,7 +118,7 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec
|
||||
|
||||
for await (const secret of Object.keys(camundaSecrets)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(secret, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
if (!(secret in secretMap) || !secretMap[secret].value) {
|
||||
try {
|
||||
|
@ -117,7 +117,7 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
|
||||
|
||||
for await (const secret of databricksSecretKeys) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
if (!(secret.key in secretMap)) {
|
||||
await deleteDatabricksSecrets({
|
||||
|
@ -1,3 +1,63 @@
|
||||
export enum GcpSyncScope {
|
||||
Global = "global"
|
||||
Global = "global",
|
||||
Region = "region"
|
||||
}
|
||||
|
||||
export enum GCPSecretManagerLocation {
|
||||
// Asia Pacific
|
||||
ASIA_SOUTHEAST3 = "asia-southeast3", // Bangkok
|
||||
ASIA_SOUTH2 = "asia-south2", // Delhi
|
||||
ASIA_EAST2 = "asia-east2", // Hong Kong
|
||||
ASIA_SOUTHEAST2 = "asia-southeast2", // Jakarta
|
||||
AUSTRALIA_SOUTHEAST2 = "australia-southeast2", // Melbourne
|
||||
ASIA_SOUTH1 = "asia-south1", // Mumbai
|
||||
ASIA_NORTHEAST2 = "asia-northeast2", // Osaka
|
||||
ASIA_NORTHEAST3 = "asia-northeast3", // Seoul
|
||||
ASIA_SOUTHEAST1 = "asia-southeast1", // Singapore
|
||||
AUSTRALIA_SOUTHEAST1 = "australia-southeast1", // Sydney
|
||||
ASIA_EAST1 = "asia-east1", // Taiwan
|
||||
ASIA_NORTHEAST1 = "asia-northeast1", // Tokyo
|
||||
|
||||
// Europe
|
||||
EUROPE_WEST1 = "europe-west1", // Belgium
|
||||
EUROPE_WEST10 = "europe-west10", // Berlin
|
||||
EUROPE_NORTH1 = "europe-north1", // Finland
|
||||
EUROPE_NORTH2 = "europe-north2", // Stockholm
|
||||
EUROPE_WEST3 = "europe-west3", // Frankfurt
|
||||
EUROPE_WEST2 = "europe-west2", // London
|
||||
EUROPE_SOUTHWEST1 = "europe-southwest1", // Madrid
|
||||
EUROPE_WEST8 = "europe-west8", // Milan
|
||||
EUROPE_WEST4 = "europe-west4", // Netherlands
|
||||
EUROPE_WEST12 = "europe-west12", // Turin
|
||||
EUROPE_WEST9 = "europe-west9", // Paris
|
||||
EUROPE_CENTRAL2 = "europe-central2", // Warsaw
|
||||
EUROPE_WEST6 = "europe-west6", // Zurich
|
||||
|
||||
// North America
|
||||
US_CENTRAL1 = "us-central1", // Iowa
|
||||
US_WEST4 = "us-west4", // Las Vegas
|
||||
US_WEST2 = "us-west2", // Los Angeles
|
||||
NORTHAMERICA_SOUTH1 = "northamerica-south1", // Mexico
|
||||
NORTHAMERICA_NORTHEAST1 = "northamerica-northeast1", // Montréal
|
||||
US_EAST4 = "us-east4", // Northern Virginia
|
||||
US_CENTRAL2 = "us-central2", // Oklahoma
|
||||
US_WEST1 = "us-west1", // Oregon
|
||||
US_WEST3 = "us-west3", // Salt Lake City
|
||||
US_EAST1 = "us-east1", // South Carolina
|
||||
NORTHAMERICA_NORTHEAST2 = "northamerica-northeast2", // Toronto
|
||||
US_EAST5 = "us-east5", // Columbus
|
||||
US_SOUTH1 = "us-south1", // Dallas
|
||||
US_WEST8 = "us-west8", // Phoenix
|
||||
|
||||
// South America
|
||||
SOUTHAMERICA_EAST1 = "southamerica-east1", // São Paulo
|
||||
SOUTHAMERICA_WEST1 = "southamerica-west1", // Santiago
|
||||
|
||||
// Middle East
|
||||
ME_CENTRAL2 = "me-central2", // Dammam
|
||||
ME_CENTRAL1 = "me-central1", // Doha
|
||||
ME_WEST1 = "me-west1", // Tel Aviv
|
||||
|
||||
// Africa
|
||||
AFRICA_SOUTH1 = "africa-south1" // Johannesburg
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ 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 { GcpSyncScope } from "@app/services/secret-sync/gcp/gcp-sync-enums";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
@ -15,9 +16,17 @@ import {
|
||||
TGcpSyncWithCredentials
|
||||
} from "./gcp-sync-types";
|
||||
|
||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||
const getProjectUrl = (secretSync: TGcpSyncWithCredentials) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
if (destinationConfig.scope === GcpSyncScope.Global) {
|
||||
return `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}`;
|
||||
}
|
||||
|
||||
return `https://secretmanager.${destinationConfig.locationId}.rep.googleapis.com/v1/projects/${destinationConfig.projectId}/locations/${destinationConfig.locationId}`;
|
||||
};
|
||||
|
||||
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||
let gcpSecrets: GCPSecret[] = [];
|
||||
|
||||
const pageSize = 100;
|
||||
@ -31,16 +40,13 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
||||
});
|
||||
|
||||
// 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"
|
||||
}
|
||||
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(`${getProjectUrl(secretSync)}/secrets`, {
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (secretsRes.secrets) {
|
||||
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
|
||||
@ -61,7 +67,7 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
|
||||
|
||||
try {
|
||||
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
||||
`${getProjectUrl(secretSync)}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@ -113,11 +119,14 @@ export const GcpSyncFns = {
|
||||
if (!(key in gcpSecrets)) {
|
||||
// case: create secret
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
||||
`${getProjectUrl(secretSync)}/secrets`,
|
||||
{
|
||||
replication: {
|
||||
automatic: {}
|
||||
}
|
||||
replication:
|
||||
destinationConfig.scope === GcpSyncScope.Global
|
||||
? {
|
||||
automatic: {}
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
params: {
|
||||
@ -131,7 +140,7 @@ export const GcpSyncFns = {
|
||||
);
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||
`${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
|
||||
{
|
||||
payload: {
|
||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||
@ -155,7 +164,7 @@ export const GcpSyncFns = {
|
||||
|
||||
for await (const key of Object.keys(gcpSecrets)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
try {
|
||||
if (!(key in secretMap) || !secretMap[key].value) {
|
||||
@ -163,15 +172,12 @@ export const GcpSyncFns = {
|
||||
if (secretSync.syncOptions.disableSecretDeletion) continue;
|
||||
|
||||
// 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"
|
||||
}
|
||||
await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
||||
if (!secretMap[key].value) {
|
||||
logger.warn(
|
||||
@ -180,7 +186,7 @@ export const GcpSyncFns = {
|
||||
}
|
||||
|
||||
await request.post(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||
`${getProjectUrl(secretSync)}/secrets/${key}:addVersion`,
|
||||
{
|
||||
payload: {
|
||||
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||
@ -212,21 +218,18 @@ export const GcpSyncFns = {
|
||||
},
|
||||
|
||||
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig, connection } = secretSync;
|
||||
const { 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"
|
||||
}
|
||||
await request.delete(`${getProjectUrl(secretSync)}/secrets/${key}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,14 +10,33 @@ import {
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SecretSync } from "../secret-sync-enums";
|
||||
import { GcpSyncScope } from "./gcp-sync-enums";
|
||||
import { GCPSecretManagerLocation, GcpSyncScope } from "./gcp-sync-enums";
|
||||
|
||||
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
const GcpSyncDestinationConfigSchema = z.object({
|
||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId)
|
||||
});
|
||||
const GcpSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
z
|
||||
.object({
|
||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId)
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "Global"
|
||||
})
|
||||
),
|
||||
z
|
||||
.object({
|
||||
scope: z.literal(GcpSyncScope.Region).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId),
|
||||
locationId: z.nativeEnum(GCPSecretManagerLocation).describe(SecretSyncs.DESTINATION_CONFIG.GCP.locationId)
|
||||
})
|
||||
.describe(
|
||||
JSON.stringify({
|
||||
title: "Region"
|
||||
})
|
||||
)
|
||||
]);
|
||||
|
||||
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.GCPSecretManager),
|
||||
|
@ -223,8 +223,9 @@ export const GithubSyncFns = {
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(encryptedSecret.name, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
|
||||
if (!(encryptedSecret.name in secretMap)) {
|
||||
await deleteSecret(client, secretSync, encryptedSecret);
|
||||
|
@ -68,6 +68,7 @@ export const HCVaultSyncFns = {
|
||||
syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
environment,
|
||||
destinationConfig: { mount, path },
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
@ -97,7 +98,7 @@ export const HCVaultSyncFns = {
|
||||
|
||||
for await (const [key] of Object.entries(variables)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, keySchema)) continue;
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!(key in secretMap)) {
|
||||
delete variables[key];
|
||||
|
@ -200,8 +200,9 @@ export const HumanitecSyncFns = {
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const humanitecSecret of humanitecSecrets) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(humanitecSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
|
||||
if (!secretMap[humanitecSecret.key]) {
|
||||
await deleteSecret(secretSync, humanitecSecret);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AxiosError } from "axios";
|
||||
import RE2 from "re2";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
|
||||
@ -68,13 +68,17 @@ type TSyncSecretDeps = {
|
||||
};
|
||||
|
||||
// Add schema to secret keys
|
||||
const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
|
||||
const addSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => {
|
||||
if (!schema) return unprocessedSecretMap;
|
||||
|
||||
const processedSecretMap: TSecretMap = {};
|
||||
|
||||
for (const [key, value] of Object.entries(unprocessedSecretMap)) {
|
||||
const newKey = new RE2("{{secretKey}}").replace(schema, key);
|
||||
const newKey = handlebars.compile(schema)({
|
||||
secretKey: key,
|
||||
environment
|
||||
});
|
||||
|
||||
processedSecretMap[newKey] = value;
|
||||
}
|
||||
|
||||
@ -82,10 +86,17 @@ const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMa
|
||||
};
|
||||
|
||||
// Strip schema from secret keys
|
||||
const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
|
||||
const stripSchema = (unprocessedSecretMap: TSecretMap, environment: string, schema?: string): TSecretMap => {
|
||||
if (!schema) return unprocessedSecretMap;
|
||||
|
||||
const [prefix, suffix] = schema.split("{{secretKey}}");
|
||||
const compiledSchemaPattern = handlebars.compile(schema)({
|
||||
secretKey: "{{secretKey}}", // Keep secretKey
|
||||
environment
|
||||
});
|
||||
|
||||
const parts = compiledSchemaPattern.split("{{secretKey}}");
|
||||
const prefix = parts[0];
|
||||
const suffix = parts[parts.length - 1];
|
||||
|
||||
const strippedMap: TSecretMap = {};
|
||||
|
||||
@ -103,21 +114,40 @@ const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecret
|
||||
};
|
||||
|
||||
// Checks if a key matches a schema
|
||||
export const matchesSchema = (key: string, schema?: string): boolean => {
|
||||
export const matchesSchema = (key: string, environment: string, schema?: string): boolean => {
|
||||
if (!schema) return true;
|
||||
|
||||
const [prefix, suffix] = schema.split("{{secretKey}}");
|
||||
if (prefix === undefined || suffix === undefined) return true;
|
||||
const compiledSchemaPattern = handlebars.compile(schema)({
|
||||
secretKey: "{{secretKey}}", // Keep secretKey
|
||||
environment
|
||||
});
|
||||
|
||||
return key.startsWith(prefix) && key.endsWith(suffix);
|
||||
// This edge-case shouldn't be possible
|
||||
if (!compiledSchemaPattern.includes("{{secretKey}}")) {
|
||||
return key === compiledSchemaPattern;
|
||||
}
|
||||
|
||||
const parts = compiledSchemaPattern.split("{{secretKey}}");
|
||||
const prefix = parts[0];
|
||||
const suffix = parts[parts.length - 1];
|
||||
|
||||
if (prefix === "" && suffix === "") return true;
|
||||
|
||||
// If prefix is empty, key must end with suffix
|
||||
if (prefix === "") return key.endsWith(suffix);
|
||||
|
||||
// If suffix is empty, key must start with prefix
|
||||
if (suffix === "") return key.startsWith(prefix);
|
||||
|
||||
return key.startsWith(prefix) && key.endsWith(suffix) && key.length >= prefix.length + suffix.length;
|
||||
};
|
||||
|
||||
// Filter only for secrets with keys that match the schema
|
||||
const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => {
|
||||
const filterForSchema = (secretMap: TSecretMap, environment: string, schema?: string): TSecretMap => {
|
||||
const filteredMap: TSecretMap = {};
|
||||
|
||||
for (const [key, value] of Object.entries(secretMap)) {
|
||||
if (matchesSchema(key, schema)) {
|
||||
if (matchesSchema(key, environment, schema)) {
|
||||
filteredMap[key] = value;
|
||||
}
|
||||
}
|
||||
@ -131,7 +161,7 @@ export const SecretSyncFns = {
|
||||
secretMap: TSecretMap,
|
||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
): Promise<void> => {
|
||||
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
|
||||
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
||||
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
@ -255,14 +285,16 @@ export const SecretSyncFns = {
|
||||
);
|
||||
}
|
||||
|
||||
return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema);
|
||||
const filtered = filterForSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
||||
const stripped = stripSchema(filtered, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
||||
return stripped;
|
||||
},
|
||||
removeSecrets: (
|
||||
secretSync: TSecretSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
): Promise<void> => {
|
||||
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
|
||||
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
||||
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
|
@ -28,10 +28,30 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
|
||||
keySchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), {
|
||||
message:
|
||||
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
|
||||
})
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
// Check if {{secretKey}} is present
|
||||
const secretKeyRegex = new RE2(/\{\{secretKey\}\}/);
|
||||
const secretKeyIsPresent = secretKeyRegex.test(val);
|
||||
|
||||
return contentIsValid && secretKeyIsPresent;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
|
||||
}
|
||||
)
|
||||
.describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema),
|
||||
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
|
||||
});
|
||||
|
@ -127,7 +127,7 @@ export const TeamCitySyncFns = {
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
if (!(key in secretMap)) {
|
||||
try {
|
||||
|
@ -232,8 +232,11 @@ export const TerraformCloudSyncFns = {
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for (const terraformCloudVariable of terraformCloudVariables) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (
|
||||
!matchesSchema(terraformCloudVariable.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema)
|
||||
)
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) {
|
||||
await deleteVariable(secretSync, terraformCloudVariable);
|
||||
|
@ -291,8 +291,9 @@ export const VercelSyncFns = {
|
||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||
|
||||
for await (const vercelSecret of vercelSecrets) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue;
|
||||
if (!matchesSchema(vercelSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
|
||||
if (!secretMap[vercelSecret.key]) {
|
||||
await deleteSecret(secretSync, vercelSecret);
|
||||
|
@ -128,6 +128,7 @@ export const WindmillSyncFns = {
|
||||
syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
environment,
|
||||
destinationConfig: { path },
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
@ -171,7 +172,7 @@ export const WindmillSyncFns = {
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, keySchema)) continue;
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!(key in secretMap)) {
|
||||
try {
|
||||
|
@ -21,6 +21,11 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
const findUserByUsername = async (username: string, tx?: Knex) =>
|
||||
(tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() });
|
||||
|
||||
const findUserByEmail = async (email: string, tx?: Knex) =>
|
||||
(tx || db)(TableName.Users).whereRaw('lower("email") = :email', { email: email.toLowerCase() }).where({
|
||||
isEmailVerified: true
|
||||
});
|
||||
|
||||
const getUsersByFilter = async ({
|
||||
limit,
|
||||
offset,
|
||||
@ -234,6 +239,7 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
findOneUserAction,
|
||||
createUserAction,
|
||||
getUsersByFilter,
|
||||
findAllMyAccounts
|
||||
findAllMyAccounts,
|
||||
findUserByEmail
|
||||
};
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ require (
|
||||
github.com/fatih/semgroup v1.2.0
|
||||
github.com/gitleaks/go-gitdiff v0.9.1
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/infisical/go-sdk v0.5.92
|
||||
github.com/infisical/go-sdk v0.5.95
|
||||
github.com/infisical/infisical-kmip v0.3.5
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||
|
@ -294,6 +294,10 @@ github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7P
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE=
|
||||
github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||
github.com/infisical/go-sdk v0.5.94 h1:wKBj+KpJEe+ZzOJ7koXQZDR0dLL9bt0Kqgf/1q+7tG4=
|
||||
github.com/infisical/go-sdk v0.5.94/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||
github.com/infisical/go-sdk v0.5.95 h1:so0YwPofbT7j6Ao8Xcxee/o3ia33meuEVDU2vWr9yfs=
|
||||
github.com/infisical/go-sdk v0.5.95/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
|
@ -7,16 +7,76 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/gateway"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func getInfisicalSdkInstance(cmd *cobra.Command) (infisicalSdk.InfisicalClientInterface, context.CancelFunc, error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
})
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// if the --token param is set, we use it directly for authentication
|
||||
if token != nil {
|
||||
infisicalClient.Auth().SetAccessToken(token.Token)
|
||||
return infisicalClient, cancel, nil
|
||||
}
|
||||
|
||||
// if the --token param is not set, we use the auth-method flag to determine the authentication method, and perform the appropriate login flow based on that
|
||||
authMethod, err := cmd.Flags().GetString("auth-method")
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
authMethodValid, strategy := util.IsAuthMethodValid(authMethod, false)
|
||||
if !authMethodValid {
|
||||
util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", authMethod))
|
||||
}
|
||||
|
||||
sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd)
|
||||
|
||||
authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){
|
||||
util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin,
|
||||
util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin,
|
||||
util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin,
|
||||
util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin,
|
||||
util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin,
|
||||
util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin,
|
||||
util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin,
|
||||
util.AuthStrategy.JWT_AUTH: sdkAuthenticator.HandleJwtAuthLogin,
|
||||
}
|
||||
|
||||
_, err = authStrategies[strategy]()
|
||||
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return infisicalClient, cancel, nil
|
||||
}
|
||||
|
||||
var gatewayCmd = &cobra.Command{
|
||||
Use: "gateway",
|
||||
Short: "Run the Infisical gateway or manage its systemd service",
|
||||
@ -26,13 +86,18 @@ var gatewayCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse token flag")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
util.HandleError(fmt.Errorf("Token not found"))
|
||||
infisicalClient, cancelSdk, err := getInfisicalSdkInstance(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "unable to get infisical client")
|
||||
}
|
||||
defer cancelSdk()
|
||||
|
||||
var accessToken atomic.Value
|
||||
accessToken.Store(infisicalClient.Auth().GetAccessToken())
|
||||
|
||||
if accessToken.Load().(string) == "" {
|
||||
util.HandleError(errors.New("no access token found"))
|
||||
}
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||
@ -41,13 +106,14 @@ var gatewayCmd = &cobra.Command{
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
sigStopCh := make(chan bool, 1)
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
ctx, cancelCmd := context.WithCancel(cmd.Context())
|
||||
defer cancelCmd()
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
close(sigStopCh)
|
||||
cancel()
|
||||
cancelCmd()
|
||||
cancelSdk()
|
||||
|
||||
// If we get a second signal, force exit
|
||||
<-sigCh
|
||||
@ -55,6 +121,34 @@ var gatewayCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
var gatewayInstance *gateway.Gateway
|
||||
|
||||
// Token refresh goroutine - runs every 10 seconds
|
||||
go func() {
|
||||
tokenRefreshTicker := time.NewTicker(10 * time.Second)
|
||||
defer tokenRefreshTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tokenRefreshTicker.C:
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newToken := infisicalClient.Auth().GetAccessToken()
|
||||
if newToken != "" && newToken != accessToken.Load().(string) {
|
||||
accessToken.Store(newToken)
|
||||
if gatewayInstance != nil {
|
||||
gatewayInstance.UpdateIdentityAccessToken(newToken)
|
||||
}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Main gateway retry loop with proper context handling
|
||||
retryTicker := time.NewTicker(5 * time.Second)
|
||||
defer retryTicker.Stop()
|
||||
@ -64,7 +158,7 @@ var gatewayCmd = &cobra.Command{
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
gatewayInstance, err := gateway.NewGateway(token.Token)
|
||||
gatewayInstance, err := gateway.NewGateway(accessToken.Load().(string))
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
@ -126,7 +220,7 @@ var gatewayInstallCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
util.HandleError(fmt.Errorf("Token not found"))
|
||||
util.HandleError(errors.New("Token not found"))
|
||||
}
|
||||
|
||||
domain, err := cmd.Flags().GetString("domain")
|
||||
@ -183,7 +277,7 @@ var gatewayRelayCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
if relayConfigFilePath == "" {
|
||||
util.HandleError(fmt.Errorf("Missing config file"))
|
||||
util.HandleError(errors.New("Missing config file"))
|
||||
}
|
||||
|
||||
gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath)
|
||||
@ -198,7 +292,19 @@ var gatewayRelayCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||
gatewayCmd.Flags().String("token", "", "connect with Infisical using machine identity access token. if not provided, you must set the auth-method flag")
|
||||
|
||||
gatewayCmd.Flags().String("auth-method", "", "login method [universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]. if not provided, you must set the token flag")
|
||||
|
||||
gatewayCmd.Flags().String("client-id", "", "client id for universal auth")
|
||||
gatewayCmd.Flags().String("client-secret", "", "client secret for universal auth")
|
||||
|
||||
gatewayCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods")
|
||||
gatewayCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth")
|
||||
gatewayCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
|
||||
|
||||
gatewayCmd.Flags().String("jwt", "", "JWT for jwt-based auth methods [oidc-auth, jwt-auth]")
|
||||
|
||||
gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||
gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")
|
||||
|
||||
|
@ -49,13 +49,13 @@ func startKmipServer(cmd *cobra.Command, args []string) {
|
||||
var identityClientSecret string
|
||||
|
||||
if strategy == util.AuthStrategy.UNIVERSAL_AUTH {
|
||||
identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
|
||||
identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME})
|
||||
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse identity client ID")
|
||||
}
|
||||
|
||||
identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
|
||||
identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME})
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse identity client secret")
|
||||
}
|
||||
|
@ -49,97 +49,6 @@ type params struct {
|
||||
keyLength uint32
|
||||
}
|
||||
|
||||
func handleUniversalAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
clientId, err := util.GetCmdFlagOrEnv(cmd, "client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
|
||||
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
clientSecret, err := util.GetCmdFlagOrEnv(cmd, "client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret)
|
||||
}
|
||||
|
||||
func handleKubernetesAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
serviceAccountTokenPath, err := util.GetCmdFlagOrEnv(cmd, "service-account-token-path", util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath)
|
||||
}
|
||||
|
||||
func handleAzureAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().AzureAuthLogin(identityId, "")
|
||||
}
|
||||
|
||||
func handleGcpIdTokenAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().GcpIdTokenAuthLogin(identityId)
|
||||
}
|
||||
|
||||
func handleGcpIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
serviceAccountKeyFilePath, err := util.GetCmdFlagOrEnv(cmd, "service-account-key-file-path", util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath)
|
||||
}
|
||||
|
||||
func handleAwsIamAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().AwsIamAuthLogin(identityId)
|
||||
}
|
||||
|
||||
func handleOidcAuthLogin(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := util.GetCmdFlagOrEnv(cmd, "machine-identity-id", util.INFISICAL_MACHINE_IDENTITY_ID_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
jwt, err := util.GetCmdFlagOrEnv(cmd, "oidc-jwt", util.INFISICAL_OIDC_AUTH_JWT_NAME)
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return infisicalClient.Auth().OidcAuthLogin(identityId, jwt)
|
||||
}
|
||||
|
||||
func formatAuthMethod(authMethod string) string {
|
||||
return strings.ReplaceAll(authMethod, "-", " ")
|
||||
}
|
||||
@ -154,8 +63,22 @@ var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Login into your Infisical account",
|
||||
DisableFlagsInUseLine: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// daniel: oidc-jwt is deprecated in favor of `jwt`. we backfill the `jwt` flag with the value of `oidc-jwt` if it's set.
|
||||
if cmd.Flags().Changed("oidc-jwt") && !cmd.Flags().Changed("jwt") {
|
||||
oidcJWT, err := cmd.Flags().GetString("oidc-jwt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.Flags().Set("jwt", oidcJWT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
presetDomain := config.INFISICAL_URL
|
||||
|
||||
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
|
||||
@ -310,17 +233,19 @@ var loginCmd = &cobra.Command{
|
||||
Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
|
||||
} else {
|
||||
|
||||
authStrategies := map[util.AuthStrategyType]func(cmd *cobra.Command, infisicalClient infisicalSdk.InfisicalClientInterface) (credential infisicalSdk.MachineIdentityCredential, e error){
|
||||
util.AuthStrategy.UNIVERSAL_AUTH: handleUniversalAuthLogin,
|
||||
util.AuthStrategy.KUBERNETES_AUTH: handleKubernetesAuthLogin,
|
||||
util.AuthStrategy.AZURE_AUTH: handleAzureAuthLogin,
|
||||
util.AuthStrategy.GCP_ID_TOKEN_AUTH: handleGcpIdTokenAuthLogin,
|
||||
util.AuthStrategy.GCP_IAM_AUTH: handleGcpIamAuthLogin,
|
||||
util.AuthStrategy.AWS_IAM_AUTH: handleAwsIamAuthLogin,
|
||||
util.AuthStrategy.OIDC_AUTH: handleOidcAuthLogin,
|
||||
sdkAuthenticator := util.NewSdkAuthenticator(infisicalClient, cmd)
|
||||
|
||||
authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){
|
||||
util.AuthStrategy.UNIVERSAL_AUTH: sdkAuthenticator.HandleUniversalAuthLogin,
|
||||
util.AuthStrategy.KUBERNETES_AUTH: sdkAuthenticator.HandleKubernetesAuthLogin,
|
||||
util.AuthStrategy.AZURE_AUTH: sdkAuthenticator.HandleAzureAuthLogin,
|
||||
util.AuthStrategy.GCP_ID_TOKEN_AUTH: sdkAuthenticator.HandleGcpIdTokenAuthLogin,
|
||||
util.AuthStrategy.GCP_IAM_AUTH: sdkAuthenticator.HandleGcpIamAuthLogin,
|
||||
util.AuthStrategy.AWS_IAM_AUTH: sdkAuthenticator.HandleAwsIamAuthLogin,
|
||||
util.AuthStrategy.OIDC_AUTH: sdkAuthenticator.HandleOidcAuthLogin,
|
||||
}
|
||||
|
||||
credential, err := authStrategies[strategy](cmd, infisicalClient)
|
||||
credential, err := authStrategies[strategy]()
|
||||
|
||||
if err != nil {
|
||||
euErrorMessage := ""
|
||||
@ -518,14 +443,18 @@ func init() {
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file")
|
||||
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
|
||||
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
|
||||
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
|
||||
loginCmd.Flags().String("method", "user", "login method [user, universal-auth, kubernetes, azure, gcp-id-token, gcp-iam, aws-iam, oidc-auth]")
|
||||
loginCmd.Flags().String("client-id", "", "client id for universal auth")
|
||||
loginCmd.Flags().String("client-secret", "", "client secret for universal auth")
|
||||
loginCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods")
|
||||
loginCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth")
|
||||
loginCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth")
|
||||
loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication")
|
||||
loginCmd.Flags().String("jwt", "", "jwt for jwt-based auth methods [oidc-auth, jwt-auth]")
|
||||
loginCmd.Flags().String("oidc-jwt", "", "JWT for OIDC authentication. Deprecated, use --jwt instead")
|
||||
|
||||
loginCmd.Flags().MarkDeprecated("oidc-jwt", "use --jwt instead")
|
||||
|
||||
}
|
||||
|
||||
func DomainOverridePrompt() (bool, error) {
|
||||
|
@ -4,11 +4,19 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -18,9 +26,13 @@ func handleConnection(ctx context.Context, quicConn quic.Connection) {
|
||||
log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String())
|
||||
// Use WaitGroup to track all streams
|
||||
var wg sync.WaitGroup
|
||||
|
||||
contextWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
// Accept the first stream, which we'll use for commands
|
||||
stream, err := quicConn.AcceptStream(ctx)
|
||||
stream, err := quicConn.AcceptStream(contextWithTimeout)
|
||||
if err != nil {
|
||||
log.Printf("Failed to accept QUIC stream: %v", err)
|
||||
break
|
||||
@ -44,7 +56,12 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
||||
|
||||
// Use buffered reader for better handling of fragmented data
|
||||
reader := bufio.NewReader(stream)
|
||||
defer stream.Close()
|
||||
defer func() {
|
||||
log.Info().Msgf("Closing stream %d", streamID)
|
||||
if stream != nil {
|
||||
stream.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
msg, err := reader.ReadBytes('\n')
|
||||
@ -89,6 +106,39 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
||||
CopyDataFromQuicToTcp(stream, destTarget)
|
||||
log.Info().Msgf("Ending secure transmission between %s->%s", quicConn.LocalAddr().String(), destTarget.LocalAddr().String())
|
||||
return
|
||||
|
||||
case "FORWARD-HTTP":
|
||||
argParts := bytes.Split(args, []byte(" "))
|
||||
if len(argParts) == 0 {
|
||||
log.Error().Msg("FORWARD-HTTP requires target URL")
|
||||
return
|
||||
}
|
||||
|
||||
targetURL := string(argParts[0])
|
||||
|
||||
if !isValidURL(targetURL) {
|
||||
log.Error().Msgf("Invalid target URL: %s", targetURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse optional parameters
|
||||
var caCertB64, verifyParam string
|
||||
for _, part := range argParts[1:] {
|
||||
partStr := string(part)
|
||||
if strings.HasPrefix(partStr, "ca=") {
|
||||
caCertB64 = strings.TrimPrefix(partStr, "ca=")
|
||||
} else if strings.HasPrefix(partStr, "verify=") {
|
||||
verifyParam = strings.TrimPrefix(partStr, "verify=")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msgf("Starting HTTP proxy to: %s", targetURL)
|
||||
|
||||
if err := handleHTTPProxy(stream, reader, targetURL, caCertB64, verifyParam); err != nil {
|
||||
log.Error().Msgf("HTTP proxy error: %v", err)
|
||||
}
|
||||
return
|
||||
|
||||
case "PING":
|
||||
if _, err := stream.Write([]byte("PONG\n")); err != nil {
|
||||
log.Error().Msgf("Error writing PONG response: %v", err)
|
||||
@ -100,11 +150,142 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
||||
}
|
||||
}
|
||||
}
|
||||
func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string, caCertB64 string, verifyParam string) error {
|
||||
transport := &http.Transport{
|
||||
DisableKeepAlives: false,
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(targetURL, "https://") {
|
||||
tlsConfig := &tls.Config{}
|
||||
|
||||
if caCertB64 != "" {
|
||||
caCert, err := base64.StdEncoding.DecodeString(caCertB64)
|
||||
if err == nil {
|
||||
caCertPool := x509.NewCertPool()
|
||||
if caCertPool.AppendCertsFromPEM(caCert) {
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
log.Info().Msg("Using provided CA certificate from gateway client")
|
||||
} else {
|
||||
log.Error().Msg("Failed to parse provided CA certificate")
|
||||
}
|
||||
} else {
|
||||
log.Error().Msgf("Failed to decode CA certificate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if verifyParam != "" {
|
||||
tlsConfig.InsecureSkipVerify = verifyParam == "false"
|
||||
log.Info().Msgf("TLS verification set to: %s", verifyParam)
|
||||
}
|
||||
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Loop to handle multiple HTTP requests on the same stream
|
||||
for {
|
||||
req, err := http.ReadRequest(reader)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
log.Info().Msg("Client closed HTTP connection")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read HTTP request: %v", err)
|
||||
}
|
||||
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
|
||||
|
||||
actionHeader := req.Header.Get("x-infisical-action")
|
||||
if actionHeader != "" {
|
||||
if actionHeader == "inject-k8s-sa-auth-token" {
|
||||
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||
if err != nil {
|
||||
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
||||
continue // Continue to next request instead of returning
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
||||
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
||||
}
|
||||
req.Header.Del("x-infisical-action")
|
||||
}
|
||||
|
||||
// Build full target URL
|
||||
var targetFullURL string
|
||||
if strings.HasPrefix(targetURL, "http://") || strings.HasPrefix(targetURL, "https://") {
|
||||
baseURL := strings.TrimSuffix(targetURL, "/")
|
||||
targetFullURL = baseURL + req.URL.Path
|
||||
if req.URL.RawQuery != "" {
|
||||
targetFullURL += "?" + req.URL.RawQuery
|
||||
}
|
||||
} else {
|
||||
baseURL := strings.TrimSuffix("http://"+targetURL, "/")
|
||||
targetFullURL = baseURL + req.URL.Path
|
||||
if req.URL.RawQuery != "" {
|
||||
targetFullURL += "?" + req.URL.RawQuery
|
||||
}
|
||||
}
|
||||
|
||||
// create the request to the target
|
||||
proxyReq, err := http.NewRequest(req.Method, targetFullURL, req.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to create proxy request: %v", err)
|
||||
stream.Write([]byte(buildHttpInternalServerError("failed to create proxy request")))
|
||||
continue // Continue to next request
|
||||
}
|
||||
proxyReq.Header = req.Header.Clone()
|
||||
|
||||
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
||||
|
||||
resp, err := client.Do(proxyReq)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to reach target: %v", err)
|
||||
stream.Write([]byte(buildHttpInternalServerError(fmt.Sprintf("failed to reach target due to networking error: %s", err.Error()))))
|
||||
continue // Continue to next request
|
||||
}
|
||||
|
||||
// Write the entire response (status line, headers, body) to the stream
|
||||
// http.Response.Write handles this for "Connection: close" correctly.
|
||||
// For other connection tokens, manual removal might be needed if they cause issues with QUIC.
|
||||
// For a simple proxy, this is generally sufficient.
|
||||
resp.Header.Del("Connection") // Good practice for proxies
|
||||
|
||||
log.Info().Msgf("Writing response to stream: %s", resp.Status)
|
||||
|
||||
if err := resp.Write(stream); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to write response to stream")
|
||||
resp.Body.Close()
|
||||
return fmt.Errorf("failed to write response to stream: %w", err)
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
|
||||
// Check if client wants to close connection
|
||||
if req.Header.Get("Connection") == "close" {
|
||||
log.Info().Msg("Client requested connection close")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildHttpInternalServerError(message string) string {
|
||||
return fmt.Sprintf("HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{\"message\": \"gateway: %s\"}", message)
|
||||
}
|
||||
|
||||
type CloseWrite interface {
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
func isValidURL(str string) bool {
|
||||
u, err := url.Parse(str)
|
||||
return err == nil && u.Scheme != "" && u.Host != ""
|
||||
}
|
||||
|
||||
func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) {
|
||||
// Create a WaitGroup to wait for both copy operations
|
||||
var wg sync.WaitGroup
|
||||
|
@ -54,6 +54,10 @@ func NewGateway(identityToken string) (Gateway, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) UpdateIdentityAccessToken(accessToken string) {
|
||||
g.httpClient.SetAuthToken(accessToken)
|
||||
}
|
||||
|
||||
func (g *Gateway) ConnectWithRelay() error {
|
||||
relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient)
|
||||
if err != nil {
|
||||
|
@ -5,7 +5,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type AuthStrategyType string
|
||||
@ -18,6 +20,7 @@ var AuthStrategy = struct {
|
||||
GCP_IAM_AUTH AuthStrategyType
|
||||
AWS_IAM_AUTH AuthStrategyType
|
||||
OIDC_AUTH AuthStrategyType
|
||||
JWT_AUTH AuthStrategyType
|
||||
}{
|
||||
UNIVERSAL_AUTH: "universal-auth",
|
||||
KUBERNETES_AUTH: "kubernetes",
|
||||
@ -26,6 +29,7 @@ var AuthStrategy = struct {
|
||||
GCP_IAM_AUTH: "gcp-iam",
|
||||
AWS_IAM_AUTH: "aws-iam",
|
||||
OIDC_AUTH: "oidc-auth",
|
||||
JWT_AUTH: "jwt-auth",
|
||||
}
|
||||
|
||||
var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{
|
||||
@ -36,6 +40,7 @@ var AVAILABLE_AUTH_STRATEGIES = []AuthStrategyType{
|
||||
AuthStrategy.GCP_IAM_AUTH,
|
||||
AuthStrategy.AWS_IAM_AUTH,
|
||||
AuthStrategy.OIDC_AUTH,
|
||||
AuthStrategy.JWT_AUTH,
|
||||
}
|
||||
|
||||
func IsAuthMethodValid(authMethod string, allowUserAuth bool) (isValid bool, strategy AuthStrategyType) {
|
||||
@ -84,3 +89,120 @@ func EstablishUserLoginSession() LoggedInUserDetails {
|
||||
|
||||
return loggedInUserDetails
|
||||
}
|
||||
|
||||
type SdkAuthenticator struct {
|
||||
infisicalClient infisicalSdk.InfisicalClientInterface
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func NewSdkAuthenticator(infisicalClient infisicalSdk.InfisicalClientInterface, cmd *cobra.Command) *SdkAuthenticator {
|
||||
return &SdkAuthenticator{
|
||||
infisicalClient: infisicalClient,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
func (a *SdkAuthenticator) HandleUniversalAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
clientId, err := GetCmdFlagOrEnv(a.cmd, "client-id", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME})
|
||||
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
clientSecret, err := GetCmdFlagOrEnv(a.cmd, "client-secret", []string{INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().UniversalAuthLogin(clientId, clientSecret)
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleJwtAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().JwtAuthLogin(identityId, jwt)
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleKubernetesAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
serviceAccountTokenPath, err := GetCmdFlagOrEnv(a.cmd, "service-account-token-path", []string{INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath)
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleAzureAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().AzureAuthLogin(identityId, "")
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleGcpIdTokenAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().GcpIdTokenAuthLogin(identityId)
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleGcpIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
serviceAccountKeyFilePath, err := GetCmdFlagOrEnv(a.cmd, "service-account-key-file-path", []string{INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyFilePath)
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleAwsIamAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().AwsIamAuthLogin(identityId)
|
||||
}
|
||||
|
||||
func (a *SdkAuthenticator) HandleOidcAuthLogin() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
||||
|
||||
identityId, err := GetCmdFlagOrEnv(a.cmd, "machine-identity-id", []string{INFISICAL_MACHINE_IDENTITY_ID_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
jwt, err := GetCmdFlagOrEnv(a.cmd, "jwt", []string{INFISICAL_JWT_NAME, INFISICAL_OIDC_AUTH_JWT_NAME})
|
||||
if err != nil {
|
||||
return infisicalSdk.MachineIdentityCredential{}, err
|
||||
}
|
||||
|
||||
return a.infisicalClient.Auth().OidcAuthLogin(identityId, jwt)
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ const (
|
||||
INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME = "INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH"
|
||||
|
||||
// OIDC Auth
|
||||
INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT"
|
||||
INFISICAL_OIDC_AUTH_JWT_NAME = "INFISICAL_OIDC_AUTH_JWT" // deprecated in favor of INFISICAL_JWT
|
||||
|
||||
// JWT AUTH
|
||||
INFISICAL_JWT_NAME = "INFISICAL_JWT"
|
||||
|
||||
// Generic env variable used for auth methods that require a machine identity ID
|
||||
INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID"
|
||||
|
@ -292,13 +292,18 @@ func GetEnvVarOrFileContent(envName string, filePath string) (string, error) {
|
||||
return fileContent, nil
|
||||
}
|
||||
|
||||
func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
|
||||
func GetCmdFlagOrEnv(cmd *cobra.Command, flag string, envNames []string) (string, error) {
|
||||
value, flagsErr := cmd.Flags().GetString(flag)
|
||||
if flagsErr != nil {
|
||||
return "", flagsErr
|
||||
}
|
||||
if value == "" {
|
||||
value = os.Getenv(envName)
|
||||
for _, env := range envNames {
|
||||
value = strings.TrimSpace(os.Getenv(env))
|
||||
if value != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("please provide %s flag", flag)
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Machine identities"
|
||||
title: "Machine identities"
|
||||
description: "Learn how to set metadata and leverage authentication attributes for machine identities."
|
||||
---
|
||||
|
||||
@ -25,7 +25,7 @@ Machine identities can have metadata set manually, just like users. In addition,
|
||||
|
||||
#### Accessing Attributes From Machine Identity Login
|
||||
|
||||
When machine identities authenticate, they may receive additional payloads/attributes from the service provider.
|
||||
When machine identities authenticate, they may receive additional payloads/attributes from the service provider.
|
||||
For methods like OIDC, these come as claims in the token and can be made available in your policies.
|
||||
|
||||
<Tabs>
|
||||
@ -50,17 +50,29 @@ For methods like OIDC, these come as claims in the token and can be made availab
|
||||
```
|
||||
|
||||
You might map:
|
||||
|
||||
- **department:** to `user.department`
|
||||
|
||||
- **department:** to `user.department`
|
||||
- **role:** to `user.role`
|
||||
|
||||
Once configured, these attributes become available in your policies using the following format:
|
||||
|
||||
|
||||
```
|
||||
{{ identity.auth.oidc.claims.<permission claim name> }}
|
||||
```
|
||||
|
||||
<img src="/images/platform/access-controls/abac-policy-oidc-format.png" />
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes Login Attributes">
|
||||
For identities authenticated using Kubernetes, the service account's namespace and name are available in their policy and can be accessed as follows:
|
||||
|
||||
```
|
||||
{{ identity.auth.kubernetes.namespace }}
|
||||
{{ identity.auth.kubernetes.name }}
|
||||
```
|
||||
|
||||
<img src="/images/platform/access-controls/abac-policy-k8s-format.png" />
|
||||
|
||||
</Tab>
|
||||
<Tab title="Other Authentication Method Attributes">
|
||||
At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
|
||||
|
@ -95,12 +95,28 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
|
||||
</Step>
|
||||
<Step title="(Optional) Modify ElastiCache Statements">
|
||||

|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
<ParamField path="Customize ElastiCache Statement" type="string">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the ElastiCache statement to your needs. This is useful if you want to only give access to a specific resource.
|
||||
|
@ -50,110 +50,304 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
|
||||
|
||||
## Set up Dynamic Secrets with AWS IAM
|
||||
|
||||
<Steps>
|
||||
<Step title="Secret Overview Dashboard">
|
||||
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select AWS IAM">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
<Tabs>
|
||||
<Tab title="Assume Role (Recommended)">
|
||||
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
||||
<Accordion title="Self-Hosted Instance">
|
||||
To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role.
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||
</ParamField>
|
||||
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
The following steps are for instances not deployed on AWS:
|
||||
<Steps>
|
||||
<Step title="Create an IAM User">
|
||||
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
|
||||
</Step>
|
||||
<Step title="Create an Inline Policy">
|
||||
Attach the following inline permission policy to the IAM User to allow it to assume any IAM Roles:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowAssumeAnyRole",
|
||||
"Effect": "Allow",
|
||||
"Action": "sts:AssumeRole",
|
||||
"Resource": "arn:aws:iam::*:role/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Obtain the IAM User Credentials">
|
||||
Obtain the AWS access key ID and secret access key for your IAM User by navigating to **IAM > Users > [Your User] > Security credentials > Access keys**.
|
||||
|
||||
<ParamField path="AWS Access Key" type="string" required>
|
||||
The managing AWS IAM User Access Key
|
||||
</ParamField>
|
||||

|
||||

|
||||

|
||||
</Step>
|
||||
<Step title="Set Up Connection Keys">
|
||||
1. Set the access key as **DYNAMIC_SECRET_AWS_ACCESS_KEY_ID**.
|
||||
2. Set the secret key as **DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY**.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
<ParamField path="AWS Secret Key" type="string" required>
|
||||
The managing AWS IAM User Secret Key
|
||||
</ParamField>
|
||||
<Steps>
|
||||
<Step title="Create the Managing User IAM Role for Infisical">
|
||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||

|
||||
|
||||
<ParamField path="AWS IAM Path" type="string">
|
||||
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
|
||||
</ParamField>
|
||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||
3. Select **Another AWS Account** and provide the appropriate Infisical AWS Account ID: use **381492033652** for the **US region**, and **345594589636** for the **EU region**. This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||
4. (Recommended) <strong>Enable "Require external ID"</strong> and input your **Project ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
|
||||
5. Assign permission as shared in prerequisite.
|
||||
|
||||
<ParamField path="AWS Region" type="string" required>
|
||||
The AWS data center region.
|
||||
</ParamField>
|
||||
<Warning type="warning" title="Security Best Practice: Use External ID to Prevent Confused Deputy Attacks">
|
||||
When configuring an IAM Role that Infisical will assume, it’s highly recommended to enable the **"Require external ID"** option and specify your **Project ID**.
|
||||
|
||||
<ParamField path="IAM User Permission Boundary" type="string" required>
|
||||
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
|
||||
</ParamField>
|
||||
This precaution helps protect your AWS account against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), a potential security vulnerability where Infisical could be tricked into performing actions on your behalf by an unauthorized actor.
|
||||
|
||||
<ParamField path="AWS IAM Groups" type="string">
|
||||
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
<strong>Always enable "Require external ID" and use your Project ID when setting up the IAM Role.</strong>
|
||||
</Warning>
|
||||
</Step>
|
||||
<Step title="Copy the AWS IAM Role ARN">
|
||||

|
||||
</Step>
|
||||
<Step title="Secret Overview Dashboard">
|
||||
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select AWS IAM">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||

|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Policy ARNs" type="string">
|
||||
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS IAM Policy Document" type="string">
|
||||
The AWS IAM inline policy that should be attached to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||

|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
<ParamField path="Method" type="string" required>
|
||||
Select *Assume Role* method.
|
||||
</ParamField>
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
<ParamField path="Aws Role ARN" type="string" required>
|
||||
The ARN of the AWS Role to assume.
|
||||
</ParamField>
|
||||
|
||||

|
||||

|
||||
<ParamField path="AWS IAM Path" type="string">
|
||||
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
|
||||
</ParamField>
|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
<ParamField path="AWS Region" type="string" required>
|
||||
The AWS data center region.
|
||||
</ParamField>
|
||||
|
||||

|
||||
<ParamField path="IAM User Permission Boundary" type="string" required>
|
||||
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
|
||||
</ParamField>
|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||
</Tip>
|
||||
<ParamField path="AWS IAM Groups" type="string">
|
||||
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Policy ARNs" type="string">
|
||||
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
<ParamField path="AWS IAM Policy Document" type="string">
|
||||
The AWS IAM inline policy that should be attached to the created users.
|
||||
Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
</Step>
|
||||
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||

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

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||
</Tip>
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Access Key">
|
||||
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
|
||||
<Steps>
|
||||
<Step title="Secret Overview Dashboard">
|
||||
Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select AWS IAM">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||

|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Method" type="string" required>
|
||||
Select *Access Key* method.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Access Key" type="string" required>
|
||||
The managing AWS IAM User Access Key
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Secret Key" type="string" required>
|
||||
The managing AWS IAM User Secret Key
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS IAM Path" type="string">
|
||||
[IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Region" type="string" required>
|
||||
The AWS data center region.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="IAM User Permission Boundary" type="string" required>
|
||||
The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS IAM Groups" type="string">
|
||||
The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS Policy ARNs" type="string">
|
||||
The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="AWS IAM Policy Document" type="string">
|
||||
The AWS IAM inline policy that should be attached to the created users.
|
||||
Multiple values can be provided by separating them with commas
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||

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

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||
</Tip>
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Audit or Revoke Leases
|
||||
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
This will allow you to see the lease details and delete the lease ahead of its expiration time.
|
||||
|
||||

|
||||
|
||||
## Renew Leases
|
||||
|
||||
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
|
||||

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||
secret
|
||||
</Warning>
|
||||
|
@ -80,11 +80,27 @@ The above configuration allows user creation and granting permissions.
|
||||
<Step title="(Optional) Modify CQL Statements">
|
||||

|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
<ParamField path="Customize CQL Statement" type="string">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the CQL statement to your needs. This is useful if you want to only give access to a specific key-space(s).
|
||||
|
@ -87,13 +87,29 @@ The port that your Elasticsearch instance is running on. _(Example: 9200)_
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
|
||||
</ParamField>
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
||||
|
@ -123,13 +123,29 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
|
||||
changetype: delete
|
||||
```
|
||||
</ParamField>
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
</Step>
|
||||
|
||||
<Step title="Click `Submit`">
|
||||
|
@ -63,13 +63,29 @@ Create a project scoped API Key with the required permission in your Mongo Atlas
|
||||
|
||||

|
||||
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
<ParamField path="Customize Scope" type="string">
|
||||
|
||||
List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project.
|
||||
|
@ -66,12 +66,28 @@ Create a user with the required permission in your MongoDB instance. This user w
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required if your DB requires it for incoming connections.
|
||||
</ParamField>
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
@ -9,7 +9,6 @@ The Infisical MS SQL dynamic secret allows you to generate Microsoft SQL server
|
||||
|
||||
Create a user with the required permission in your SQL instance. This user will be used to create new accounts on-demand.
|
||||
|
||||
|
||||
## Set up Dynamic Secrets with MS SQL
|
||||
|
||||
<Steps>
|
||||
@ -27,104 +26,123 @@ Create a user with the required permission in your SQL instance. This user will
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||
</ParamField>
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Metadata" type="list" required>
|
||||
List of key/value metadata pairs
|
||||
</ParamField>
|
||||
<ParamField path="Metadata" type="list" required>
|
||||
List of key/value metadata pairs
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Service" type="string" required>
|
||||
Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**.
|
||||
</ParamField>
|
||||
<ParamField path="Service" type="string" required>
|
||||
Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Host" type="string" required>
|
||||
Database host
|
||||
</ParamField>
|
||||
<ParamField path="Host" type="string" required>
|
||||
Database host
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Port" type="number" required>
|
||||
Database port
|
||||
</ParamField>
|
||||
<ParamField path="Port" type="number" required>
|
||||
Database port
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="User" type="string" required>
|
||||
Username that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
<ParamField path="User" type="string" required>
|
||||
Username that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Password" type="string" required>
|
||||
Password that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
<ParamField path="Password" type="string" required>
|
||||
Password that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Database Name" type="string" required>
|
||||
Name of the database for which you want to create dynamic secrets
|
||||
</ParamField>
|
||||
<ParamField path="Database Name" type="string" required>
|
||||
Name of the database for which you want to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
|
||||
</ParamField>
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
|
||||
</ParamField>
|
||||
|
||||

|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Modify SQL Statements">
|
||||

|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
<ParamField path="Customize SQL Statement" type="string">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
|
||||
</ParamField>
|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
|
||||
<Note>
|
||||
If this step fails, you may have to add the CA certificate.
|
||||
</Note>
|
||||
<Note>
|
||||
If this step fails, you may have to add the CA certificate.
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||

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

|
||||

|
||||

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
|
||||
</Tip>
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
|
||||
</Tip>
|
||||
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Audit or Revoke Leases
|
||||
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
|
||||
|
||||

|
||||
|
||||
## Renew Leases
|
||||
|
||||
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
|
||||

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||
secret
|
||||
</Warning>
|
||||
|
@ -69,15 +69,28 @@ Create a user with the required permission in your SQL instance. This user will
|
||||
</Step>
|
||||
<Step title="(Optional) Modify SQL Statements">
|
||||

|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
<ParamField path="Customize SQL Statement" type="string">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
</Step>
|
||||
<Step title="Click `Submit`">
|
||||
|
@ -71,15 +71,28 @@ Create a user with the required permission in your SQL instance. This user will
|
||||
</Step>
|
||||
<Step title="(Optional) Modify SQL Statements">
|
||||

|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
</ParamField>
|
||||
<ParamField path="Customize SQL Statement" type="string">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
|
@ -72,12 +72,28 @@ Create a user with the required permission in your SQL instance. This user will
|
||||
</Step>
|
||||
<Step title="(Optional) Modify SQL Statements">
|
||||

|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
<ParamField path="Username Template" type="string" default="{{randomUsername}}">
|
||||
Specifies a template for generating usernames. This field allows customization of how usernames are automatically created.
|
||||
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
Allowed template variables are
|
||||
- `{{randomUsername}}`: Random username string
|
||||
- `{{unixTimestamp}}`: Current Unix timestamp
|
||||
- `{{identity.name}}`: Name of the identity that is generating the secret
|
||||
- `{{random N}}`: Random string of N characters
|
||||
|
||||
Allowed template functions are
|
||||
- `truncate`: Truncates a string to a specified length
|
||||
- `replace`: Replaces a substring with another value
|
||||
|
||||
Examples:
|
||||
```
|
||||
{{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
|
||||
{{unixTimestamp}} // 17490641580
|
||||
{{identity.name}} // testuser
|
||||
{{random-5}} // x9k2m
|
||||
{{truncate identity.name 4}} // test
|
||||
{{replace identity.name 'user' 'replace'}} // testreplace
|
||||
```
|
||||
</ParamField>
|
||||
<ParamField path="Customize SQL Statement" type="string">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs. This is useful if you want to only give access to a specific table(s).
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user