Merge pull request #3667 from akhilmhdh/feat/dynamic-secret-username-template

Feat/dynamic secret username template
This commit is contained in:
Maidul Islam
2025-06-01 21:59:56 -04:00
committed by GitHub
80 changed files with 1502 additions and 453 deletions

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "usernameTemplate");
if (!hasColumn) {
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.string("usernameTemplate").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.DynamicSecret, "usernameTemplate");
if (hasColumn) {
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.dropColumn("usernameTemplate");
});
}
}

View File

@@ -28,7 +28,8 @@ export const DynamicSecretsSchema = z.object({
updatedAt: z.date(),
encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
gatewayId: z.string().uuid().nullable().optional(),
usernameTemplate: z.string().nullable().optional()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -6,6 +6,8 @@ import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -13,6 +15,28 @@ import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchema
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
const validateUsernameTemplateCharacters = characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Underscore,
CharacterType.Hyphen,
CharacterType.OpenBrace,
CharacterType.CloseBrace,
CharacterType.CloseBracket,
CharacterType.OpenBracket,
CharacterType.Fullstop
]);
const userTemplateSchema = z
.string()
.trim()
.max(255)
.refine((el) => validateUsernameTemplateCharacters(el))
.refine((el) =>
isValidHandleBarTemplate(el, {
allowedExpressions: (val) => ["randomUsername", "unixTimestamp"].includes(val)
})
);
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -52,7 +76,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
metadata: ResourceMetadataSchema.optional()
metadata: ResourceMetadataSchema.optional(),
usernameTemplate: userTemplateSchema.optional()
}),
response: {
200: z.object({
@@ -73,39 +98,6 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
}
});
server.route({
method: "POST",
url: "/entra-id/users",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
}),
response: {
200: z
.object({
name: z.string().min(1).describe("The name of the user"),
id: z.string().min(1).describe("The ID of the user"),
email: z.string().min(1).describe("The email of the user")
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
tenantId: req.body.tenantId,
applicationId: req.body.applicationId,
clientSecret: req.body.clientSecret
});
return data;
}
});
server.route({
method: "PATCH",
url: "/:name",
@@ -150,7 +142,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
})
.nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
metadata: ResourceMetadataSchema.optional()
metadata: ResourceMetadataSchema.optional(),
usernameTemplate: userTemplateSchema.nullable().optional()
})
}),
response: {
@@ -328,4 +321,37 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
return { leases };
}
});
server.route({
method: "POST",
url: "/entra-id/users",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"),
clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration")
}),
response: {
200: z
.object({
name: z.string().min(1).describe("The name of the user"),
id: z.string().min(1).describe("The ID of the user"),
email: z.string().min(1).describe("The email of the user")
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
tenantId: req.body.tenantId,
applicationId: req.body.applicationId,
clientSecret: req.body.clientSecret
});
return data;
}
});
};

View File

@@ -132,7 +132,11 @@ export const dynamicSecretLeaseServiceFactory = ({
let result;
try {
result = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
result = await selectedProvider.create({
inputs: decryptedStoredInput,
expireAt: expireAt.getTime(),
usernameTemplate: dynamicSecretCfg.usernameTemplate
});
} catch (error: unknown) {
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
throw new BadRequestError({ message: error.sqlMessage as string });

View File

@@ -78,7 +78,8 @@ export const dynamicSecretServiceFactory = ({
actorOrgId,
defaultTTL,
actorAuthMethod,
metadata
metadata,
usernameTemplate
}: TCreateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -163,7 +164,8 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
folderId: folder.id,
name,
gatewayId: selectedGatewayId
gatewayId: selectedGatewayId,
usernameTemplate
},
tx
);
@@ -199,7 +201,8 @@ export const dynamicSecretServiceFactory = ({
newName,
actorOrgId,
actorAuthMethod,
metadata
metadata,
usernameTemplate
}: TUpdateDynamicSecretDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -311,7 +314,8 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
name: newName ?? name,
status: null,
gatewayId: selectedGatewayId
gatewayId: selectedGatewayId,
usernameTemplate
},
tx
);

View File

@@ -22,6 +22,7 @@ export type TCreateDynamicSecretDTO = {
name: string;
projectSlug: string;
metadata?: ResourceMetadataDTO;
usernameTemplate?: string | null;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateDynamicSecretDTO = {
@@ -34,6 +35,7 @@ export type TUpdateDynamicSecretDTO = {
inputs?: TProvider["inputs"];
projectSlug: string;
metadata?: ResourceMetadataDTO;
usernameTemplate?: string | null;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteDynamicSecretDTO = {

View File

@@ -132,9 +132,15 @@ const generatePassword = () => {
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
const generateUsername = (usernameTemplate?: string | null) => {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
const randomUsername = `inf-${customAlphabet(charset, 32)()}`;
if (!usernameTemplate) return randomUsername;
return handlebars.compile(usernameTemplate)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
@@ -168,13 +174,14 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
return true;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
if (!(await validateConnection(providerInputs))) {
throw new BadRequestError({ message: "Failed to establish connection" });
}
const leaseUsername = generateUsername();
const leaseUsername = generateUsername(usernameTemplate);
const leasePassword = generatePassword();
const leaseExpiration = new Date(expireAt).toISOString();

View File

@@ -16,6 +16,7 @@ import {
PutUserPolicyCommand,
RemoveUserFromGroupCommand
} from "@aws-sdk/client-iam";
import handlebars from "handlebars";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
@@ -23,8 +24,14 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername;
return handlebars.compile(usernameTemplate)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const AwsIamProvider = (): TDynamicProviderFns => {
@@ -53,11 +60,13 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
return isConnected;
};
const create = async (inputs: unknown) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const createUserRes = await client.send(
new CreateUserCommand({

View File

@@ -55,7 +55,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
return data.success;
};
const create = async (inputs: unknown) => {
const create = async ({ inputs }: { inputs: unknown }) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
if (!data.success) {
@@ -88,7 +88,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
const revoke = async (inputs: unknown, entityId: string) => {
// Creates a new password
await create(inputs);
await create({ inputs });
return { entityId };
};

View File

@@ -14,8 +14,14 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const CassandraProvider = (): TDynamicProviderFns => {
@@ -69,11 +75,12 @@ export const CassandraProvider = (): TDynamicProviderFns => {
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const { keyspace } = providerInputs;
const expiration = new Date(expireAt).toISOString();

View File

@@ -1,4 +1,5 @@
import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import { z } from "zod";
@@ -12,8 +13,14 @@ const generatePassword = () => {
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const ElasticSearchProvider = (): TDynamicProviderFns => {
@@ -64,11 +71,12 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
return infoResponse;
};
const create = async (inputs: unknown) => {
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
const { inputs, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
await connection.security.putUser({

View File

@@ -22,8 +22,14 @@ const encodePassword = (password?: string) => {
return base64Password;
};
const generateUsername = () => {
return alphaNumericNanoId(20);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
const generateLDIF = ({
@@ -190,7 +196,8 @@ export const LdapProvider = (): TDynamicProviderFns => {
return dnArray;
};
const create = async (inputs: unknown) => {
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
const { inputs, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
@@ -217,7 +224,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
});
}
} else {
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });

View File

@@ -360,7 +360,11 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
]);
export type TDynamicProviderFns = {
create: (inputs: unknown, expireAt: number) => Promise<{ entityId: string; data: unknown }>;
create: (arg: {
inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
}) => Promise<{ entityId: string; data: unknown }>;
validateConnection: (inputs: unknown) => Promise<boolean>;
validateProviderInputs: (inputs: object) => Promise<unknown>;
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;

View File

@@ -1,4 +1,5 @@
import axios, { AxiosError } from "axios";
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import { z } from "zod";
@@ -12,8 +13,14 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername;
return handlebars.compile(usernameTemplate)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const MongoAtlasProvider = (): TDynamicProviderFns => {
@@ -57,11 +64,12 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
await client({

View File

@@ -1,3 +1,4 @@
import handlebars from "handlebars";
import { MongoClient } from "mongodb";
import { customAlphabet } from "nanoid";
import { z } from "zod";
@@ -12,8 +13,14 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername;
return handlebars.compile(usernameTemplate)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const MongoDBProvider = (): TDynamicProviderFns => {
@@ -53,11 +60,12 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
return isConnected;
};
const create = async (inputs: unknown) => {
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
const { inputs, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const db = client.db(providerInputs.database);

View File

@@ -1,4 +1,5 @@
import axios, { Axios } from "axios";
import handlebars from "handlebars";
import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
@@ -14,8 +15,14 @@ const generatePassword = () => {
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
type TCreateRabbitMQUser = {
@@ -110,11 +117,12 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
return infoResponse;
};
const create = async (inputs: unknown) => {
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
const { inputs, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
await createRabbitMqUser({

View File

@@ -15,8 +15,14 @@ const generatePassword = () => {
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => {
@@ -115,11 +121,12 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
return pingResponse;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();

View File

@@ -15,8 +15,14 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(25);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
enum SapCommands {
@@ -81,11 +87,12 @@ export const SapAseProvider = (): TDynamicProviderFns => {
return true;
};
const create = async (inputs: unknown) => {
const create = async (data: { inputs: unknown; usernameTemplate?: string | null }) => {
const { inputs, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const username = `inf_${generateUsername()}`;
const password = `${generatePassword()}`;
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const client = await $getClient(providerInputs);
const masterClient = await $getClient(providerInputs, true);

View File

@@ -21,8 +21,14 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
export const SapHanaProvider = (): TDynamicProviderFns => {
@@ -91,10 +97,11 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
return testResult;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();

View File

@@ -17,8 +17,14 @@ const generatePassword = (size = 48) => {
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return `infisical_${alphaNumericNanoId(32)}`; // username must start with alpha character, hence prefix
const generateUsername = (usernameTemplate?: string | null) => {
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)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
const getDaysToExpiry = (expiryDate: Date) => {
@@ -82,12 +88,13 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
return isValidConnection;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const username = generateUsername(usernameTemplate);
const password = generatePassword();
try {

View File

@@ -104,11 +104,21 @@ const generatePassword = (provider: SqlProviders, requirements?: PasswordRequire
}
};
const generateUsername = (provider: SqlProviders) => {
// For oracle, the client assumes everything is upper case when not using quotes around the password
if (provider === SqlProviders.Oracle) return alphaNumericNanoId(32).toUpperCase();
const generateUsername = (provider: SqlProviders, usernameTemplate?: string | null) => {
let randomUsername = "";
return alphaNumericNanoId(32);
// For oracle, the client assumes everything is upper case when not using quotes around the password
if (provider === SqlProviders.Oracle) {
randomUsername = alphaNumericNanoId(32).toUpperCase();
} else {
randomUsername = alphaNumericNanoId(32);
}
if (!usernameTemplate) return randomUsername;
return handlebars.compile(usernameTemplate)({
randomUsername,
unixTimestamp: Math.floor(Date.now() / 100)
});
};
type TSqlDatabaseProviderDTO = {
@@ -210,9 +220,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const create = async (data: { inputs: unknown; expireAt: number; usernameTemplate?: string | null }) => {
const { inputs, expireAt, usernameTemplate } = data;
const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername(providerInputs.client);
const username = generateUsername(providerInputs.client, usernameTemplate);
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });

View File

@@ -19,3 +19,15 @@ export const validateHandlebarTemplate = (templateName: string, template: string
throw new BadRequestError({ message: `Template sanitization failed: ${templateName}` });
});
};
export const isValidHandleBarTemplate = (template: string, dto: SanitizationArg) => {
const parsedAst = handlebars.parse(template);
return parsedAst.body.every((el) => {
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;
}
return false;
});
};

View File

@@ -235,11 +235,9 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,
algorithm: true
}).merge(
z.object({
metadata: ResourceMetadataSchema.optional()
})
);
}).extend({
metadata: ResourceMetadataSchema.optional()
});
export const SanitizedAuditLogStreamSchema = z.object({
id: z.string(),