mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
Add ST V3 copy to clipboard, default to is active
This commit is contained in:
Generated
+706
-29
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.30.3",
|
||||
"aws-sdk": "^2.1364.0",
|
||||
"axios": "^1.3.5",
|
||||
@@ -29,12 +30,14 @@
|
||||
"helmet": "^5.1.1",
|
||||
"infisical-node": "^1.2.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsrp": "^0.2.4",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^7.4.1",
|
||||
"mysql2": "^3.6.2",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
@@ -44,6 +47,7 @@
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"pino": "^8.16.1",
|
||||
"pino-http": "^8.5.1",
|
||||
"pg": "^8.11.3",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.1",
|
||||
"query-string": "^7.1.3",
|
||||
@@ -98,11 +102,13 @@
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/jmespath": "^0.15.1",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.3",
|
||||
"@types/nodemailer": "^6.4.6",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/pg": "^8.10.7",
|
||||
"@types/picomatch": "^2.3.0",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/supertest": "^2.0.12",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
INTEGRATION_BITBUCKET_API_URL,
|
||||
INTEGRATION_CHECKLY_API_URL,
|
||||
INTEGRATION_GCP_SECRET_MANAGER,
|
||||
INTEGRATION_NORTHFLANK_API_URL,
|
||||
INTEGRATION_QOVERY_API_URL,
|
||||
@@ -344,6 +345,59 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of Checkly groups for a specific user
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getIntegrationAuthChecklyGroups = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { integrationAuthId },
|
||||
query: { accountId }
|
||||
} = await validateRequest(reqValidator.GetIntegrationAuthChecklyGroupsV1, req);
|
||||
|
||||
const { integrationAuth, accessToken } = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: new Types.ObjectId(integrationAuthId)
|
||||
});
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
integrationAuth.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
interface ChecklyGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
if (accountId && accountId !== "") {
|
||||
const { data }: { data: ChecklyGroup[] } = (
|
||||
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"X-Checkly-Account": accountId
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
groups: data.map((g: ChecklyGroup) => ({
|
||||
name: g.name,
|
||||
groupId: g.id,
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
groups: []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Qovery Orgs for a specific user
|
||||
* @param req
|
||||
|
||||
@@ -140,7 +140,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
query: { secretPath, environment, workspaceId }
|
||||
} = validatedData;
|
||||
const {
|
||||
query: { folderId, include_imports: includeImports }
|
||||
query: { include_imports: includeImports }
|
||||
} = validatedData;
|
||||
|
||||
// if the service token has single scope, it will get all secrets for that scope by default
|
||||
@@ -156,13 +156,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
workspaceId = serviceTokenDetails.workspace.toString();
|
||||
}
|
||||
|
||||
if (folderId && folderId !== "root") {
|
||||
const folder = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
|
||||
}
|
||||
|
||||
if (!environment || !workspaceId)
|
||||
throw BadRequestError({ message: "Missing environment or workspace id" });
|
||||
|
||||
@@ -177,7 +170,6 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId,
|
||||
secretPath,
|
||||
authData: req.authData
|
||||
});
|
||||
@@ -467,20 +459,13 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
export const getSecrets = async (req: Request, res: Response) => {
|
||||
const validatedData = await validateRequest(reqValidator.GetSecretsV3, req);
|
||||
const {
|
||||
query: { environment, workspaceId, include_imports: includeImports, folderId }
|
||||
query: { environment, workspaceId, include_imports: includeImports }
|
||||
} = validatedData;
|
||||
|
||||
let {
|
||||
query: { secretPath }
|
||||
} = validatedData;
|
||||
|
||||
if (folderId && folderId !== "root") {
|
||||
const folder = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folder) return res.send({ secrets: [] });
|
||||
|
||||
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
|
||||
}
|
||||
|
||||
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
|
||||
authData: req.authData,
|
||||
workspaceId,
|
||||
@@ -492,7 +477,6 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
const secrets = await SecretService.getSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId,
|
||||
secretPath,
|
||||
authData: req.authData
|
||||
});
|
||||
@@ -875,6 +859,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: createdSecrets
|
||||
});
|
||||
@@ -919,6 +911,14 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: updatedSecrets
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import * as cloudProductsController from "./cloudProductsController";
|
||||
import * as roleController from "./roleController";
|
||||
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
|
||||
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
|
||||
import * as secretRotationProviderController from "./secretRotationProviderController";
|
||||
import * as secretRotationController from "./secretRotationController";
|
||||
|
||||
export {
|
||||
secretController,
|
||||
@@ -23,5 +25,7 @@ export {
|
||||
cloudProductsController,
|
||||
roleController,
|
||||
secretApprovalPolicyController,
|
||||
secretApprovalRequestController
|
||||
secretApprovalRequestController,
|
||||
secretRotationProviderController,
|
||||
secretRotationController
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretRotation";
|
||||
import * as secretRotationService from "../../secretRotation/service";
|
||||
import {
|
||||
getUserProjectPermissions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const createSecretRotation = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
provider,
|
||||
customProvider,
|
||||
interval,
|
||||
outputs,
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId,
|
||||
inputs
|
||||
}
|
||||
} = await validateRequest(reqValidator.createSecretRotationV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotation = await secretRotationService.createSecretRotation({
|
||||
workspaceId,
|
||||
inputs,
|
||||
environment,
|
||||
secretPath,
|
||||
outputs,
|
||||
interval,
|
||||
customProvider,
|
||||
provider
|
||||
});
|
||||
|
||||
return res.send({ secretRotation });
|
||||
};
|
||||
|
||||
export const restartSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { id }
|
||||
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
|
||||
|
||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString());
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotation = await secretRotationService.restartSecretRotation({ id });
|
||||
return res.send({ secretRotation });
|
||||
};
|
||||
|
||||
export const deleteSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
|
||||
|
||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString());
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationService.deleteSecretRotation({ id });
|
||||
return res.send({ secretRotations });
|
||||
};
|
||||
|
||||
export const getSecretRotations = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretRotationV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId);
|
||||
return res.send({ secretRotations });
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Request, Response } from "express";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../validation/secretRotationProvider";
|
||||
import * as secretRotationProviderService from "../../secretRotation/service";
|
||||
import {
|
||||
getUserProjectPermissions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
export const getProviderTemplates = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const rotationProviderList = await secretRotationProviderService.getProviderTemplate({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
return res.send(rotationProviderList);
|
||||
};
|
||||
@@ -209,7 +209,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
const isActive = false;
|
||||
const isActive = true;
|
||||
const serviceTokenData = await new ServiceTokenDataV3({
|
||||
name,
|
||||
user,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export enum ActorType {
|
||||
USER = "user",
|
||||
SERVICE = "service",
|
||||
SERVICE_V3 = "service-v3"
|
||||
USER = "user",
|
||||
SERVICE = "service",
|
||||
SERVICE_V3 = "service-v3",
|
||||
Machine = "machine"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
ActorType,
|
||||
EventType
|
||||
} from "./enums";
|
||||
import {
|
||||
IServiceTokenV3Scope,
|
||||
IServiceTokenV3TrustedIp
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
import { ActorType, EventType } from "./enums";
|
||||
import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
@@ -28,14 +22,15 @@ export interface ServiceActor {
|
||||
}
|
||||
|
||||
export interface ServiceActorV3 {
|
||||
type: ActorType.SERVICE_V3;
|
||||
metadata: ServiceActorMetadata;
|
||||
type: ActorType.SERVICE_V3;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor
|
||||
| ServiceActorV3;
|
||||
export interface MachineActor {
|
||||
type: ActorType.Machine;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | ServiceActorV3 | MachineActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@@ -226,36 +221,36 @@ interface DeleteServiceTokenEvent {
|
||||
}
|
||||
|
||||
interface CreateServiceTokenV3Event {
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateServiceTokenV3Event {
|
||||
type: EventType.UPDATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<IServiceTokenV3Scope>;
|
||||
trustedIps?: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
}
|
||||
type: EventType.UPDATE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
scopes?: Array<IServiceTokenV3Scope>;
|
||||
trustedIps?: Array<IServiceTokenV3TrustedIp>;
|
||||
expiresAt?: Date;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenV3Event {
|
||||
type: EventType.DELETE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
expiresAt?: Date;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
type: EventType.DELETE_SERVICE_TOKEN_V3;
|
||||
metadata: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
expiresAt?: Date;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
@@ -427,15 +422,15 @@ interface UpdateUserRole {
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
deniedPermissions: {
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}[]
|
||||
}
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
deniedPermissions: {
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
interface SecretApprovalMerge {
|
||||
type: EventType.SECRET_APPROVAL_MERGED;
|
||||
|
||||
@@ -10,6 +10,8 @@ import secretScanning from "./secretScanning";
|
||||
import roles from "./role";
|
||||
import secretApprovalPolicy from "./secretApprovalPolicy";
|
||||
import secretApprovalRequest from "./secretApprovalRequest";
|
||||
import secretRotationProvider from "./secretRotationProvider";
|
||||
import secretRotation from "./secretRotation";
|
||||
|
||||
export {
|
||||
secret,
|
||||
@@ -23,5 +25,7 @@ export {
|
||||
secretScanning,
|
||||
roles,
|
||||
secretApprovalPolicy,
|
||||
secretApprovalRequest
|
||||
secretApprovalRequest,
|
||||
secretRotationProvider,
|
||||
secretRotation
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import express from "express";
|
||||
|
||||
import { AuthMode } from "../../../variables";
|
||||
import { requireAuth } from "../../../middleware";
|
||||
import { secretRotationController } from "../../controllers/v1";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretRotationController.createSecretRotation
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/restart",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretRotationController.restartSecretRotations
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretRotationController.getSecretRotations
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretRotationController.deleteSecretRotations
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,17 @@
|
||||
import express from "express";
|
||||
|
||||
import { AuthMode } from "../../../variables";
|
||||
import { requireAuth } from "../../../middleware";
|
||||
import { secretRotationProviderController } from "../../controllers/v1";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
"/:workspaceId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretRotationProviderController.getProviderTemplates
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
import { ISecretRotation } from "./types";
|
||||
|
||||
const secretRotationSchema = new Schema(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace"
|
||||
},
|
||||
provider: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
customProvider: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "SecretRotationProvider"
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
interval: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lastRotatedAt: {
|
||||
type: String
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ["success", "failed"]
|
||||
},
|
||||
statusMessage: {
|
||||
type: String
|
||||
},
|
||||
// encrypted data on input keys and secrets got
|
||||
encryptedData: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
encryptedDataIV: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
encryptedDataTag: {
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
select: false,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
select: false,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
outputs: [
|
||||
{
|
||||
key: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secret: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Secret"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretRotation = model<ISecretRotation>("SecretRotation", secretRotationSchema);
|
||||
@@ -0,0 +1,288 @@
|
||||
import Queue, { Job } from "bull";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../../config";
|
||||
import { BotService, EventService, TelemetryService } from "../../../services";
|
||||
import { SecretRotation } from "../models";
|
||||
import { rotationTemplates } from "../templates";
|
||||
import {
|
||||
ISecretRotationData,
|
||||
ISecretRotationEncData,
|
||||
ISecretRotationProviderTemplate,
|
||||
TProviderFunctionTypes
|
||||
} from "../types";
|
||||
import {
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptSymmetric128BitHexKeyUTF8
|
||||
} from "../../../utils/crypto";
|
||||
import { ISecret, Secret } from "../../../models";
|
||||
import { ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../../../variables";
|
||||
import { EESecretService } from "../../services";
|
||||
import { SecretVersion } from "../../models";
|
||||
import { eventPushSecrets } from "../../../events";
|
||||
import { logger } from "../../../utils/logging";
|
||||
|
||||
import {
|
||||
secretRotationPreSetFn,
|
||||
secretRotationRemoveFn,
|
||||
secretRotationSetFn,
|
||||
secretRotationTestFn
|
||||
} from "./queue.utils";
|
||||
|
||||
const secretRotationQueue = new Queue("secret-rotation-service", process.env.REDIS_URL as string);
|
||||
|
||||
secretRotationQueue.process(async (job: Job) => {
|
||||
logger.info(`secretRotationQueue.process: [rotationDocument=${job.data.rotationDocId}]`);
|
||||
const rotationStratDocId = job.data.rotationDocId;
|
||||
const secretRotation = await SecretRotation.findById(rotationStratDocId)
|
||||
.select("+encryptedData +encryptedDataTag +encryptedDataIV +keyEncoding")
|
||||
.populate<{
|
||||
outputs: [
|
||||
{
|
||||
key: string;
|
||||
secret: ISecret;
|
||||
}
|
||||
];
|
||||
}>("outputs.secret");
|
||||
|
||||
const infisicalRotationProvider = rotationTemplates.find(
|
||||
({ name }) => name === secretRotation?.provider
|
||||
);
|
||||
|
||||
try {
|
||||
if (!infisicalRotationProvider || !secretRotation)
|
||||
throw new Error("Failed to find rotation strategy");
|
||||
|
||||
if (secretRotation.outputs.some(({ secret }) => !secret))
|
||||
throw new Error("Secrets not found in dashboard");
|
||||
|
||||
const workspaceId = secretRotation.workspace;
|
||||
|
||||
// deep copy
|
||||
const provider = JSON.parse(
|
||||
JSON.stringify(infisicalRotationProvider)
|
||||
) as ISecretRotationProviderTemplate;
|
||||
|
||||
// decrypt user provided inputs for secret rotation
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
let decryptedData = "";
|
||||
if (rootEncryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||
// case: encoding scheme is base64
|
||||
decryptedData = client.decryptSymmetric(
|
||||
secretRotation.encryptedData,
|
||||
rootEncryptionKey,
|
||||
secretRotation.encryptedDataIV,
|
||||
secretRotation.encryptedDataTag
|
||||
);
|
||||
} else if (encryptionKey && secretRotation.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||
// case: encoding scheme is utf8
|
||||
decryptedData = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secretRotation.encryptedData,
|
||||
iv: secretRotation.encryptedDataIV,
|
||||
tag: secretRotation.encryptedDataTag,
|
||||
key: encryptionKey
|
||||
});
|
||||
}
|
||||
|
||||
const variables = JSON.parse(decryptedData) as ISecretRotationEncData;
|
||||
|
||||
// rotation set cycle
|
||||
const newCredential: ISecretRotationData = {
|
||||
inputs: variables.inputs,
|
||||
outputs: {},
|
||||
internal: {}
|
||||
};
|
||||
// special glue code for database
|
||||
if (provider.template.functions.set.type === TProviderFunctionTypes.DB) {
|
||||
const lastCred = variables.creds.at(-1);
|
||||
if (lastCred && variables.creds.length === 1) {
|
||||
newCredential.internal.username =
|
||||
lastCred.internal.username === variables.inputs.username1
|
||||
? variables.inputs.username2
|
||||
: variables.inputs.username1;
|
||||
} else {
|
||||
newCredential.internal.username = lastCred
|
||||
? lastCred.internal.username
|
||||
: variables.inputs.username1;
|
||||
}
|
||||
}
|
||||
if (provider.template.functions.set?.pre) {
|
||||
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
|
||||
}
|
||||
await secretRotationSetFn(provider.template.functions.set, newCredential);
|
||||
await secretRotationTestFn(provider.template.functions.test, newCredential);
|
||||
|
||||
if (variables.creds.length === 2) {
|
||||
const deleteCycleCred = variables.creds.pop();
|
||||
if (deleteCycleCred && provider.template.functions.remove) {
|
||||
const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred };
|
||||
await secretRotationRemoveFn(provider.template.functions.remove, deleteCycleVar);
|
||||
}
|
||||
}
|
||||
variables.creds.unshift({ outputs: newCredential.outputs, internal: newCredential.internal });
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(
|
||||
JSON.stringify(variables),
|
||||
rootEncryptionKey
|
||||
);
|
||||
|
||||
// save the rotation state
|
||||
await SecretRotation.findByIdAndUpdate(rotationStratDocId, {
|
||||
encryptedData: ciphertext,
|
||||
encryptedDataIV: iv,
|
||||
encryptedDataTag: tag,
|
||||
status: "success",
|
||||
statusMessage: "Rotated successfully",
|
||||
lastRotatedAt: new Date().toUTCString()
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
workspaceId: secretRotation.workspace
|
||||
});
|
||||
|
||||
const encryptedSecrets = secretRotation.outputs.map(({ key: outputKey, secret }) => ({
|
||||
secret,
|
||||
value: encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext:
|
||||
typeof newCredential.outputs[outputKey] === "object"
|
||||
? JSON.stringify(newCredential.outputs[outputKey])
|
||||
: String(newCredential.outputs[outputKey]),
|
||||
key
|
||||
})
|
||||
}));
|
||||
|
||||
// now save the secret do a bulk update
|
||||
// can't use the updateSecret function due to various parameter required issue
|
||||
// REFACTOR(akhilmhdh): secret module should be lot more flexible. Ability to update bulk or individually by blindIndex, by id etc
|
||||
await Secret.bulkWrite(
|
||||
encryptedSecrets.map(({ secret, value }) => ({
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: workspaceId,
|
||||
environment: secretRotation.environment,
|
||||
_id: secret._id,
|
||||
type: SECRET_SHARED
|
||||
},
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag
|
||||
}
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: encryptedSecrets.map(({ secret, value }) => {
|
||||
const {
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
folder,
|
||||
secretBlindIndex,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
skipMultilineEncoding,
|
||||
environment,
|
||||
algorithm,
|
||||
keyEncoding
|
||||
} = secret;
|
||||
|
||||
return new SecretVersion({
|
||||
secret: _id,
|
||||
version: version + 1,
|
||||
workspace: workspace,
|
||||
type,
|
||||
folder,
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: secretBlindIndex,
|
||||
secretKeyCiphertext: secretKeyCiphertext,
|
||||
secretKeyIV: secretKeyIV,
|
||||
secretKeyTag: secretKeyTag,
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag,
|
||||
algorithm,
|
||||
keyEncoding,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// akhilmhdh: @tony need to do something about this as its depend on authData which is not possibile in here
|
||||
// await EEAuditLogService.createAuditLog(
|
||||
// {actor:ActorType.Machine},
|
||||
// {
|
||||
// type: EventType.UPDATE_SECRETS,
|
||||
// metadata: {
|
||||
// environment,
|
||||
// secretPath,
|
||||
// secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
|
||||
// secretId: _id.toString(),
|
||||
// secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
// secretVersion: version + 1
|
||||
// }))
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// workspaceId
|
||||
// }
|
||||
// );
|
||||
|
||||
const folderId = encryptedSecrets?.[0]?.secret?.folder;
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment: secretRotation.environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: secretRotation.workspace,
|
||||
environment: secretRotation.environment,
|
||||
secretPath: secretRotation.secretPath
|
||||
})
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets rotated",
|
||||
properties: {
|
||||
numberOfSecrets: encryptedSecrets.length,
|
||||
environment: secretRotation.environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
await SecretRotation.findByIdAndUpdate(rotationStratDocId, {
|
||||
status: "failed",
|
||||
statusMessage: (err as Error).message,
|
||||
lastRotatedAt: new Date().toUTCString()
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
|
||||
export const startSecretRotationQueue = async (rotationDocId: string, interval: number) => {
|
||||
// when migration to bull mq just use the option immedite to trigger repeatable immediately
|
||||
secretRotationQueue.add({ rotationDocId }, { jobId: rotationDocId, removeOnComplete: true });
|
||||
return secretRotationQueue.add(
|
||||
{ rotationDocId },
|
||||
{ repeat: { every: daysToMillisecond(interval) }, jobId: rotationDocId }
|
||||
);
|
||||
};
|
||||
|
||||
export const removeSecretRotationQueue = async (rotationDocId: string, interval: number) => {
|
||||
return secretRotationQueue.removeRepeatable({ every: interval * 1000, jobId: rotationDocId });
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import axios from "axios";
|
||||
import jmespath from "jmespath";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { Client as PgClient } from "pg";
|
||||
import mysql from "mysql2";
|
||||
import {
|
||||
ISecretRotationData,
|
||||
TAssignOp,
|
||||
TDbProviderClients,
|
||||
TDbProviderFunction,
|
||||
TDirectAssignOp,
|
||||
THttpProviderFunction,
|
||||
TProviderFunction,
|
||||
TProviderFunctionTypes
|
||||
} from "../types";
|
||||
const REGEX = /\${([^}]+)}/g;
|
||||
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
|
||||
|
||||
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
||||
if (!data) return;
|
||||
|
||||
if (typeof data === "number") return data;
|
||||
|
||||
if (typeof data === "string") {
|
||||
return data.replace(REGEX, (_a, b) => getValue(b) as string);
|
||||
}
|
||||
|
||||
if (typeof data === "object" && Array.isArray(data)) {
|
||||
data.forEach((el, index) => {
|
||||
data[index] = interpolate(el, getValue);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof data === "object") {
|
||||
if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref);
|
||||
const temp = data as Record<string, unknown>; // for converting ts object to record type
|
||||
Object.keys(temp).forEach((key) => {
|
||||
temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue);
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getInterpolationValue = (variables: ISecretRotationData) => (key: string) => {
|
||||
if (key.includes("|")) {
|
||||
const [keyword, ...arg] = key.split("|").map((el) => el.trim());
|
||||
switch (keyword) {
|
||||
case "random": {
|
||||
return nanoId(parseInt(arg[0], 10));
|
||||
}
|
||||
default: {
|
||||
throw Error(`Interpolation key not found - ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const [type, keyName] = key.split(".").map((el) => el.trim());
|
||||
return variables[type as keyof ISecretRotationData][keyName];
|
||||
};
|
||||
|
||||
export const secretRotationHttpFn = async (
|
||||
func: THttpProviderFunction,
|
||||
variables: ISecretRotationData
|
||||
) => {
|
||||
// string interpolation
|
||||
const headers = interpolate(func.header, getInterpolationValue(variables));
|
||||
const url = interpolate(func.url, getInterpolationValue(variables));
|
||||
const body = interpolate(func.body, getInterpolationValue(variables));
|
||||
// axios will automatically throw error if req status is not between 2xx range
|
||||
return axios({ method: func.method, url, headers, data: body });
|
||||
};
|
||||
|
||||
export const secretRotationDbFn = async (
|
||||
func: TDbProviderFunction,
|
||||
variables: ISecretRotationData
|
||||
) => {
|
||||
const { type, client, pre, ...dbConnection } = func;
|
||||
const { username, password, host, database, port, query, ca } = interpolate(
|
||||
dbConnection,
|
||||
getInterpolationValue(variables)
|
||||
);
|
||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||
if (host === "localhost" || host === "127.0.0.1") throw new Error("Invalid db host");
|
||||
if (client === TDbProviderClients.Pg) {
|
||||
const pgClient = new PgClient({ user: username, password, host, database, port, ssl });
|
||||
await pgClient.connect();
|
||||
const res = await pgClient.query(query);
|
||||
await pgClient.end();
|
||||
return res.rows[0];
|
||||
} else if (client === TDbProviderClients.Sql) {
|
||||
const sqlClient = mysql.createPool({
|
||||
user: username,
|
||||
password,
|
||||
host,
|
||||
database,
|
||||
port,
|
||||
connectionLimit: 1,
|
||||
ssl
|
||||
});
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
sqlClient.query(query, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
sqlClient.end(function (err) {
|
||||
if (err) return reject(err);
|
||||
return resolve({});
|
||||
});
|
||||
});
|
||||
return (res as any)?.[0];
|
||||
}
|
||||
};
|
||||
|
||||
export const secretRotationPreSetFn = (
|
||||
op: Record<string, TDirectAssignOp>,
|
||||
variables: ISecretRotationData
|
||||
) => {
|
||||
const getValFn = getInterpolationValue(variables);
|
||||
Object.entries(op || {}).forEach(([key, assignFn]) => {
|
||||
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
|
||||
variables[type][keyName] = interpolate(assignFn.value, getValFn);
|
||||
});
|
||||
};
|
||||
|
||||
export const secretRotationSetFn = async (
|
||||
func: TProviderFunction,
|
||||
variables: ISecretRotationData
|
||||
) => {
|
||||
const getValFn = getInterpolationValue(variables);
|
||||
// http setter
|
||||
if (func.type === TProviderFunctionTypes.HTTP) {
|
||||
const res = await secretRotationHttpFn(func, variables);
|
||||
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
|
||||
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
|
||||
if (assignFn.assign === TAssignOp.JmesPath) {
|
||||
variables[type][keyName] = jmespath.search(res.data, assignFn.path);
|
||||
} else if (assignFn.value) {
|
||||
variables[type][keyName] = interpolate(assignFn.value, getValFn);
|
||||
}
|
||||
});
|
||||
// db setter
|
||||
} else if (func.type === TProviderFunctionTypes.DB) {
|
||||
const data = await secretRotationDbFn(func, variables);
|
||||
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
|
||||
const [type, keyName] = key.split(".") as [keyof ISecretRotationData, string];
|
||||
if (assignFn.assign === TAssignOp.JmesPath) {
|
||||
if (typeof data === "object") {
|
||||
variables[type][keyName] = jmespath.search(data, assignFn.path);
|
||||
}
|
||||
} else if (assignFn.value) {
|
||||
variables[type][keyName] = interpolate(assignFn.value, getValFn);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const secretRotationTestFn = async (
|
||||
func: TProviderFunction,
|
||||
variables: ISecretRotationData
|
||||
) => {
|
||||
if (func.type === TProviderFunctionTypes.HTTP) {
|
||||
await secretRotationHttpFn(func, variables);
|
||||
} else if (func.type === TProviderFunctionTypes.DB) {
|
||||
await secretRotationDbFn(func, variables);
|
||||
}
|
||||
};
|
||||
|
||||
export const secretRotationRemoveFn = async (
|
||||
func: TProviderFunction,
|
||||
variables: ISecretRotationData
|
||||
) => {
|
||||
if (!func) return;
|
||||
if (func.type === TProviderFunctionTypes.HTTP) {
|
||||
// string interpolation
|
||||
return await secretRotationHttpFn(func, variables);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import { ISecretRotationEncData, TCreateSecretRotation, TGetProviderTemplates } from "./types";
|
||||
import { rotationTemplates } from "./templates";
|
||||
import { SecretRotation } from "./models";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import Ajv from "ajv";
|
||||
import { removeSecretRotationQueue, startSecretRotationQueue } from "./queue/queue";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8
|
||||
} from "../../variables";
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||
|
||||
const ajv = new Ajv({ strict: false });
|
||||
|
||||
export const getProviderTemplate = async ({ workspaceId }: TGetProviderTemplates) => {
|
||||
return {
|
||||
custom: [],
|
||||
providers: rotationTemplates
|
||||
};
|
||||
};
|
||||
|
||||
export const createSecretRotation = async ({
|
||||
workspaceId,
|
||||
secretPath,
|
||||
environment,
|
||||
provider,
|
||||
interval,
|
||||
inputs,
|
||||
outputs
|
||||
}: TCreateSecretRotation) => {
|
||||
const rotationTemplate = rotationTemplates.find(({ name }) => name === provider);
|
||||
if (!rotationTemplate) throw BadRequestError({ message: "Provider not found" });
|
||||
|
||||
const formattedInputs: Record<string, unknown> = {};
|
||||
Object.entries(inputs).forEach(([key, value]) => {
|
||||
const type = rotationTemplate.template.inputs.properties[key].type;
|
||||
if (type === "string") {
|
||||
formattedInputs[key] = value;
|
||||
return;
|
||||
}
|
||||
if (type === "integer") {
|
||||
formattedInputs[key] = parseInt(value as string, 10);
|
||||
return;
|
||||
}
|
||||
formattedInputs[key] = JSON.parse(value as string);
|
||||
});
|
||||
// ensure input one follows the correct schema
|
||||
const valid = ajv.validate(rotationTemplate.template.inputs, formattedInputs);
|
||||
if (!valid) {
|
||||
throw BadRequestError({ message: ajv.errors?.[0].message });
|
||||
}
|
||||
|
||||
const encData: Partial<ISecretRotationEncData> = {
|
||||
inputs: formattedInputs,
|
||||
creds: []
|
||||
};
|
||||
|
||||
const secretRotation = new SecretRotation({
|
||||
workspace: workspaceId,
|
||||
provider,
|
||||
environment,
|
||||
secretPath,
|
||||
interval,
|
||||
outputs: Object.entries(outputs).map(([key, secret]) => ({ key, secret }))
|
||||
});
|
||||
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
if (rootEncryptionKey) {
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(
|
||||
JSON.stringify(encData),
|
||||
rootEncryptionKey
|
||||
);
|
||||
secretRotation.encryptedDataIV = iv;
|
||||
secretRotation.encryptedDataTag = tag;
|
||||
secretRotation.encryptedData = ciphertext;
|
||||
secretRotation.algorithm = ALGORITHM_AES_256_GCM;
|
||||
secretRotation.keyEncoding = ENCODING_SCHEME_BASE64;
|
||||
} else if (encryptionKey) {
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: JSON.stringify(encData),
|
||||
key: encryptionKey
|
||||
});
|
||||
secretRotation.encryptedDataIV = iv;
|
||||
secretRotation.encryptedDataTag = tag;
|
||||
secretRotation.encryptedData = ciphertext;
|
||||
secretRotation.algorithm = ALGORITHM_AES_256_GCM;
|
||||
secretRotation.keyEncoding = ENCODING_SCHEME_UTF8;
|
||||
}
|
||||
|
||||
await secretRotation.save();
|
||||
await startSecretRotationQueue(secretRotation._id.toString(), interval);
|
||||
|
||||
return secretRotation;
|
||||
};
|
||||
|
||||
export const deleteSecretRotation = async ({ id }: { id: string }) => {
|
||||
const doc = await SecretRotation.findByIdAndRemove(id);
|
||||
if (!doc) throw BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
await removeSecretRotationQueue(doc._id.toString(), doc.interval);
|
||||
return doc;
|
||||
};
|
||||
|
||||
export const restartSecretRotation = async ({ id }: { id: string }) => {
|
||||
const secretRotation = await SecretRotation.findById(id);
|
||||
if (!secretRotation) throw BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
await removeSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval);
|
||||
await startSecretRotationQueue(secretRotation._id.toString(), secretRotation.interval);
|
||||
|
||||
return secretRotation;
|
||||
};
|
||||
|
||||
export const getSecretRotationById = async ({ id }: { id: string }) => {
|
||||
const doc = await SecretRotation.findById(id);
|
||||
if (!doc) throw BadRequestError({ message: "Rotation not found" });
|
||||
return doc;
|
||||
};
|
||||
|
||||
export const getSecretRotationOfWorkspace = async (workspaceId: string) => {
|
||||
const secretRotations = await SecretRotation.find({
|
||||
workspace: workspaceId
|
||||
}).populate("outputs.secret");
|
||||
|
||||
return secretRotations;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ISecretRotationProviderTemplate } from "../types";
|
||||
import { MYSQL_TEMPLATE } from "./mysql";
|
||||
import { POSTGRES_TEMPLATE } from "./postgres";
|
||||
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
||||
|
||||
export const rotationTemplates: ISecretRotationProviderTemplate[] = [
|
||||
{
|
||||
name: "sendgrid",
|
||||
title: "Twilio Sendgrid",
|
||||
image: "sendgrid.png",
|
||||
description: "Rotate Twilio Sendgrid API keys",
|
||||
template: SENDGRID_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "postgres",
|
||||
title: "PostgreSQL",
|
||||
image: "postgres.png",
|
||||
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
||||
template: POSTGRES_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
title: "MySQL",
|
||||
image: "mysql.png",
|
||||
description: "Rotate MySQL@7/MariaDB user credentials",
|
||||
template: MYSQL_TEMPLATE
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,83 @@
|
||||
import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types";
|
||||
|
||||
export const MYSQL_TEMPLATE = {
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_username: { type: "string" as const },
|
||||
admin_password: { type: "string" as const },
|
||||
host: { type: "string" as const },
|
||||
database: { type: "string" as const },
|
||||
port: { type: "integer" as const, default: "3306" },
|
||||
username1: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user1",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
username2: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user2",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
|
||||
},
|
||||
required: [
|
||||
"admin_username",
|
||||
"admin_password",
|
||||
"host",
|
||||
"database",
|
||||
"username1",
|
||||
"username2",
|
||||
"port"
|
||||
],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
db_username: { type: "string" },
|
||||
db_password: { type: "string" }
|
||||
},
|
||||
internal: {
|
||||
rotated_password: { type: "string" },
|
||||
username: { type: "string" }
|
||||
},
|
||||
functions: {
|
||||
set: {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.Sql,
|
||||
username: "${inputs.admin_username}",
|
||||
password: "${inputs.admin_password}",
|
||||
host: "${inputs.host}",
|
||||
database: "${inputs.database}",
|
||||
port: "${inputs.port}",
|
||||
ca: "${inputs.ca}",
|
||||
query: "ALTER USER ${internal.username} IDENTIFIED BY '${internal.rotated_password}'",
|
||||
setter: {
|
||||
"outputs.db_username": {
|
||||
assign: TAssignOp.Direct as const,
|
||||
value: "${internal.username}"
|
||||
},
|
||||
"outputs.db_password": {
|
||||
assign: TAssignOp.Direct as const,
|
||||
value: "${internal.rotated_password}"
|
||||
}
|
||||
},
|
||||
pre: {
|
||||
"internal.rotated_password": {
|
||||
assign: TAssignOp.Direct as const,
|
||||
value: "${random | 32}"
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.Sql,
|
||||
username: "${internal.username}",
|
||||
password: "${internal.rotated_password}",
|
||||
host: "${inputs.host}",
|
||||
database: "${inputs.database}",
|
||||
port: "${inputs.port}",
|
||||
ca: "${inputs.ca}",
|
||||
query: "SELECT NOW()"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { TAssignOp, TDbProviderClients, TProviderFunctionTypes } from "../types";
|
||||
|
||||
export const POSTGRES_TEMPLATE = {
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_username: { type: "string" as const },
|
||||
admin_password: { type: "string" as const },
|
||||
host: { type: "string" as const },
|
||||
database: { type: "string" as const },
|
||||
port: { type: "integer" as const, default: "5432" },
|
||||
username1: {
|
||||
type: "string",
|
||||
default: "infisical-pg-user1",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
username2: {
|
||||
type: "string",
|
||||
default: "infisical-pg-user2",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
|
||||
},
|
||||
required: [
|
||||
"admin_username",
|
||||
"admin_password",
|
||||
"host",
|
||||
"database",
|
||||
"username1",
|
||||
"username2",
|
||||
"port"
|
||||
],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
db_username: { type: "string" },
|
||||
db_password: { type: "string" }
|
||||
},
|
||||
internal: {
|
||||
rotated_password: { type: "string" },
|
||||
username: { type: "string" }
|
||||
},
|
||||
functions: {
|
||||
set: {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.Pg,
|
||||
username: "${inputs.admin_username}",
|
||||
password: "${inputs.admin_password}",
|
||||
host: "${inputs.host}",
|
||||
database: "${inputs.database}",
|
||||
port: "${inputs.port}",
|
||||
ca: "${inputs.ca}",
|
||||
query: "ALTER USER ${internal.username} WITH PASSWORD '${internal.rotated_password}'",
|
||||
setter: {
|
||||
"outputs.db_username": {
|
||||
assign: TAssignOp.Direct as const,
|
||||
value: "${internal.username}"
|
||||
},
|
||||
"outputs.db_password": {
|
||||
assign: TAssignOp.Direct as const,
|
||||
value: "${internal.rotated_password}"
|
||||
}
|
||||
},
|
||||
pre: {
|
||||
"internal.rotated_password": {
|
||||
assign: TAssignOp.Direct as const,
|
||||
value: "${random | 32}"
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.Pg,
|
||||
username: "${internal.username}",
|
||||
password: "${internal.rotated_password}",
|
||||
host: "${inputs.host}",
|
||||
database: "${inputs.database}",
|
||||
port: "${inputs.port}",
|
||||
ca: "${inputs.ca}",
|
||||
query: "SELECT NOW()"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { TAssignOp, TProviderFunctionTypes } from "../types";
|
||||
|
||||
export const SENDGRID_TEMPLATE = {
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" },
|
||||
api_key_scopes: {
|
||||
type: "array",
|
||||
items: { type: "string" as const },
|
||||
desc: "Scopes for created tokens by rotation(Array)"
|
||||
}
|
||||
},
|
||||
required: ["admin_api_key", "api_key_scopes"],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
api_key: { type: "string" }
|
||||
},
|
||||
internal: {
|
||||
api_key_id: { type: "string" }
|
||||
},
|
||||
functions: {
|
||||
set: {
|
||||
type: TProviderFunctionTypes.HTTP as const,
|
||||
url: "https://api.sendgrid.com/v3/api_keys",
|
||||
method: "POST",
|
||||
header: {
|
||||
Authorization: "Bearer ${inputs.admin_api_key}"
|
||||
},
|
||||
body: {
|
||||
name: "infisical-${random | 16}",
|
||||
scopes: { ref: "inputs.api_key_scopes" }
|
||||
},
|
||||
setter: {
|
||||
"outputs.api_key": {
|
||||
assign: TAssignOp.JmesPath as const,
|
||||
path: "api_key"
|
||||
},
|
||||
"internal.api_key_id": {
|
||||
assign: TAssignOp.JmesPath as const,
|
||||
path: "api_key_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
remove: {
|
||||
type: TProviderFunctionTypes.HTTP as const,
|
||||
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
|
||||
header: {
|
||||
Authorization: "Bearer ${inputs.admin_api_key}"
|
||||
},
|
||||
method: "DELETE"
|
||||
},
|
||||
test: {
|
||||
type: TProviderFunctionTypes.HTTP as const,
|
||||
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
|
||||
header: {
|
||||
Authorization: "Bearer ${inputs.admin_api_key}"
|
||||
},
|
||||
method: "GET"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Document, Types } from "mongoose";
|
||||
|
||||
export interface ISecretRotation extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
interval: number;
|
||||
provider: string;
|
||||
customProvider: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
outputs: Array<{
|
||||
key: string;
|
||||
secret: Types.ObjectId;
|
||||
}>;
|
||||
status?: "success" | "failed";
|
||||
lastRotatedAt?: string;
|
||||
statusMessage?: string;
|
||||
encryptedData: string;
|
||||
encryptedDataIV: string;
|
||||
encryptedDataTag: string;
|
||||
algorithm: string;
|
||||
keyEncoding: string;
|
||||
}
|
||||
|
||||
export type ISecretRotationEncData = {
|
||||
inputs: Record<string, unknown>;
|
||||
creds: Array<{
|
||||
outputs: Record<string, unknown>;
|
||||
internal: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ISecretRotationData = {
|
||||
inputs: Record<string, unknown>;
|
||||
outputs: Record<string, unknown>;
|
||||
internal: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ISecretRotationProviderTemplate = {
|
||||
name: string;
|
||||
title: string;
|
||||
image?: string;
|
||||
description?: string;
|
||||
template: TProviderTemplate;
|
||||
};
|
||||
|
||||
export enum TProviderFunctionTypes {
|
||||
HTTP = "http",
|
||||
DB = "database"
|
||||
}
|
||||
|
||||
export enum TDbProviderClients {
|
||||
// postgres, cockroack db, amazon red shift
|
||||
Pg = "pg",
|
||||
// mysql and maria db
|
||||
Sql = "sql"
|
||||
}
|
||||
|
||||
export enum TAssignOp {
|
||||
Direct = "direct",
|
||||
JmesPath = "jmesopath"
|
||||
}
|
||||
|
||||
export type TJmesPathAssignOp = {
|
||||
assign: TAssignOp.JmesPath;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type TDirectAssignOp = {
|
||||
assign: TAssignOp.Direct;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp;
|
||||
|
||||
export type THttpProviderFunction = {
|
||||
type: TProviderFunctionTypes.HTTP;
|
||||
url: string;
|
||||
method: string;
|
||||
header?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
body?: Record<string, unknown>;
|
||||
setter?: Record<string, TAssignFunction>;
|
||||
pre?: Record<string, TDirectAssignOp>;
|
||||
};
|
||||
|
||||
export type TDbProviderFunction = {
|
||||
type: TProviderFunctionTypes.DB;
|
||||
client: TDbProviderClients;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
database: string;
|
||||
port: string;
|
||||
query: string;
|
||||
setter?: Record<string, TAssignFunction>;
|
||||
pre?: Record<string, TDirectAssignOp>;
|
||||
};
|
||||
|
||||
export type TProviderFunction = THttpProviderFunction | TDbProviderFunction;
|
||||
|
||||
export type TProviderTemplate = {
|
||||
inputs: {
|
||||
type: "object";
|
||||
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
|
||||
required?: string[];
|
||||
};
|
||||
outputs: Record<string, unknown>;
|
||||
functions: {
|
||||
set: TProviderFunction;
|
||||
remove?: TProviderFunction;
|
||||
test: TProviderFunction;
|
||||
};
|
||||
};
|
||||
|
||||
// function type args
|
||||
export type TGetProviderTemplates = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type TCreateSecretRotation = {
|
||||
provider: string;
|
||||
customProvider?: string;
|
||||
workspaceId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
interval: number;
|
||||
inputs: Record<string, unknown>;
|
||||
outputs: Record<string, string>;
|
||||
};
|
||||
@@ -38,6 +38,7 @@ interface FeatureSet {
|
||||
trial_end: number | null;
|
||||
has_used_trial: boolean;
|
||||
secretApproval: boolean;
|
||||
secretRotation: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,8 @@ class EELicenseService {
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: false
|
||||
secretApproval: false,
|
||||
secretRotation: true,
|
||||
}
|
||||
|
||||
public localFeatureSet: NodeCache;
|
||||
|
||||
@@ -50,7 +50,8 @@ export enum ProjectPermissionSub {
|
||||
Workspace = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval"
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -74,6 +75,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
@@ -92,6 +94,11 @@ const buildAdminPermission = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@@ -162,6 +169,7 @@ const buildMemberPermission = () => {
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
@@ -214,6 +222,7 @@ const buildViewerPermission = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createSecretRotationV1 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
secretPath: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
interval: z.number().min(1),
|
||||
provider: z.string().trim(),
|
||||
customProvider: z.string().trim().optional(),
|
||||
inputs: z.record(z.unknown()),
|
||||
outputs: z.record(z.string())
|
||||
})
|
||||
});
|
||||
|
||||
export const restartSecretRotationV1 = z.object({
|
||||
body: z.object({
|
||||
id: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const getSecretRotationV1 = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const removeSecretRotationV1 = z.object({
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const getSecretRotationProvidersV1 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string()
|
||||
})
|
||||
});
|
||||
@@ -553,14 +553,22 @@ export const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
folderId,
|
||||
secretPath = "/"
|
||||
}: GetSecretsParams) => {
|
||||
let secrets: ISecret[] = [];
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
|
||||
if (!folderId) {
|
||||
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
let folderId = "root";
|
||||
if (!folders && folderId !== "root") return [];
|
||||
// get folder from folder tree
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) return [];
|
||||
folderId = folder?.id;
|
||||
}
|
||||
|
||||
// get personal secrets first
|
||||
|
||||
@@ -27,8 +27,10 @@ import {
|
||||
users as eeUsersRouter,
|
||||
workspace as eeWorkspaceRouter,
|
||||
roles as v1RoleRouter,
|
||||
secretApprovalPolicy as v1SecretApprovalPolicy,
|
||||
secretApprovalRequest as v1SecretApprovalRequest,
|
||||
secretApprovalPolicy as v1SecretApprovalPolicyRouter,
|
||||
secretApprovalRequest as v1SecretApprovalRequestRouter,
|
||||
secretRotation as v1SecretRotation,
|
||||
secretRotationProvider as v1SecretRotationProviderRouter,
|
||||
secretScanning as v1SecretScanningRouter
|
||||
} from "./ee/routes/v1";
|
||||
import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3";
|
||||
@@ -194,6 +196,8 @@ const main = async () => {
|
||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||
app.use("/api/v3/api-key", v3apiKeyDataRouter); // new
|
||||
app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new
|
||||
app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter);
|
||||
app.use("/api/v1/secret-rotations", v1SecretRotation);
|
||||
|
||||
// v1 routes
|
||||
app.use("/api/v1/signup", v1SignupRouter);
|
||||
@@ -217,9 +221,9 @@ const main = async () => {
|
||||
app.use("/api/v1/webhooks", v1WebhooksRouter);
|
||||
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
|
||||
app.use("/api/v1/roles", v1RoleRouter);
|
||||
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
|
||||
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicyRouter);
|
||||
app.use("/api/v1/sso", v1SSORouter);
|
||||
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest);
|
||||
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequestRouter);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
|
||||
@@ -911,7 +911,7 @@ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of projects for the Checkly integration
|
||||
* Return list of accounts for the Checkly integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - api key for the Checkly API
|
||||
* @returns {Object[]} apps - Сheckly accounts
|
||||
|
||||
@@ -2104,7 +2104,7 @@ const syncSecretsSupabase = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Checkly app
|
||||
* Sync/push [secrets] to Checkly app/group
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
@@ -2121,94 +2121,154 @@ const syncSecretsCheckly = async ({
|
||||
accessToken: string;
|
||||
appendices?: { prefix: string; suffix: string };
|
||||
}) => {
|
||||
let getSecretsRes = (
|
||||
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
})
|
||||
).data.reduce(
|
||||
(obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret.value
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
getSecretsRes = Object.keys(getSecretsRes).reduce(
|
||||
(
|
||||
result: {
|
||||
[key: string]: string;
|
||||
},
|
||||
key
|
||||
) => {
|
||||
if (
|
||||
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
|
||||
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
|
||||
) {
|
||||
result[key] = getSecretsRes[key];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
if (integration.targetServiceId) {
|
||||
// sync secrets to checkly group envars
|
||||
|
||||
// add secrets
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in getSecretsRes)) {
|
||||
// case: secret does not exist in checkly
|
||||
// -> add secret
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
|
||||
{
|
||||
key,
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// case: secret exists in checkly
|
||||
// -> update/set secret
|
||||
|
||||
if (secrets[key] !== getSecretsRes[key]) {
|
||||
await standardRequest.put(
|
||||
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
|
||||
{
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const key of Object.keys(getSecretsRes)) {
|
||||
if (!(key in secrets)) {
|
||||
// delete secret
|
||||
await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, {
|
||||
let getGroupSecretsRes = (
|
||||
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
});
|
||||
})
|
||||
).data.environmentVariables.reduce(
|
||||
(obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret.value
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
getGroupSecretsRes = Object.keys(getGroupSecretsRes).reduce(
|
||||
(
|
||||
result: {
|
||||
[key: string]: string;
|
||||
},
|
||||
key
|
||||
) => {
|
||||
if (
|
||||
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
|
||||
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
|
||||
) {
|
||||
result[key] = getGroupSecretsRes[key];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const groupEnvironmentVariables = Object.keys(secrets).map(key => ({
|
||||
key,
|
||||
value: secrets[key].value
|
||||
}));
|
||||
|
||||
await standardRequest.put(
|
||||
`${INTEGRATION_CHECKLY_API_URL}/v1/check-groups/${integration.targetServiceId}`,
|
||||
{
|
||||
environmentVariables: groupEnvironmentVariables
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// sync secrets to checkly global envars
|
||||
|
||||
let getSecretsRes = (
|
||||
await standardRequest.get(`${INTEGRATION_CHECKLY_API_URL}/v1/variables`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
})
|
||||
).data.reduce(
|
||||
(obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret.value
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
getSecretsRes = Object.keys(getSecretsRes).reduce(
|
||||
(
|
||||
result: {
|
||||
[key: string]: string;
|
||||
},
|
||||
key
|
||||
) => {
|
||||
if (
|
||||
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
|
||||
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
|
||||
) {
|
||||
result[key] = getSecretsRes[key];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// add secrets
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in getSecretsRes)) {
|
||||
// case: secret does not exist in checkly
|
||||
// -> add secret
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
|
||||
{
|
||||
key,
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// case: secret exists in checkly
|
||||
// -> update/set secret
|
||||
|
||||
if (secrets[key] !== getSecretsRes[key]) {
|
||||
await standardRequest.put(
|
||||
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
|
||||
{
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const key of Object.keys(getSecretsRes)) {
|
||||
if (!(key in secrets)) {
|
||||
// delete secret
|
||||
await standardRequest.delete(`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"X-Checkly-Account": integration.appId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,39 +1,27 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IServiceTokenData,
|
||||
IServiceTokenDataV3,
|
||||
IUser,
|
||||
} from "../../models";
|
||||
import {
|
||||
ServiceActor,
|
||||
ServiceActorV3,
|
||||
UserActor,
|
||||
UserAgentType
|
||||
} from "../../ee/models";
|
||||
import { IServiceTokenData, IServiceTokenDataV3, IUser } from "../../models";
|
||||
import { ServiceActor, ServiceActorV3, UserActor, UserAgentType } from "../../ee/models";
|
||||
|
||||
interface BaseAuthData {
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
userAgentType: UserAgentType;
|
||||
tokenVersionId?: Types.ObjectId;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
userAgentType: UserAgentType;
|
||||
tokenVersionId?: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface UserAuthData extends BaseAuthData {
|
||||
actor: UserActor;
|
||||
authPayload: IUser;
|
||||
actor: UserActor;
|
||||
authPayload: IUser;
|
||||
}
|
||||
|
||||
export interface ServiceTokenV3AuthData extends BaseAuthData {
|
||||
actor: ServiceActorV3;
|
||||
authPayload: IServiceTokenDataV3;
|
||||
actor: ServiceActorV3;
|
||||
authPayload: IServiceTokenDataV3;
|
||||
}
|
||||
|
||||
export interface ServiceTokenAuthData extends BaseAuthData {
|
||||
actor: ServiceActor;
|
||||
authPayload: IServiceTokenData;
|
||||
actor: ServiceActor;
|
||||
authPayload: IServiceTokenData;
|
||||
}
|
||||
|
||||
export type AuthData =
|
||||
| UserAuthData
|
||||
| ServiceTokenV3AuthData
|
||||
| ServiceTokenAuthData;
|
||||
export type AuthData = UserAuthData | ServiceTokenV3AuthData | ServiceTokenAuthData;
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface CreateSecretParams {
|
||||
export interface GetSecretsParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretPath: string;
|
||||
authData: AuthData;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,14 @@ router.get(
|
||||
integrationAuthController.getIntegrationAuthVercelBranches
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:integrationAuthId/checkly/groups",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
integrationAuthController.getIntegrationAuthChecklyGroups
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:integrationAuthId/qovery/orgs",
|
||||
requireAuth({
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Redis, { Redis as TRedis } from "ioredis";
|
||||
import { logger } from "../utils/logging";
|
||||
|
||||
let redisClient: TRedis | null;
|
||||
|
||||
if (process.env.REDIS_URL) {
|
||||
redisClient = new Redis(process.env.REDIS_URL as string);
|
||||
} else {
|
||||
console.warn("Redis URL not set, skipping Redis initialization.");
|
||||
logger.warn("Redis URL not set, skipping Redis initialization.");
|
||||
redisClient = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
initializeGoogleStrategy,
|
||||
initializeSamlStrategy
|
||||
} from "../authn/passport";
|
||||
import { logger } from "../logging";
|
||||
|
||||
/**
|
||||
* Prepare Infisical upon startup. This includes tasks like:
|
||||
@@ -45,8 +46,7 @@ import {
|
||||
*/
|
||||
export const setup = async () => {
|
||||
if ((await getRedisUrl()) === undefined || (await getRedisUrl()) === "") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
logger.error(
|
||||
"WARNING: Redis is not yet configured. Infisical may not function as expected without it."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,15 @@ export const GetIntegrationAuthVercelBranchesV1 = z.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const GetIntegrationAuthChecklyGroupsV1 = z.object({
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
query: z.object({
|
||||
accountId: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const GetIntegrationAuthQoveryOrgsV1 = z.object({
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
|
||||
@@ -228,7 +228,6 @@ export const GetSecretsRawV3 = z.object({
|
||||
workspaceId: z.string().trim().optional(),
|
||||
environment: z.string().trim().optional(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
folderId: z.string().trim().optional(),
|
||||
include_imports: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
@@ -302,7 +301,6 @@ export const GetSecretsV3 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
folderId: z.string().trim().optional(),
|
||||
include_imports: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
|
||||
Reference in New Issue
Block a user