mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 20:51:50 +00:00
Merge remote-tracking branch 'origin' into ssh-non-interactive
This commit is contained in:
@@ -11,6 +11,7 @@ export enum AppConnection {
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0"
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ import {
|
||||
} from "./terraform-cloud";
|
||||
import { VercelConnectionMethod } from "./vercel";
|
||||
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
||||
import {
|
||||
getWindmillConnectionListItem,
|
||||
validateWindmillConnectionCredentials,
|
||||
WindmillConnectionMethod
|
||||
} from "./windmill";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
@@ -65,6 +70,7 @@ export const listAppConnectionOptions = () => {
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem(),
|
||||
getWindmillConnectionListItem(),
|
||||
getAuth0ConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
@@ -128,7 +134,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -159,6 +166,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
default:
|
||||
@@ -204,5 +213,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -13,5 +13,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
};
|
||||
|
||||
@@ -49,6 +49,8 @@ import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-c
|
||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
||||
import { ValidateWindmillConnectionCredentialsSchema } from "./windmill";
|
||||
import { windmillConnectionService } from "./windmill/windmill-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@@ -71,6 +73,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
@@ -446,6 +449,7 @@ export const appConnectionServiceFactory = ({
|
||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -75,6 +75,12 @@ import {
|
||||
TVercelConnectionConfig,
|
||||
TVercelConnectionInput
|
||||
} from "./vercel";
|
||||
import {
|
||||
TValidateWindmillConnectionCredentialsSchema,
|
||||
TWindmillConnection,
|
||||
TWindmillConnectionConfig,
|
||||
TWindmillConnectionInput
|
||||
} from "./windmill";
|
||||
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
@@ -89,6 +95,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection
|
||||
| TCamundaConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
);
|
||||
|
||||
@@ -109,6 +116,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TPostgresConnectionInput
|
||||
| TMsSqlConnectionInput
|
||||
| TCamundaConnectionInput
|
||||
| TWindmillConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
);
|
||||
|
||||
@@ -132,9 +140,10 @@ export type TAppConnectionConfig =
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
| TTerraformCloudConnectionConfig
|
||||
| TVercelConnectionConfig
|
||||
| TSqlConnectionConfig
|
||||
| TCamundaConnectionConfig
|
||||
| TVercelConnectionConfig
|
||||
| TWindmillConnectionConfig
|
||||
| TAuth0ConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
@@ -148,8 +157,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
| TValidateMsSqlConnectionCredentialsSchema
|
||||
| TValidateCamundaConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateWindmillConnectionCredentialsSchema
|
||||
| TValidateAuth0ConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
||||
@@ -77,20 +77,22 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
|
||||
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
|
||||
const { credentials, app } = config;
|
||||
|
||||
const client = await getSqlConnectionClient({ app, credentials });
|
||||
let client: Knex | undefined;
|
||||
|
||||
try {
|
||||
client = await getSqlConnectionClient({ app, credentials });
|
||||
|
||||
await client.raw(`Select 1`);
|
||||
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
(error as Error)?.message?.replaceAll(credentials.password, "********************") ??
|
||||
"Unable to validate connection: verify credentials"
|
||||
message: `Unable to validate connection: ${
|
||||
(error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials"
|
||||
}`
|
||||
});
|
||||
} finally {
|
||||
await client.destroy();
|
||||
await client?.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ export const validateTerraformCloudConnectionCredentials = async (config: TTerra
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
@@ -27,10 +27,8 @@ export const getVercelConnectionListItem = () => {
|
||||
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
|
||||
const { credentials: inputCredentials } = config;
|
||||
|
||||
let response: AxiosResponse<VercelApp[]> | null = null;
|
||||
|
||||
try {
|
||||
response = await request.get<VercelApp[]>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
|
||||
await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${inputCredentials.apiToken}`
|
||||
}
|
||||
@@ -38,17 +36,14 @@ export const validateVercelConnectionCredentials = async (config: TVercelConnect
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to validate credentials: ${
|
||||
error.response?.data ? JSON.stringify(error.response?.data) : error.message || "Unknown error"
|
||||
}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
if (!response?.data) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get organizations: Response was empty"
|
||||
message: `Unable to validate connection: ${(error as Error).message || "Verify credentials"}`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./windmill-connection-enums";
|
||||
export * from "./windmill-connection-fns";
|
||||
export * from "./windmill-connection-schemas";
|
||||
export * from "./windmill-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum WindmillConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { WindmillConnectionMethod } from "./windmill-connection-enums";
|
||||
import { TWindmillConnection, TWindmillConnectionConfig, TWindmillWorkspace } from "./windmill-connection-types";
|
||||
|
||||
export const getWindmillInstanceUrl = async (config: TWindmillConnectionConfig) => {
|
||||
const instanceUrl = config.credentials.instanceUrl
|
||||
? removeTrailingSlash(config.credentials.instanceUrl)
|
||||
: "https://app.windmill.dev";
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
return instanceUrl;
|
||||
};
|
||||
|
||||
export const getWindmillConnectionListItem = () => {
|
||||
return {
|
||||
name: "Windmill" as const,
|
||||
app: AppConnection.Windmill as const,
|
||||
methods: Object.values(WindmillConnectionMethod) as [WindmillConnectionMethod.AccessToken]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateWindmillConnectionCredentials = async (config: TWindmillConnectionConfig) => {
|
||||
const instanceUrl = await getWindmillInstanceUrl(config);
|
||||
const { accessToken } = config.credentials;
|
||||
|
||||
try {
|
||||
await request.get(`${instanceUrl}/api/workspaces/list`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
|
||||
export const listWindmillWorkspaces = async (appConnection: TWindmillConnection) => {
|
||||
const instanceUrl = await getWindmillInstanceUrl(appConnection);
|
||||
const { accessToken } = appConnection.credentials;
|
||||
|
||||
const resp = await request.get<TWindmillWorkspace[]>(`${instanceUrl}/api/workspaces/list`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return resp.data.filter((workspace) => !workspace.deleted);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { WindmillConnectionMethod } from "./windmill-connection-enums";
|
||||
|
||||
export const WindmillConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.describe(AppConnections.CREDENTIALS.WINDMILL.accessToken),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.WINDMILL.instanceUrl)
|
||||
});
|
||||
|
||||
const BaseWindmillConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Windmill) });
|
||||
|
||||
export const WindmillConnectionSchema = BaseWindmillConnectionSchema.extend({
|
||||
method: z.literal(WindmillConnectionMethod.AccessToken),
|
||||
credentials: WindmillConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedWindmillConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseWindmillConnectionSchema.extend({
|
||||
method: z.literal(WindmillConnectionMethod.AccessToken),
|
||||
credentials: WindmillConnectionAccessTokenCredentialsSchema.pick({
|
||||
instanceUrl: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateWindmillConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(WindmillConnectionMethod.AccessToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.Windmill).method),
|
||||
credentials: WindmillConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Windmill).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateWindmillConnectionSchema = ValidateWindmillConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Windmill)
|
||||
);
|
||||
|
||||
export const UpdateWindmillConnectionSchema = z
|
||||
.object({
|
||||
credentials: WindmillConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Windmill).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Windmill));
|
||||
|
||||
export const WindmillConnectionListItemSchema = z.object({
|
||||
name: z.literal("Windmill"),
|
||||
app: z.literal(AppConnection.Windmill),
|
||||
methods: z.nativeEnum(WindmillConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listWindmillWorkspaces } from "./windmill-connection-fns";
|
||||
import { TWindmillConnection } from "./windmill-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TWindmillConnection>;
|
||||
|
||||
export const windmillConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Windmill, connectionId, actor);
|
||||
|
||||
try {
|
||||
const workspaces = await listWindmillWorkspaces(appConnection);
|
||||
return workspaces;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listWorkspaces
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateWindmillConnectionSchema,
|
||||
ValidateWindmillConnectionCredentialsSchema,
|
||||
WindmillConnectionSchema
|
||||
} from "./windmill-connection-schemas";
|
||||
|
||||
export type TWindmillConnection = z.infer<typeof WindmillConnectionSchema>;
|
||||
|
||||
export type TWindmillConnectionInput = z.infer<typeof CreateWindmillConnectionSchema> & {
|
||||
app: AppConnection.Windmill;
|
||||
};
|
||||
|
||||
export type TValidateWindmillConnectionCredentialsSchema = typeof ValidateWindmillConnectionCredentialsSchema;
|
||||
|
||||
export type TWindmillConnectionConfig = DiscriminativePick<
|
||||
TWindmillConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TWindmillWorkspace = { id: string; name: string; deleted: boolean };
|
||||
@@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||
try {
|
||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter);
|
||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession)
|
||||
.where(filter)
|
||||
.orderBy("lastUsed", "desc");
|
||||
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Find all token session", error });
|
||||
|
||||
@@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
|
||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||
|
||||
const revokeMySessionById = async (userId: string, sessionId: string) =>
|
||||
tokenDAL.deleteTokenSession({ userId, id: sessionId });
|
||||
|
||||
const validateRefreshToken = async (refreshToken?: string) => {
|
||||
const appCfg = getConfig();
|
||||
if (!refreshToken)
|
||||
@@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
clearTokenSessionById,
|
||||
getTokenSessionByUser,
|
||||
revokeAllMySessions,
|
||||
revokeMySessionById,
|
||||
validateRefreshToken,
|
||||
fnValidateJwtIdentity,
|
||||
getUserTokenSessionById
|
||||
|
||||
@@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
@@ -11,6 +13,7 @@ import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
@@ -28,7 +31,15 @@ import {
|
||||
TOauthTokenExchangeDTO,
|
||||
TVerifyMfaTokenDTO
|
||||
} from "./auth-login-type";
|
||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type";
|
||||
import {
|
||||
ActorType,
|
||||
AuthMethod,
|
||||
AuthModeJwtTokenPayload,
|
||||
AuthModeMfaJwtTokenPayload,
|
||||
AuthTokenType,
|
||||
MfaMethod
|
||||
} from "./auth-type";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
@@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({
|
||||
tokenService,
|
||||
smtpService,
|
||||
orgDAL,
|
||||
totpService
|
||||
totpService,
|
||||
auditLogService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({
|
||||
mfaMethod: decodedToken.mfaMethod
|
||||
});
|
||||
|
||||
// In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced)
|
||||
if (
|
||||
selectedOrg.authEnforced &&
|
||||
selectedOrg.bypassOrgAuthEnabled &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC
|
||||
) {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent),
|
||||
actor: {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
userId: user.id,
|
||||
username: user.username
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.ORG_ADMIN_BYPASS_SSO,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Notify all admins via email (besides the actor)
|
||||
const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin);
|
||||
const adminEmails = orgAdmins
|
||||
.filter((admin) => admin.user.id !== user.id)
|
||||
.map((admin) => admin.user.email)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (adminEmails.length > 0) {
|
||||
await smtpService.sendMail({
|
||||
recipients: adminEmails,
|
||||
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||
substitutions: {
|
||||
email: user.email,
|
||||
timestamp: new Date().toISOString(),
|
||||
ip: ipAddress,
|
||||
userAgent,
|
||||
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com")
|
||||
},
|
||||
template: SmtpTemplates.OrgAdminBreakglassAccess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...tokens,
|
||||
isMfaEnabled: false
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { TCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
@@ -12,7 +14,7 @@ export const validateCertificateDetailsAgainstTemplate = (
|
||||
template: TCertificateTemplates
|
||||
) => {
|
||||
// these are validated in router using validateTemplateRegexField
|
||||
const commonNameRegex = new RegExp(template.commonName);
|
||||
const commonNameRegex = new RE2(template.commonName);
|
||||
if (!commonNameRegex.test(cert.commonName)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid common name based on template policy"
|
||||
@@ -25,7 +27,7 @@ export const validateCertificateDetailsAgainstTemplate = (
|
||||
});
|
||||
}
|
||||
|
||||
const subjectAlternativeNameRegex = new RegExp(template.subjectAlternativeName);
|
||||
const subjectAlternativeNameRegex = new RE2(template.subjectAlternativeName);
|
||||
cert.altNames.forEach((altName) => {
|
||||
if (!subjectAlternativeNameRegex.test(altName)) {
|
||||
throw new BadRequestError({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import axios from "axios";
|
||||
import jwt from "jsonwebtoken";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@@ -67,6 +68,15 @@ const awsRegionFromHeader = (authorizationHeader: string): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
function isValidAwsRegion(region: string | null): boolean {
|
||||
const validRegionPattern = new RE2("^[a-z0-9-]+$");
|
||||
if (typeof region !== "string" || region.length === 0 || region.length > 20) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return validRegionPattern.test(region);
|
||||
}
|
||||
|
||||
export const identityAwsAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
@@ -84,8 +94,12 @@ export const identityAwsAuthServiceFactory = ({
|
||||
|
||||
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
|
||||
const body: string = Buffer.from(iamRequestBody, "base64").toString();
|
||||
|
||||
const region = headers.Authorization ? awsRegionFromHeader(headers.Authorization) : null;
|
||||
|
||||
if (!isValidAwsRegion(region)) {
|
||||
throw new BadRequestError({ message: "Invalid AWS region" });
|
||||
}
|
||||
|
||||
const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint;
|
||||
|
||||
const {
|
||||
@@ -125,7 +139,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
|
||||
// considers exact matches + wildcard matches
|
||||
// heavily validated in router
|
||||
const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`);
|
||||
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
|
||||
return regex.test(extractPrincipalArn(Arn));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import RE2 from "re2";
|
||||
import safe from "safe-regex";
|
||||
import { z } from "zod";
|
||||
|
||||
const twelveDigitRegex = /^\d{12}$/;
|
||||
const twelveDigitRegex = new RE2(/^\d{12}$/);
|
||||
// akhilmhdh: change this to a normal function later. Checked no redosable at the moment
|
||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
|
||||
const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/);
|
||||
|
||||
export const validateAccountIds = z
|
||||
.string()
|
||||
|
||||
@@ -435,12 +435,16 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||
if (!identityKubernetesAuth) {
|
||||
throw new NotFoundError({ message: `Failed to find Kubernetes Auth for identity with ID ${identityId}` });
|
||||
}
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have Kubernetes Auth attached"
|
||||
});
|
||||
}
|
||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
|
||||
@@ -50,7 +50,7 @@ const getIntegrationSecretsV2 = async (
|
||||
}
|
||||
|
||||
// process secrets in current folder
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId });
|
||||
|
||||
secrets.forEach((secret) => {
|
||||
const secretKey = secret.key;
|
||||
@@ -63,7 +63,6 @@ const getIntegrationSecretsV2 = async (
|
||||
// if no imports then return secrets in the current folder
|
||||
if (!secretImports.length) return content;
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
projectId: dto.projectId,
|
||||
decryptor: dto.decryptor,
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { randomUUID } from "crypto";
|
||||
import https from "https";
|
||||
import sodium from "libsodium-wrappers";
|
||||
import isEqual from "lodash.isequal";
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
|
||||
@@ -4151,7 +4152,7 @@ const syncSecretsWindmill = async ({
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
|
||||
const pattern = new RE2("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$");
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) {
|
||||
if (!(key in res)) {
|
||||
|
||||
@@ -787,13 +787,19 @@ export const kmsServiceFactory = ({
|
||||
return projectDataKey;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await lock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.kmsSecretManagerEncryptedDataKey) {
|
||||
throw new Error("Missing project data key");
|
||||
throw new BadRequestError({ message: "Missing project data key" });
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
TableName,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
@@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
|
||||
try {
|
||||
const conn = tx || db;
|
||||
const conn = tx || db.replicaNode();
|
||||
const members = await conn(TableName.OrgMembership)
|
||||
// .replicaNode()(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
@@ -251,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => {
|
||||
try {
|
||||
const conn = tx || db.replicaNode();
|
||||
const members = await conn(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.where(`${TableName.OrgMembership}.role`, role)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
conn.ref("id").withSchema(TableName.OrgMembership),
|
||||
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
conn.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("role").withSchema(TableName.OrgMembership),
|
||||
conn.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("status").withSchema(TableName.OrgMembership),
|
||||
conn.ref("username").withSchema(TableName.Users),
|
||||
conn.ref("email").withSchema(TableName.Users),
|
||||
conn.ref("firstName").withSchema(TableName.Users),
|
||||
conn.ref("lastName").withSchema(TableName.Users),
|
||||
conn.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||
...data,
|
||||
user: { username, email, firstName, lastName, id: userId, publicKey }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org members by role" });
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgGhostUser = async (orgId: string) => {
|
||||
try {
|
||||
const member = await db
|
||||
@@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
findAllOrgsByUserId,
|
||||
ghostUserExists,
|
||||
findOrgMembersByUsername,
|
||||
findOrgMembersByRole,
|
||||
findOrgGhostUser,
|
||||
create,
|
||||
updateById,
|
||||
|
||||
@@ -110,8 +110,8 @@ type TOrgServiceFactoryDep = {
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
||||
incidentContactDAL: TIncidentContactsDALFactory;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "findEnforceableOidcCfg">;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||
smtpService: TSmtpService;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
@@ -403,13 +403,33 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
|
||||
if (authEnforced) {
|
||||
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
||||
const oidcCfg = await oidcConfigDAL.findEnforceableOidcCfg(orgId);
|
||||
const samlCfg = await samlConfigDAL.findOne({
|
||||
orgId,
|
||||
isActive: true
|
||||
});
|
||||
const oidcCfg = await oidcConfigDAL.findOne({
|
||||
orgId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!samlCfg && !oidcCfg)
|
||||
throw new NotFoundError({
|
||||
message: `SAML or OIDC configuration for organization with ID '${orgId}' not found`
|
||||
});
|
||||
|
||||
if (samlCfg && !samlCfg.lastUsed) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"To apply the new SAML auth enforcement, please log in via SAML at least once. This step is required to enforce SAML-based authentication."
|
||||
});
|
||||
}
|
||||
|
||||
if (oidcCfg && !oidcCfg.lastUsed) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"To apply the new OIDC auth enforcement, please log in via OIDC at least once. This step is required to enforce OIDC-based authentication."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let defaultMembershipRole: string | undefined;
|
||||
|
||||
@@ -159,8 +159,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
decryptor,
|
||||
expandSecretReferences,
|
||||
hasSecretAccess,
|
||||
viewSecretValue,
|
||||
projectId
|
||||
viewSecretValue
|
||||
}: {
|
||||
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
||||
importEnv: { id: string; slug: string; name: string };
|
||||
@@ -177,7 +176,6 @@ export const fnSecretsV2FromImports = async ({
|
||||
environment: string;
|
||||
}) => Promise<string | undefined>;
|
||||
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
||||
projectId: string;
|
||||
}) => {
|
||||
const cyclicDetector = new Set();
|
||||
const stack: {
|
||||
@@ -209,7 +207,10 @@ export const fnSecretsV2FromImports = async ({
|
||||
);
|
||||
if (!importedFolders.length) continue;
|
||||
|
||||
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
|
||||
const importedFolderIds = importedFolders.filter(Boolean).map((el) => el?.id) as string[];
|
||||
|
||||
if (!importedFolderIds.length) continue;
|
||||
|
||||
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
|
||||
|
||||
const importedSecrets = await secretDAL.find(
|
||||
@@ -218,8 +219,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
type: SecretType.Shared
|
||||
},
|
||||
{
|
||||
sort: [["id", "asc"]],
|
||||
useCache: { projectId }
|
||||
sort: [["id", "asc"]]
|
||||
}
|
||||
);
|
||||
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
||||
|
||||
@@ -698,7 +698,6 @@ export const secretImportServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
projectId,
|
||||
secretImports,
|
||||
folderDAL,
|
||||
viewSecretValue: true,
|
||||
|
||||
@@ -9,7 +9,8 @@ export enum SecretSync {
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Camunda = "camunda",
|
||||
Vercel = "vercel"
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
|
||||
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
||||
import { WINDMILL_SYNC_LIST_OPTION, WindmillSyncFns } from "./windmill";
|
||||
|
||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
@@ -41,7 +42,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION,
|
||||
[SecretSync.TerraformCloud]: TERRAFORM_CLOUD_SYNC_LIST_OPTION,
|
||||
[SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION,
|
||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION
|
||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
|
||||
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -136,6 +138,8 @@ export const SecretSyncFns = {
|
||||
}).syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Vercel:
|
||||
return VercelSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Windmill:
|
||||
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -192,6 +196,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Vercel:
|
||||
secretMap = await VercelSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Windmill:
|
||||
secretMap = await WindmillSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -243,6 +250,8 @@ export const SecretSyncFns = {
|
||||
}).removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Vercel:
|
||||
return VercelSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Windmill:
|
||||
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -12,7 +12,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Humanitec]: "Humanitec",
|
||||
[SecretSync.TerraformCloud]: "Terraform Cloud",
|
||||
[SecretSync.Camunda]: "Camunda",
|
||||
[SecretSync.Vercel]: "Vercel"
|
||||
[SecretSync.Vercel]: "Vercel",
|
||||
[SecretSync.Windmill]: "Windmill"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -26,5 +27,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Humanitec]: AppConnection.Humanitec,
|
||||
[SecretSync.TerraformCloud]: AppConnection.TerraformCloud,
|
||||
[SecretSync.Camunda]: AppConnection.Camunda,
|
||||
[SecretSync.Vercel]: AppConnection.Vercel
|
||||
[SecretSync.Vercel]: AppConnection.Vercel,
|
||||
[SecretSync.Windmill]: AppConnection.Windmill
|
||||
};
|
||||
|
||||
@@ -76,6 +76,7 @@ type TSecretSyncQueueFactoryDep = {
|
||||
| "findBySecretKeys"
|
||||
| "bulkUpdate"
|
||||
| "deleteMany"
|
||||
| "invalidateSecretCacheByProjectId"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
|
||||
@@ -213,7 +214,7 @@ export const secretSyncQueueFactory = ({
|
||||
canExpandValue: () => true
|
||||
});
|
||||
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId, projectId });
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId });
|
||||
|
||||
await Promise.allSettled(
|
||||
secrets.map(async (secret) => {
|
||||
@@ -243,7 +244,6 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
if (secretImports.length) {
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
projectId,
|
||||
decryptor: decryptSecretValue,
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
@@ -382,6 +382,9 @@ export const secretSyncQueueFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (secretsToUpdate.length || secretsToCreate.length)
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
|
||||
return importedSecretMap;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ import {
|
||||
} from "@app/services/secret-sync/github";
|
||||
import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
|
||||
import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
TWindmillSync,
|
||||
TWindmillSyncInput,
|
||||
TWindmillSyncListItem,
|
||||
TWindmillSyncWithCredentials
|
||||
} from "@app/services/secret-sync/windmill";
|
||||
|
||||
import {
|
||||
TAwsParameterStoreSync,
|
||||
@@ -74,7 +80,8 @@ export type TSecretSync =
|
||||
| THumanitecSync
|
||||
| TTerraformCloudSync
|
||||
| TCamundaSync
|
||||
| TVercelSync;
|
||||
| TVercelSync
|
||||
| TWindmillSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -87,7 +94,8 @@ export type TSecretSyncWithCredentials =
|
||||
| THumanitecSyncWithCredentials
|
||||
| TTerraformCloudSyncWithCredentials
|
||||
| TCamundaSyncWithCredentials
|
||||
| TVercelSyncWithCredentials;
|
||||
| TVercelSyncWithCredentials
|
||||
| TWindmillSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -100,7 +108,8 @@ export type TSecretSyncInput =
|
||||
| THumanitecSyncInput
|
||||
| TTerraformCloudSyncInput
|
||||
| TCamundaSyncInput
|
||||
| TVercelSyncInput;
|
||||
| TVercelSyncInput
|
||||
| TWindmillSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -113,7 +122,8 @@ export type TSecretSyncListItem =
|
||||
| THumanitecSyncListItem
|
||||
| TTerraformCloudSyncListItem
|
||||
| TCamundaSyncListItem
|
||||
| TVercelSyncListItem;
|
||||
| TVercelSyncListItem
|
||||
| TWindmillSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./windmill-sync-constants";
|
||||
export * from "./windmill-sync-fns";
|
||||
export * from "./windmill-sync-schemas";
|
||||
export * from "./windmill-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const WINDMILL_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Windmill",
|
||||
destination: SecretSync.Windmill,
|
||||
connection: AppConnection.Windmill,
|
||||
canImportSecrets: true
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import {
|
||||
TDeleteWindmillVariable,
|
||||
TPostWindmillVariable,
|
||||
TWindmillListVariables,
|
||||
TWindmillListVariablesResponse,
|
||||
TWindmillSyncWithCredentials,
|
||||
TWindmillVariable
|
||||
} from "@app/services/secret-sync/windmill/windmill-sync-types";
|
||||
|
||||
import { TSecretMap } from "../secret-sync-types";
|
||||
|
||||
const PAGE_LIMIT = 100;
|
||||
|
||||
const listWindmillVariables = async ({ instanceUrl, workspace, accessToken, path }: TWindmillListVariables) => {
|
||||
const variables: Record<string, TWindmillVariable> = {};
|
||||
|
||||
// windmill paginates but doesn't return if there's more pages so we need to check if page size full
|
||||
let page: number | null = 1;
|
||||
|
||||
while (page) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: variablesPage } = await request.get<TWindmillListVariablesResponse>(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/list`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
params: {
|
||||
page,
|
||||
limit: PAGE_LIMIT,
|
||||
path_start: path
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
for (const variable of variablesPage) {
|
||||
const variableName = variable.path.replace(path, "");
|
||||
|
||||
if (variable.is_secret) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data: variableValue } = await request.get<string>(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/get_value/${variable.path}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
variables[variableName] = {
|
||||
...variable,
|
||||
value: variableValue
|
||||
};
|
||||
} else {
|
||||
variables[variableName] = variable;
|
||||
}
|
||||
}
|
||||
|
||||
if (variablesPage.length >= PAGE_LIMIT) {
|
||||
page += 1;
|
||||
} else {
|
||||
page = null;
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
const createWindmillVariable = async ({
|
||||
path,
|
||||
value,
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
workspace,
|
||||
description
|
||||
}: TPostWindmillVariable) =>
|
||||
request.post(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/create`,
|
||||
{
|
||||
path,
|
||||
value,
|
||||
is_secret: true,
|
||||
description
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const updateWindmillVariable = async ({
|
||||
path,
|
||||
value,
|
||||
instanceUrl,
|
||||
accessToken,
|
||||
workspace,
|
||||
description
|
||||
}: TPostWindmillVariable) =>
|
||||
request.post(
|
||||
`${instanceUrl}/api/w/${workspace}/variables/update/${path}`,
|
||||
{
|
||||
value,
|
||||
is_secret: true,
|
||||
description
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const deleteWindmillVariable = async ({ path, instanceUrl, accessToken, workspace }: TDeleteWindmillVariable) =>
|
||||
request.delete(`${instanceUrl}/api/w/${workspace}/variables/delete/${path}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
export const WindmillSyncFns = {
|
||||
syncSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { path },
|
||||
syncOptions: { disableSecretDeletion }
|
||||
} = secretSync;
|
||||
|
||||
// url needs to be lowercase
|
||||
const workspace = secretSync.destinationConfig.workspace.toLowerCase();
|
||||
|
||||
const instanceUrl = await getWindmillInstanceUrl(connection);
|
||||
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value, comment = "" }] = entry;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
instanceUrl,
|
||||
workspace,
|
||||
path: path + key,
|
||||
value,
|
||||
accessToken,
|
||||
description: comment
|
||||
};
|
||||
if (key in variables) {
|
||||
if (variables[key].value !== value || variables[key].description !== comment)
|
||||
await updateWindmillVariable(payload);
|
||||
} else {
|
||||
await createWindmillVariable(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (!(key in secretMap)) {
|
||||
try {
|
||||
await deleteWindmillVariable({
|
||||
instanceUrl,
|
||||
workspace,
|
||||
path: variable.path,
|
||||
accessToken
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TWindmillSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { path }
|
||||
} = secretSync;
|
||||
|
||||
// url needs to be lowercase
|
||||
const workspace = secretSync.destinationConfig.workspace.toLowerCase();
|
||||
|
||||
const instanceUrl = await getWindmillInstanceUrl(connection);
|
||||
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (key in secretMap) {
|
||||
try {
|
||||
await deleteWindmillVariable({
|
||||
path: variable.path,
|
||||
instanceUrl,
|
||||
workspace,
|
||||
accessToken
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TWindmillSyncWithCredentials) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { path }
|
||||
} = secretSync;
|
||||
|
||||
// url needs to be lowercase
|
||||
const workspace = secretSync.destinationConfig.workspace.toLowerCase();
|
||||
|
||||
const instanceUrl = await getWindmillInstanceUrl(connection);
|
||||
|
||||
const { accessToken } = connection.credentials;
|
||||
|
||||
const variables = await listWindmillVariables({ instanceUrl, accessToken, workspace, path });
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(variables).map(([key, variable]) => [key, { value: variable.value ?? "" }])
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const pathCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
]);
|
||||
|
||||
const WindmillSyncDestinationConfigSchema = z.object({
|
||||
workspace: z.string().trim().min(1, "Workspace required").describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.workspace),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Path required")
|
||||
.refine(
|
||||
(val) =>
|
||||
(val.startsWith("u/") || val.startsWith("f/")) &&
|
||||
val.endsWith("/") &&
|
||||
val.split("/").length >= 3 &&
|
||||
val
|
||||
.split("/")
|
||||
.slice(0, -1) // Remove last empty segment from trailing slash
|
||||
.every((segment) => segment && pathCharacterValidator(segment)),
|
||||
'Invalid path - must follow Windmill path format. ex: "f/folder/path/"'
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.WINDMILL.path)
|
||||
});
|
||||
|
||||
const WindmillSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const WindmillSyncSchema = BaseSecretSyncSchema(SecretSync.Windmill, WindmillSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Windmill),
|
||||
destinationConfig: WindmillSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateWindmillSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Windmill,
|
||||
WindmillSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: WindmillSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateWindmillSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Windmill,
|
||||
WindmillSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: WindmillSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const WindmillSyncListItemSchema = z.object({
|
||||
name: z.literal("Windmill"),
|
||||
connection: z.literal(AppConnection.Windmill),
|
||||
destination: z.literal(SecretSync.Windmill),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TWindmillConnection } from "@app/services/app-connection/windmill";
|
||||
|
||||
import { CreateWindmillSyncSchema, WindmillSyncListItemSchema, WindmillSyncSchema } from "./windmill-sync-schemas";
|
||||
|
||||
export type TWindmillSync = z.infer<typeof WindmillSyncSchema>;
|
||||
|
||||
export type TWindmillSyncInput = z.infer<typeof CreateWindmillSyncSchema>;
|
||||
|
||||
export type TWindmillSyncListItem = z.infer<typeof WindmillSyncListItemSchema>;
|
||||
|
||||
export type TWindmillSyncWithCredentials = TWindmillSync & {
|
||||
connection: TWindmillConnection;
|
||||
};
|
||||
|
||||
export type TWindmillVariable = {
|
||||
path: string;
|
||||
value: string;
|
||||
is_secret: boolean;
|
||||
is_oauth: boolean;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type TWindmillListVariablesResponse = TWindmillVariable[];
|
||||
|
||||
export type TWindmillListVariables = {
|
||||
accessToken: string;
|
||||
instanceUrl: string;
|
||||
path: string;
|
||||
workspace: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TPostWindmillVariable = TWindmillListVariables & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TDeleteWindmillVariable = TWindmillListVariables;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MongoAbility } from "@casl/ability";
|
||||
import { Knex } from "knex";
|
||||
import { validate as uuidValidate } from "uuid";
|
||||
|
||||
@@ -15,46 +16,29 @@ import {
|
||||
TFindFilter,
|
||||
TFindOpt
|
||||
} from "@app/lib/knex";
|
||||
import { BufferKeysToString, OrderByDirection } from "@app/lib/types";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
import type { TFindSecretsByFolderIdsFilter } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||
import type {
|
||||
TFindSecretsByFolderIdsFilter,
|
||||
TGetSecretsDTO
|
||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||
|
||||
export const SecretDalCacheKeys = {
|
||||
export const SecretServiceCacheKeys = {
|
||||
get productKey() {
|
||||
const { INFISICAL_PLATFORM_VERSION } = getConfig();
|
||||
return `${ProjectType.SecretManager}:${INFISICAL_PLATFORM_VERSION || 0}`;
|
||||
},
|
||||
getSecretDalVersion: (projectId: string) => {
|
||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`;
|
||||
return `${SecretServiceCacheKeys.productKey}:${projectId}:${TableName.SecretV2}-dal-version`;
|
||||
},
|
||||
findByFolderIds: (
|
||||
getSecretsOfServiceLayer: (
|
||||
projectId: string,
|
||||
version: number,
|
||||
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderIds"]>[0]
|
||||
dto: TGetSecretsDTO & { permissionRules: MongoAbility["rules"] }
|
||||
) => {
|
||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
||||
return `${SecretServiceCacheKeys.productKey}:${projectId}:${
|
||||
TableName.SecretV2
|
||||
}-dal:v${version}:find-by-folder-ids:${generateCacheKeyFromData(cacheKey)}`;
|
||||
},
|
||||
findByFolderId: (
|
||||
projectId: string,
|
||||
version: number,
|
||||
{ useCache, tx, ...cacheKey }: Parameters<TSecretV2BridgeDALFactory["findByFolderId"]>[0]
|
||||
) => {
|
||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
||||
TableName.SecretV2
|
||||
}-dal:v${version}:find-by-folder-id:${generateCacheKeyFromData(cacheKey)}`;
|
||||
},
|
||||
find: (projectId: string, version: number, ...args: Parameters<TSecretV2BridgeDALFactory["find"]>) => {
|
||||
const [filter, opts] = args;
|
||||
delete opts?.tx;
|
||||
delete opts?.useCache;
|
||||
return `${SecretDalCacheKeys.productKey}:${projectId}:${
|
||||
TableName.SecretV2
|
||||
}-dal:v${version}:find:${generateCacheKeyFromData({
|
||||
filter,
|
||||
opts
|
||||
})}`;
|
||||
}-dal:v${version}:get-secrets-service-layer:${dto.actorId}-${generateCacheKeyFromData(dto)}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,14 +48,14 @@ interface TSecretV2DalArg {
|
||||
keyStore: TKeyStoreFactory;
|
||||
}
|
||||
|
||||
const SECRET_DAL_TTL = 5 * 60;
|
||||
const SECRET_DAL_VERSION_TTL = 15 * 60;
|
||||
const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
||||
export const SECRET_DAL_TTL = 5 * 60;
|
||||
export const SECRET_DAL_VERSION_TTL = 15 * 60;
|
||||
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
||||
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
const secretOrm = ormify(db, TableName.SecretV2);
|
||||
|
||||
const invalidateSecretCacheByProjectId = async (projectId: string) => {
|
||||
const secretDalVersionKey = SecretDalCacheKeys.getSecretDalVersion(projectId);
|
||||
const secretDalVersionKey = SecretServiceCacheKeys.getSecretDalVersion(projectId);
|
||||
await keyStore.incrementBy(secretDalVersionKey, 1);
|
||||
await keyStore.setExpiry(secretDalVersionKey, SECRET_DAL_VERSION_TTL);
|
||||
};
|
||||
@@ -128,35 +112,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (
|
||||
filter: TFindFilter<TSecretsV2>,
|
||||
opts: TFindOpt<TSecretsV2> & { useCache?: { projectId: string } } = {}
|
||||
) => {
|
||||
const { offset, limit, sort, tx, useCache } = opts;
|
||||
const find = async (filter: TFindFilter<TSecretsV2>, opts: TFindOpt<TSecretsV2> = {}) => {
|
||||
const { offset, limit, sort, tx } = opts;
|
||||
try {
|
||||
let secretDalVersion = 0;
|
||||
if (useCache) {
|
||||
const cachedSecretDalVersion = await keyStore.getItem(
|
||||
SecretDalCacheKeys.getSecretDalVersion(useCache.projectId)
|
||||
);
|
||||
secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||
const cacheKey = SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts);
|
||||
const cachedSecrets = await keyStore.getItem(cacheKey);
|
||||
if (cachedSecrets) {
|
||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||
|
||||
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
||||
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
||||
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
||||
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
||||
const createdAt = new Date(el.createdAt);
|
||||
const updatedAt = new Date(el.updatedAt);
|
||||
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
||||
});
|
||||
return sanitizedSecrets;
|
||||
}
|
||||
}
|
||||
|
||||
const query = (tx || db)(TableName.SecretV2)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter))
|
||||
@@ -225,22 +183,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
]
|
||||
});
|
||||
|
||||
if (useCache) {
|
||||
const cachedSecrets = data.map((el) => {
|
||||
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
||||
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
||||
return { ...el, encryptedValue, encryptedComment };
|
||||
});
|
||||
const cache = JSON.stringify(cachedSecrets);
|
||||
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
||||
await keyStore.setItemWithExpiry(
|
||||
SecretDalCacheKeys.find(useCache.projectId, secretDalVersion, filter, opts),
|
||||
SECRET_DAL_TTL,
|
||||
cache
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
|
||||
@@ -345,15 +287,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderId = async (dto: {
|
||||
folderId: string;
|
||||
userId?: string;
|
||||
tx?: Knex;
|
||||
projectId: string;
|
||||
useCache?: boolean;
|
||||
}) => {
|
||||
const findByFolderId = async (dto: { folderId: string; userId?: string; tx?: Knex }) => {
|
||||
try {
|
||||
const { folderId, tx, projectId } = dto;
|
||||
const { folderId, tx } = dto;
|
||||
let { userId } = dto;
|
||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo
|
||||
if (userId && !uuidValidate(userId)) {
|
||||
@@ -361,27 +297,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
userId = undefined;
|
||||
}
|
||||
|
||||
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
|
||||
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||
|
||||
if (dto.useCache) {
|
||||
const cacheKey = SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto);
|
||||
const cachedSecrets = await keyStore.getItem(cacheKey);
|
||||
if (cachedSecrets) {
|
||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||
|
||||
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
||||
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
||||
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
||||
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
||||
const createdAt = new Date(el.createdAt);
|
||||
const updatedAt = new Date(el.updatedAt);
|
||||
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
||||
});
|
||||
return sanitizedSecrets;
|
||||
}
|
||||
}
|
||||
|
||||
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.where({ folderId })
|
||||
.where((bd) => {
|
||||
@@ -437,22 +352,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
if (dto.useCache) {
|
||||
const newCachedSecrets = data.map((el) => {
|
||||
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
||||
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
||||
return { ...el, encryptedValue, encryptedComment };
|
||||
});
|
||||
const cache = JSON.stringify(newCachedSecrets);
|
||||
|
||||
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
||||
await keyStore.setItemWithExpiry(
|
||||
SecretDalCacheKeys.findByFolderId(projectId, secretDalVersion, dto),
|
||||
SECRET_DAL_TTL,
|
||||
cache
|
||||
);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get all secret" });
|
||||
@@ -542,11 +441,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
folderIds: string[];
|
||||
userId?: string;
|
||||
tx?: Knex;
|
||||
projectId: string;
|
||||
filters?: TFindSecretsByFolderIdsFilter;
|
||||
useCache?: boolean;
|
||||
}) => {
|
||||
const { folderIds, tx, filters, useCache, projectId } = dto;
|
||||
const { folderIds, tx, filters } = dto;
|
||||
let { userId } = dto;
|
||||
try {
|
||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||
@@ -555,26 +452,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
userId = undefined;
|
||||
}
|
||||
|
||||
const cachedSecretDalVersion = await keyStore.getItem(SecretDalCacheKeys.getSecretDalVersion(projectId));
|
||||
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||
if (useCache) {
|
||||
const cacheKey = SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto);
|
||||
const cachedSecrets = await keyStore.getItem(cacheKey);
|
||||
if (cachedSecrets) {
|
||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||
|
||||
const unsanitizedSecrets = JSON.parse(cachedSecrets) as BufferKeysToString<(typeof data)[number]>[];
|
||||
const sanitizedSecrets = unsanitizedSecrets.map((el) => {
|
||||
const encryptedValue = el.encryptedValue ? Buffer.from(el.encryptedValue, "base64") : null;
|
||||
const encryptedComment = el.encryptedComment ? Buffer.from(el.encryptedComment, "base64") : null;
|
||||
const createdAt = new Date(el.createdAt);
|
||||
const updatedAt = new Date(el.updatedAt);
|
||||
return { ...el, encryptedComment, encryptedValue, createdAt, updatedAt };
|
||||
});
|
||||
return sanitizedSecrets;
|
||||
}
|
||||
}
|
||||
|
||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
|
||||
.where((bd) => {
|
||||
@@ -700,22 +577,6 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
if (useCache) {
|
||||
const cachedSecrets = data.map((el) => {
|
||||
const encryptedValue = el.encryptedValue ? el.encryptedValue.toString("base64") : null;
|
||||
const encryptedComment = el.encryptedComment ? el.encryptedComment.toString("base64") : null;
|
||||
return { ...el, encryptedValue, encryptedComment };
|
||||
});
|
||||
const cache = JSON.stringify(cachedSecrets);
|
||||
|
||||
if (Buffer.byteLength(cache, "utf8") < MAX_SECRET_CACHE_BYTES) {
|
||||
await keyStore.setItemWithExpiry(
|
||||
SecretDalCacheKeys.findByFolderIds(projectId, secretDalVersion, dto),
|
||||
SECRET_DAL_TTL,
|
||||
cache
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import path from "node:path";
|
||||
|
||||
import RE2 from "re2";
|
||||
|
||||
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
@@ -13,9 +15,8 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
||||
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
||||
// akhilmhdh: JS regex with global save state in .test
|
||||
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([a-zA-Z0-9-_.]+)}/;
|
||||
const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`;
|
||||
const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING);
|
||||
|
||||
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
|
||||
|
||||
@@ -36,7 +37,14 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3;
|
||||
* // ]
|
||||
*/
|
||||
export const getAllSecretReferences = (maybeSecretReference: string) => {
|
||||
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
|
||||
const references = [];
|
||||
let match;
|
||||
|
||||
const regex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = regex.exec(maybeSecretReference)) !== null) {
|
||||
references.push(match[1]);
|
||||
}
|
||||
|
||||
const nestedReferences = references
|
||||
.filter((el) => el.includes("."))
|
||||
@@ -501,7 +509,7 @@ export const expandSecretReferencesFactory = ({
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return { value: "", tags: [] };
|
||||
const secrets = await secretDAL.findByFolderId({ folderId: folder.id, projectId, useCache: true });
|
||||
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
|
||||
|
||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -531,9 +539,17 @@ export const expandSecretReferencesFactory = ({
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
|
||||
const refs = value?.match(INTERPOLATION_SYNTAX_REG);
|
||||
|
||||
if (refs) {
|
||||
const matchRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
|
||||
const refs = [];
|
||||
let match;
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = matchRegex.exec(value || "")) !== null) {
|
||||
refs.push(match[0]);
|
||||
}
|
||||
|
||||
if (refs.length > 0) {
|
||||
for (const interpolationSyntax of refs) {
|
||||
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
|
||||
const entities = interpolationKey.trim().split(".");
|
||||
@@ -592,7 +608,7 @@ export const expandSecretReferencesFactory = ({
|
||||
trace
|
||||
};
|
||||
|
||||
const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue);
|
||||
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue);
|
||||
if (dto.shouldStackTrace) {
|
||||
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
|
||||
trace?.children.push(stackTraceNode);
|
||||
@@ -626,7 +642,7 @@ export const expandSecretReferencesFactory = ({
|
||||
}) => {
|
||||
if (!inputSecret.value) return inputSecret.value;
|
||||
|
||||
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
|
||||
const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value);
|
||||
if (!shouldExpand) return inputSecret.value;
|
||||
|
||||
const { expandedValue } = await recursivelyExpandSecret(inputSecret);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-app
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { diff, groupBy } from "@app/lib/fn";
|
||||
@@ -43,7 +44,12 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||
import {
|
||||
MAX_SECRET_CACHE_BYTES,
|
||||
SECRET_DAL_TTL,
|
||||
SecretServiceCacheKeys,
|
||||
TSecretV2BridgeDALFactory
|
||||
} from "./secret-v2-bridge-dal";
|
||||
import {
|
||||
buildHierarchy,
|
||||
expandSecretReferencesFactory,
|
||||
@@ -105,6 +111,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
>;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setExpiry" | "setItemWithExpiry" | "deleteItem">;
|
||||
};
|
||||
|
||||
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
|
||||
@@ -127,7 +134,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretApprovalRequestDAL,
|
||||
secretApprovalRequestSecretDAL,
|
||||
kmsService,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
keyStore
|
||||
}: TSecretV2BridgeServiceFactoryDep) => {
|
||||
const $validateSecretReferences = async (
|
||||
projectId: string,
|
||||
@@ -800,12 +808,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const groupedFolderMappings = groupBy(folderMappings, (folderMapping) => folderMapping.folderId);
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds({
|
||||
projectId,
|
||||
folderIds: folderMappings.map((folderMapping) => folderMapping.folderId),
|
||||
userId,
|
||||
tx: undefined,
|
||||
filters,
|
||||
useCache: true
|
||||
filters
|
||||
});
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
@@ -909,21 +915,22 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return decryptedSecrets;
|
||||
};
|
||||
|
||||
const getSecrets = async ({
|
||||
actorId,
|
||||
path,
|
||||
environment,
|
||||
projectId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
viewSecretValue,
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
recursive,
|
||||
expandSecretReferences: shouldExpandSecretReferences,
|
||||
throwOnMissingReadValuePermission = true,
|
||||
...params
|
||||
}: TGetSecretsDTO) => {
|
||||
const getSecrets = async (dto: TGetSecretsDTO) => {
|
||||
const {
|
||||
actorId,
|
||||
path,
|
||||
environment,
|
||||
projectId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
viewSecretValue,
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
recursive,
|
||||
expandSecretReferences: shouldExpandSecretReferences,
|
||||
throwOnMissingReadValuePermission = true,
|
||||
...params
|
||||
} = dto;
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
@@ -934,6 +941,42 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
|
||||
|
||||
const cachedSecretDalVersion = await keyStore.getItem(SecretServiceCacheKeys.getSecretDalVersion(projectId));
|
||||
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||
const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, {
|
||||
...dto,
|
||||
permissionRules: permission.rules
|
||||
});
|
||||
|
||||
const { decryptor: secretManagerDecryptor, encryptor: secretManagerEncryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
|
||||
if (encryptedCachedSecrets) {
|
||||
try {
|
||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
||||
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
|
||||
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
|
||||
secrets: typeof decryptedSecrets;
|
||||
imports: typeof importedSecrets;
|
||||
};
|
||||
return {
|
||||
secrets: secrets.map((el) => ({
|
||||
...el,
|
||||
createdAt: new Date(el.createdAt),
|
||||
updatedAt: new Date(el.updatedAt)
|
||||
})),
|
||||
imports
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(err, "Secret service layer cache miss");
|
||||
await keyStore.deleteItem(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
let paths: { folderId: string; path: string }[] = [];
|
||||
|
||||
if (recursive) {
|
||||
@@ -958,17 +1001,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const groupedPaths = groupBy(paths, (p) => p.folderId);
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds({
|
||||
projectId,
|
||||
folderIds: paths.map((p) => p.folderId),
|
||||
userId: actorId,
|
||||
tx: undefined,
|
||||
filters: params,
|
||||
useCache: true
|
||||
});
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
filters: params
|
||||
});
|
||||
|
||||
// scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets
|
||||
@@ -1086,15 +1122,19 @@ export const secretV2BridgeServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!includeImports) {
|
||||
return {
|
||||
secrets: decryptedSecrets
|
||||
};
|
||||
const payload = { secrets: decryptedSecrets, imports: [] };
|
||||
const encryptedUpdatedCachedSecrets = secretManagerEncryptor({
|
||||
plainText: Buffer.from(JSON.stringify(payload))
|
||||
}).cipherTextBlob;
|
||||
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
||||
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId));
|
||||
const allowedImports = secretImports.filter(({ isReplication }) => !isReplication);
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
projectId,
|
||||
viewSecretValue,
|
||||
secretImports: allowedImports,
|
||||
secretDAL,
|
||||
@@ -1129,10 +1169,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
imports: importedSecrets
|
||||
};
|
||||
const payload = { secrets: decryptedSecrets, imports: importedSecrets };
|
||||
const encryptedUpdatedCachedSecrets = secretManagerEncryptor({
|
||||
plainText: Buffer.from(JSON.stringify(payload))
|
||||
}).cipherTextBlob;
|
||||
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
||||
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const getSecretById = async ({ actorId, actor, actorOrgId, actorAuthMethod, secretId }: TGetASecretByIdDTO) => {
|
||||
@@ -1312,7 +1356,6 @@ export const secretV2BridgeServiceFactory = ({
|
||||
if (!secret && includeImports) {
|
||||
const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
projectId,
|
||||
secretImports,
|
||||
viewSecretValue,
|
||||
secretDAL,
|
||||
@@ -2729,7 +2772,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)])
|
||||
);
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id), projectId, useCache: true });
|
||||
const secrets = await secretDAL.findByFolderIds({ folderIds: folders.map((f) => f.id) });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import path from "path";
|
||||
import RE2 from "re2";
|
||||
|
||||
import {
|
||||
ActionProjectType,
|
||||
@@ -218,7 +219,9 @@ type TInterpolateSecretArg = {
|
||||
};
|
||||
|
||||
const MAX_SECRET_REFERENCE_DEPTH = 5;
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
||||
const INTERPOLATION_PATTERN_STRING = String.raw`\${([a-zA-Z0-9-_.]+)}`;
|
||||
const INTERPOLATION_TEST_REGEX = new RE2(INTERPOLATION_PATTERN_STRING);
|
||||
|
||||
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
||||
const secretCache: Record<string, Record<string, string>> = {};
|
||||
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
||||
@@ -273,9 +276,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
if (!value) return "";
|
||||
if (depth > MAX_SECRET_REFERENCE_DEPTH) return "";
|
||||
|
||||
const refs = value.match(INTERPOLATION_SYNTAX_REG);
|
||||
const refs = [];
|
||||
let match;
|
||||
const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = execRegex.exec(value)) !== null) {
|
||||
refs.push(match[0]);
|
||||
}
|
||||
|
||||
let expandedValue = value;
|
||||
if (refs) {
|
||||
if (refs.length > 0) {
|
||||
for (const interpolationSyntax of refs) {
|
||||
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
|
||||
const entities = interpolationKey.trim().split(".");
|
||||
@@ -284,7 +295,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
const [secretKey] = entities;
|
||||
// eslint-disable-next-line
|
||||
let referenceValue = await fetchSecret(environment, secretPath, secretKey);
|
||||
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
|
||||
if (INTERPOLATION_TEST_REGEX.test(referenceValue)) {
|
||||
// eslint-disable-next-line
|
||||
referenceValue = await recursivelyExpandSecret({
|
||||
environment,
|
||||
@@ -305,7 +316,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
|
||||
// eslint-disable-next-line
|
||||
let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
||||
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
|
||||
if (INTERPOLATION_TEST_REGEX.test(referenceValue)) {
|
||||
// eslint-disable-next-line
|
||||
referenceValue = await recursivelyExpandSecret({
|
||||
environment: secretReferenceEnvironment,
|
||||
@@ -332,7 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
||||
}) => {
|
||||
if (!inputSecret.value) return inputSecret.value;
|
||||
|
||||
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
|
||||
const shouldExpand = INTERPOLATION_TEST_REGEX.test(inputSecret.value);
|
||||
if (!shouldExpand) return inputSecret.value;
|
||||
|
||||
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
|
||||
@@ -451,7 +462,18 @@ export const fnSecretBlindIndexCheckV2 = async ({
|
||||
* // ]
|
||||
*/
|
||||
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
|
||||
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
|
||||
const matches = [];
|
||||
let match;
|
||||
|
||||
const execRegex = new RE2(INTERPOLATION_PATTERN_STRING, "g");
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = execRegex.exec(maybeSecretReference)) !== null) {
|
||||
matches.push(match);
|
||||
}
|
||||
|
||||
const references = matches.map((m) => m[1]);
|
||||
|
||||
return references
|
||||
.filter((el) => el.includes("."))
|
||||
.map((el) => {
|
||||
|
||||
@@ -367,7 +367,7 @@ export const secretQueueFactory = ({
|
||||
canExpandValue: () => true
|
||||
});
|
||||
// process secrets in current folder
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId, projectId: dto.projectId });
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId({ folderId: dto.folderId });
|
||||
|
||||
await Promise.allSettled(
|
||||
secrets.map(async (secret) => {
|
||||
@@ -397,7 +397,6 @@ export const secretQueueFactory = ({
|
||||
// if no imports then return secrets in the current folder
|
||||
if (!secretImports.length) return content;
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
projectId: dto.projectId,
|
||||
decryptor: dto.decryptor,
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
|
||||
@@ -44,6 +44,7 @@ export enum SmtpTemplates {
|
||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
||||
ProjectAccessRequest = "projectAccess.handlebars",
|
||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
|
||||
OrgAdminBreakglassAccess = "orgAdminBreakglassAccess.handlebars",
|
||||
ServiceTokenExpired = "serviceTokenExpired.handlebars"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Organization admin has bypassed SSO</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<p>The organization admin {{email}} has bypassed enforced SSO login.</p>
|
||||
<p><strong>Timestamp</strong>: {{timestamp}}</p>
|
||||
<p><strong>IP address</strong>: {{ip}}</p>
|
||||
<p><strong>User agent</strong>: {{userAgent}}</p>
|
||||
<p>If you'd like to disable Admin SSO Bypass, please visit <a href="{{siteUrl}}/organization/settings">Organization Settings</a> > Security.</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user