1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-27 09:40:45 +00:00

Compare commits

..

5 Commits

27 changed files with 980 additions and 778 deletions

@ -3,130 +3,24 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
import {
checkSecretsPermission,
containsGlobPatterns,
repackageSecretV3ToRaw
} from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
import { Permission } from "../../models/serviceTokenDataV3";
import { Folder, IServiceTokenData } from "../../models";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secrets";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { ActorType } from "../../ee/models";
import { UnauthorizedRequestError } from "../../utils/errors";
import { AuthData } from "../../interfaces/middleware";
import { ProjectPermissionActions } from "../../ee/services/ProjectRoleService";
import {
generateSecretApprovalRequest,
getSecretPolicyOfBoard
} from "../../ee/services/SecretApprovalService";
import { CommitType } from "../../ee/models/secretApprovalRequest";
import { IRole } from "../../ee/models/role";
const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
),
membership
};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return {
authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
})
};
}
default: {
throw UnauthorizedRequestError();
}
}
};
/**
* Return secrets for workspace with id [workspaceId] and environment
@ -253,21 +147,21 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
repackageSecretV3ToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
secrets: el.secrets.map((secret) => repackageSecretV3ToRaw({ secret, key }))
}))
});
}
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
const rep = repackageSecretV3ToRaw({
secret,
key
});
@ -376,7 +270,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret,
key
})
@ -522,19 +416,11 @@ export const createSecretRaw = async (req: Request, res: Response) => {
skipMultilineEncoding
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret: secretWithoutBlindIndex,
key
})
@ -651,16 +537,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
skipMultilineEncoding
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret,
key
})
@ -773,7 +651,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret: repackageSecretV3ToRaw({
secret,
key
})
@ -960,14 +838,6 @@ export const createSecret = async (req: Request, res: Response) => {
skipMultilineEncoding
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
@ -1073,14 +943,6 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretKeyIV
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secret
});
@ -1137,14 +999,6 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secretPath
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return res.status(200).send({
secret
});
@ -1189,14 +1043,6 @@ 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
});
@ -1241,14 +1087,6 @@ 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
});
@ -1293,14 +1131,6 @@ export const deleteSecretByNameBatch = 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: deletedSecrets
});

@ -0,0 +1,42 @@
import { Request, Response } from "express";
// import { validateRequest } from "../../helpers/validation";
// import * as reqValidator from "../../validation/environments";
/**
* Create environment with name [environmentName] and slug [environmentSlug]
* in project with id [projectId]
* @param req
* @param res
*/
export const createEnvironment = async (req: Request, res: Response) => {
// TODO
}
/**
* Return list of environments in project with id [projectId]
* @param req
* @param res
*/
export const getEnvironments = async (req: Request, res: Response) => {
// TODO
}
/**
* Delete environment with name [environmentName] and slug [environmentSlug]
* in project with id [projectId]
* @param req
* @param res
*/
export const updateEnvironment = async (req: Request, res: Response) => {
// TODO
}
/**
* Delete environment with name [environmentName] and slug [environmentSlug]
* in project with id [projectId]
* @param req
* @param res
*/
export const deleteEnvironment = async (req: Request, res: Response) => {
// TODO
}

@ -0,0 +1,87 @@
import { Request, Response } from "express";
// import { Types } from "mongoose";
// import { FolderVersion } from "../../ee/models";
// import { Folder } from "../../models";
// import { appendFolder } from "../../services/FolderService";
// import { validateRequest } from "../../helpers/validation";
// import * as reqValidator from "../../validation/folders";
// import { ResourceNotFoundError } from "../../utils/errors";
/**
*
* Return list of folders in project with id [projectId]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
// TODO
}
/**
*
* Create folder with name [folderName] in project with id [projectId]
* under path [path]
* @param req
* @param res
* @returns
*/
export const createFolder = async (req: Request, res: Response) => {
// TODO
// const {
// body: {
// folderName,
// projectId,
// environmentSlug,
// path
// }
// } = await validateRequest(reqValidator.CreateFolderV2, req);
// // get folder
// const folders = await Folder.findOne({
// workspace: new Types.ObjectId(projectId),
// environment: environmentSlug
// });
// if (!folders) throw ResourceNotFoundError();
// const { parent, child: folder, hasCreated } = appendFolder(folders.nodes, { folderName, directory: path });
// if (!hasCreated) return res.json({ folder });
// await Folder.findByIdAndUpdate(folders._id, folders);
// await new FolderVersion({
// project: new Types.ObjectId(projectId),
// nodes: parent
// }).save();
// return res.status(200).send({
// folder
// });
}
/**
*
* Update folder with name [folderName] in project with id [projectId]
* under path [path]
* @param req
* @param res
* @returns
*/
export const updateFolder = async (req: Request, res: Response) => {
// TODO
}
/**
* Delete folder with name [folderName] in project with id [projectId]
* under path [path]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
// TODO
}

@ -0,0 +1,9 @@
import * as secretsController from "./secretsController";
import * as environmentsController from "./environmentsController";
import * as foldersController from "./foldersController";
export {
secretsController,
environmentsController,
foldersController
}

@ -0,0 +1,353 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { BotService, SecretService } from "../../services";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { ProjectPermissionActions } from "../../ee/services/ProjectRoleService";
import { checkSecretsPermission, packageSecretV4 } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secrets";
import { getFolderIdFromServiceToken } from "../../services/FolderService";
/**
* Get secrets in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const {
query: {
projectId,
environmentSlug,
path,
includeImports
}
} = await validateRequest(reqValidator.GetSecretsV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Read
});
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
secretPath: path,
authData: req.authData
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
let packagedSecrets = secrets.map((secret) => packageSecretV4({
secret,
key
}));
const folderId = await getFolderIdFromServiceToken(projectId, environmentSlug, path);
if (includeImports) {
const importGroups = await getAllImportedSecrets(
projectId,
environmentSlug,
folderId,
() => true
);
importGroups.forEach((importGroup) => {
packagedSecrets = packagedSecrets.concat(
importGroup.secrets.map((secret) => packageSecretV4({ secret, key }))
);
});
}
return res.status(200).send({
secrets: packagedSecrets
});
}
/**
* Get secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const getSecret = async (req: Request, res: Response) => {
const {
params: {
secretName
},
query: {
projectId,
environmentSlug,
path,
type,
includeImports
}
} = await validateRequest(reqValidator.GetSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Read
});
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
secretPath: path,
authData: req.authData,
include_imports: includeImports
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}
/**
* Create secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const createSecret = async (req: Request, res: Response) =>{
const {
params: {
secretName
},
body: {
projectId,
environmentSlug,
path,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Create
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretName,
key
});
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath: path,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}
/**
* Create secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const {
params: {
secretName
},
body: {
projectId,
environmentSlug,
path,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.UpdateSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Edit
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
let secretValueCiphertext, secretValueIV, secretValueTag;
if (secretValue) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
secretValueCiphertext = ciphertext;
secretValueIV = iv;
secretValueTag = tag;
}
let secretCommentCiphertext, secretCommentIV, secretCommentTag;
if (secretComment) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
secretCommentCiphertext = ciphertext;
secretCommentIV = iv;
secretCommentTag = tag;
}
const secret = await SecretService.updateSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath: path,
skipMultilineEncoding
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}
/**
* Delete secret named [secretName] in project with id
* [projectId]under path [path]
* @param req
* @param res
* @returns
*/
export const deleteSecret = async (req: Request, res: Response) => {
const {
params: {
secretName
},
body: {
projectId,
environmentSlug,
path,
type
}
} = await validateRequest(reqValidator.DeleteSecretV4, req);
await checkSecretsPermission({
authData: req.authData,
workspaceId: projectId,
environment: environmentSlug,
secretPath: path,
secretAction: ProjectPermissionActions.Delete
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(projectId)
});
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId: new Types.ObjectId(projectId),
environment: environmentSlug,
type,
authData: req.authData,
secretPath: path
});
const packagedSecret = packageSecretV4({
secret,
key
});
return res.status(200).send({
secret: packagedSecret
});
}

@ -1,5 +1,5 @@
import { Types } from "mongoose";
import { EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS } from "../variables";
import { EVENT_PUSH_SECRETS } from "../variables";
interface PushSecret {
ciphertextKey: string;
@ -37,18 +37,4 @@ const eventPushSecrets = ({
};
};
/**
* Return event for pulling secrets
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to pull secrets from
* @returns
*/
const eventPullSecrets = ({ workspaceId }: { workspaceId: string }) => {
return {
name: EVENT_PULL_SECRETS,
workspaceId,
payload: {}
};
};
export { eventPushSecrets };

@ -11,6 +11,7 @@ import {
} from "../interfaces/services/SecretService";
import {
Folder,
IMembership,
ISecret,
IServiceTokenData,
IServiceTokenDataV3,
@ -20,7 +21,7 @@ import {
TFolderRootSchema
} from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models";
import { ActorType, EventType, IRole, SecretVersion } from "../ee/models";
import {
BadRequestError,
InternalServerError,
@ -33,6 +34,8 @@ import {
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8,
K8_USER_AGENT_NAME,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL,
SECRET_SHARED
} from "../variables";
@ -42,7 +45,8 @@ import {
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "../utils/crypto";
import { TelemetryService } from "../services";
import { EventService, TelemetryService } from "../services";
import { eventPushSecrets } from "../events";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EEAuditLogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadUserObj } from "../utils/authn/helpers";
@ -50,6 +54,112 @@ import { getFolderByPath, getFolderIdFromServiceToken } from "../services/Folder
import picomatch from "picomatch";
import path from "path";
import { getAnImportedSecret } from "../services/SecretImportService";
import { AuthData } from "../interfaces/middleware";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
} from "../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../validation";
export const checkSecretsPermission = async ({
authData,
workspaceId,
environment,
secretPath,
secretAction
}: {
authData: AuthData;
workspaceId: string;
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
),
membership
};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return {
authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
})
};
}
default: {
throw UnauthorizedRequestError();
}
}
};
/**
* Validate scope for service token v3
@ -119,13 +229,12 @@ const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
/**
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
*
* Precondition: the workspace for secret [secret] must have E2EE disabled
* @param {ISecret} secret - secret to repackage to raw
* @param {String} key - symmetric key to use to decrypt secret
* @returns
*/
export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
export const repackageSecretV3ToRaw = ({ secret, key }: { secret: ISecret; key: string }) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
@ -160,7 +269,8 @@ export const repackageSecretToRaw = ({ secret, key }: { secret: ISecret; key: st
user: secret.user,
secretKey,
secretValue,
secretComment
secretComment,
skipMultilineEncoding: secret.skipMultilineEncoding
};
};
@ -517,6 +627,14 @@ export const createSecretHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return secret;
};
@ -755,7 +873,7 @@ export const updateSecretHelper = async ({
secretValueIV,
secretValueTag,
secretPath,
tags,
tags, // maybe this can accept just a secretComment?
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
@ -850,6 +968,9 @@ export const updateSecretHelper = async ({
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
@ -877,9 +998,12 @@ export const updateSecretHelper = async ({
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
@ -933,6 +1057,14 @@ export const updateSecretHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return secret;
};
@ -1060,6 +1192,14 @@ export const deleteSecretHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return {
secrets,
secret
@ -1407,6 +1547,14 @@ export const createSecretBatchHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return newlyCreatedSecrets;
};
@ -1602,6 +1750,14 @@ export const updateSecretBatchHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return;
};
@ -1725,7 +1881,54 @@ export const deleteSecretBatchHelper = async ({
});
}
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath
})
});
return {
secrets: deletedSecrets
};
};
// --- v4/secrets helpers
export const packageSecretV4 = ({
secret,
key
}: {
secret: ISecret;
key: string;
}) => {
const {
_id,
version,
workspace,
type,
environment,
user,
secretKey,
secretValue,
secretComment,
skipMultilineEncoding
} = repackageSecretV3ToRaw({
secret,
key
});
return ({
_id,
version,
projectId: workspace,
environmentSlug: environment,
type,
user,
secretName: secretKey,
secretValue,
secretComment,
skipMultilineEncoding
});
}

@ -77,6 +77,9 @@ import {
users as v3UsersRouter,
workspaces as v3WorkspacesRouter
} from "./routes/v3";
import {
secrets as v4SecretsRouter
} from "./routes/v4";
import { healthCheck } from "./routes/status";
// import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
@ -252,6 +255,9 @@ const main = async () => {
app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
app.use("/api/v3/us", v3UsersRouter);
// v4 routes (user-facing)
app.use("/api/v4/secrets", v4SecretsRouter);
// api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));

@ -51,9 +51,9 @@ export interface UpdateSecretParams {
environment: string;
type: "shared" | "personal";
authData: AuthData;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueCiphertext?: string;
secretValueIV?: string;
secretValueTag?: string;
secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;

@ -120,6 +120,7 @@ const secretSchema = new Schema<ISecret>(
},
skipMultilineEncoding: {
type: Boolean,
default: false,
required: false
},
algorithm: {

@ -0,0 +1,39 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { environmentsController } from "../../controllers/v4";
// import { AuthMode } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.createEnvironment
);
router.get(
"/",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.getEnvironments
);
router.patch(
"/:environmentSlug",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.updateEnvironment
);
router.delete(
"/:environmentSlug",
requireAuth({
acceptedAuthModes: []
}),
environmentsController.deleteEnvironment
)
export default router;

@ -0,0 +1,39 @@
import express from "express";
const router = express.Router();
import { requireAuth} from "../../middleware";
import { foldersController } from "../../controllers/v4";
// import { AuthMode } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: []
}),
foldersController.createFolder
);
router.get(
"/",
requireAuth({
acceptedAuthModes: []
}),
foldersController.getFolders
);
router.patch(
":/folderName",
requireAuth({
acceptedAuthModes: []
}),
foldersController.updateFolder
);
router.delete(
"/:folderName",
requireAuth({
acceptedAuthModes: []
}),
foldersController.deleteFolder
);
export default router;

@ -0,0 +1,5 @@
import secrets from "./secrets";
export {
secrets
}

@ -0,0 +1,46 @@
import express from "express";
const router = express.Router();
import { requireAuth} from "../../middleware";
import { secretsController } from "../../controllers/v4";
router.get(
"/",
requireAuth({
acceptedAuthModes: []
}),
secretsController.getSecrets
);
router.get(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.getSecret
);
router.post(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.createSecret
);
router.patch(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.updateSecret
);
router.delete(
"/:secretName",
requireAuth({
acceptedAuthModes: []
}),
secretsController.deleteSecret
);
export default router;

@ -47,3 +47,39 @@ export const ReorderWorkspaceEnvironmentsV2 = z.object({
otherEnvironmentName: z.string().trim()
})
});
export const CreateEnvironmentV4 = z.object({
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
environmentName: z.string().trim()
})
});
export const GetEnvironmentsV4 = z.object({
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
environmentName: z.string().trim()
})
});
export const UpdateEnvironmentV4 = z.object({
params: z.object({
environmentSlug: z.string().trim(),
}),
body: z.object({
projectId: z.string().trim(),
environmentName: z.string().trim().optional(),
newEnvironmentSlug: z.string().trim().optional()
})
});
export const DeleteEnvironmentV4 = z.object({
params: z.object({
environmentSlug: z.string().trim()
}),
body: z.object({
projectId: z.string().trim()
})
});

@ -39,3 +39,12 @@ export const GetFoldersV1 = z.object({
directory: z.string().trim().default("/")
})
});
export const CreateFolderV2 = z.object({
body: z.object({
folderName: z.string().trim(),
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/")
})
});

@ -449,3 +449,80 @@ export const DeleteSecretByNameBatchV3 = z.object({
.min(1)
})
});
export const GetSecretsV4 = z.object({
query: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
includeImports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
})
});
export const GetSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
query: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_PERSONAL),
includeImports: z
.enum(["true", "false"])
.default("true")
.transform((value) => value === "true")
})
});
export const CreateSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
secretComment: z.string().trim().optional().default(""),
skipMultilineEncoding: z.boolean().optional().default(false),
})
});
export const UpdateSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional(),
secretComment: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional()
})
});
export const DeleteSecretV4 = z.object({
params: z.object({
secretName: z.string().trim()
}),
body: z.object({
projectId: z.string().trim(),
environmentSlug: z.string().trim(),
path: z.string().trim().default("/"),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED)
})
});

@ -1,17 +0,0 @@
infisical:
address: "http://localhost:8080"
auth:
type: "token"
config:
token-path: "./role-id"
sinks:
- type: "file"
config:
path: "/Users/maidulislam/Desktop/test/infisical-token"
- type: "file"
config:
path: "access-token"
- type: "file"
config:
path: "maiduls-access-token"
templates:

@ -3,7 +3,6 @@ package api
import (
"fmt"
"net/http"
"strings"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/go-resty/resty/v2"
@ -360,50 +359,3 @@ func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceToken
return createServiceTokenResponse, nil
}
func CallServiceTokenV3Refresh(httpClient *resty.Client, request ServiceTokenV3RefreshTokenRequest) (ServiceTokenV3RefreshTokenResponse, error) {
var serviceTokenV3RefreshTokenResponse ServiceTokenV3RefreshTokenResponse
response, err := httpClient.
R().
SetResult(&serviceTokenV3RefreshTokenResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v3/service-token/me/token", config.INFISICAL_URL))
if err != nil {
return ServiceTokenV3RefreshTokenResponse{}, fmt.Errorf("CallServiceTokenV3Refresh: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return ServiceTokenV3RefreshTokenResponse{}, fmt.Errorf("CallServiceTokenV3Refresh: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return serviceTokenV3RefreshTokenResponse, nil
}
func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) {
var getRawSecretsV3Response GetRawSecretsV3Response
response, err := httpClient.
R().
SetResult(&getRawSecretsV3Response).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
SetQueryParam("workspaceId", request.WorkspaceId).
SetQueryParam("environment", request.Environment).
SetQueryParam("include_imports", "false").
Get(fmt.Sprintf("%v/v3/secrets/raw", config.INFISICAL_URL))
if err != nil {
return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unable to complete api request [err=%w]", err)
}
if response.IsError() && strings.Contains(response.String(), "Failed to find bot key") {
return GetRawSecretsV3Response{}, fmt.Errorf("project with id %s is a legacy project type, please navigate to project settings and disable end to end encryption then try again", request.WorkspaceId)
}
if response.IsError() {
return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode())
}
return getRawSecretsV3Response, nil
}

@ -421,34 +421,3 @@ type CreateServiceTokenResponse struct {
ServiceToken string `json:"serviceToken"`
ServiceTokenData ServiceTokenData `json:"serviceTokenData"`
}
type ServiceTokenV3RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token"`
}
type ServiceTokenV3RefreshTokenResponse struct {
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
type GetRawSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
}
type GetRawSecretsV3Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKey string `json:"secretKey"`
SecretValue string `json:"secretValue"`
SecretComment string `json:"secretComment"`
} `json:"secrets"`
Imports []any `json:"imports"`
}

@ -1,327 +0,0 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/signal"
"strings"
"syscall"
"text/template"
"time"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/spf13/cobra"
)
const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
type Config struct {
Infisical InfisicalConfig `yaml:"infisical"`
Auth AuthConfig `yaml:"auth"`
Sinks []Sink `yaml:"sinks"`
Templates []Template `yaml:"templates"`
}
type InfisicalConfig struct {
Address string `yaml:"address"`
}
type AuthConfig struct {
Type string `yaml:"type"`
Config interface{} `yaml:"config"`
}
type TokenAuthConfig struct {
TokenPath string `yaml:"token-path"`
}
type OAuthConfig struct {
ClientID string `yaml:"client-id"`
ClientSecret string `yaml:"client-secret"`
}
type Sink struct {
Type string `yaml:"type"`
Config SinkDetails `yaml:"config"`
}
type SinkDetails struct {
Path string `yaml:"path"`
}
type Template struct {
SourcePath string `yaml:"source-path"`
DestinationPath string `yaml:"destination-path"`
}
func ReadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
func FileExists(filepath string) bool {
info, err := os.Stat(filepath)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// WriteToFile writes data to the specified file path.
func WriteBytesToFile(data *bytes.Buffer, outputPath string) error {
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer outputFile.Close()
_, err = outputFile.Write(data.Bytes())
return err
}
func appendAPIEndpoint(address string) string {
// Ensure the address does not already end with "/api"
if strings.HasSuffix(address, "/api") {
return address
}
// Check if the address ends with a slash and append accordingly
if address[len(address)-1] == '/' {
return address + "api"
}
return address + "/api"
}
func ParseAgentConfig(filePath string) (*Config, error) {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}
var rawConfig struct {
Infisical InfisicalConfig `yaml:"infisical"`
Auth struct {
Type string `yaml:"type"`
Config map[string]interface{} `yaml:"config"`
} `yaml:"auth"`
Sinks []Sink `yaml:"sinks"`
Templates []Template `yaml:"templates"`
}
if err := yaml.Unmarshal(data, &rawConfig); err != nil {
return nil, err
}
// Set defaults
if rawConfig.Infisical.Address == "" {
rawConfig.Infisical.Address = DEFAULT_INFISICAL_CLOUD_URL
}
config.INFISICAL_URL = appendAPIEndpoint(rawConfig.Infisical.Address)
log.Info().Msgf("Infisical instance address set to %s", rawConfig.Infisical.Address)
config := &Config{
Infisical: rawConfig.Infisical,
Auth: AuthConfig{
Type: rawConfig.Auth.Type,
},
Sinks: rawConfig.Sinks,
Templates: rawConfig.Templates,
}
// Marshal and then unmarshal the config based on the type
configBytes, err := yaml.Marshal(rawConfig.Auth.Config)
if err != nil {
return nil, err
}
switch rawConfig.Auth.Type {
case "token":
var tokenConfig TokenAuthConfig
if err := yaml.Unmarshal(configBytes, &tokenConfig); err != nil {
return nil, err
}
config.Auth.Config = tokenConfig
case "oauth": // aws, gcp, k8s service account, etc
var oauthConfig OAuthConfig
if err := yaml.Unmarshal(configBytes, &oauthConfig); err != nil {
return nil, err
}
config.Auth.Config = oauthConfig
default:
return nil, fmt.Errorf("unknown auth type: %s", rawConfig.Auth.Type)
}
return config, nil
}
func secretTemplateFunction(accessToken string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
secrets, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false)
if err != nil {
return nil, err
}
return secrets, nil
}
}
func ProcessTemplate(templatePath string, data interface{}, accessToken string) (*bytes.Buffer, error) {
// custom template function to fetch secrets from Infisical
secretFunction := secretTemplateFunction(accessToken)
funcs := template.FuncMap{
"secret": secretFunction,
}
tmpl, err := template.New(templatePath).Funcs(funcs).ParseFiles(templatePath)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, err
}
return &buf, nil
}
func refreshTokenAndProcessTemplate(refreshToken string, config *Config, errChan chan error) {
for {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second)
tokenResponse, err := api.CallServiceTokenV3Refresh(httpClient, api.ServiceTokenV3RefreshTokenRequest{RefreshToken: refreshToken})
if err != nil {
errChan <- fmt.Errorf("unable to complete renewal because [%s]", err)
}
for _, sinkFile := range config.Sinks {
if sinkFile.Type == "file" {
err = ioutil.WriteFile(sinkFile.Config.Path, []byte(tokenResponse.AccessToken), 0644)
if err != nil {
errChan <- err
return
}
} else {
errChan <- errors.New("unsupported sink type. Only 'file' type is supported")
return
}
}
refreshToken = tokenResponse.RefreshToken
nextRefreshCycle := time.Duration(tokenResponse.ExpiresIn-5) * time.Second // when the next access refresh will happen
d, err := time.ParseDuration(nextRefreshCycle.String())
if err != nil {
errChan <- fmt.Errorf("unable to parse refresh time because %s", err)
return
}
log.Info().Msgf("token refreshed and saved to selected path; next cycle will occur in %s", d.String())
for _, secretTemplate := range config.Templates {
processedTemplate, err := ProcessTemplate(secretTemplate.SourcePath, nil, tokenResponse.AccessToken)
if err != nil {
errChan <- err
return
}
if err := WriteBytesToFile(processedTemplate, secretTemplate.DestinationPath); err != nil {
errChan <- err
return
}
log.Info().Msgf("secret template at path %s has been rendered and saved to path %s", secretTemplate.SourcePath, secretTemplate.DestinationPath)
}
time.Sleep(nextRefreshCycle)
}
}
// runCmd represents the run command
var agentCmd = &cobra.Command{
Example: `
infisical agent
`,
Use: "agent",
Short: "agent",
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
log.Info().Msg("starting Infisical agent...")
configPath, err := cmd.Flags().GetString("config")
if err != nil {
util.HandleError(err, "Unable to parse flag config")
}
if !FileExists(configPath) {
log.Error().Msgf("Unable to locate %s. The provided agent config file path is either missing or incorrect", configPath)
return
}
agentConfig, err := ParseAgentConfig(configPath)
if err != nil {
log.Error().Msgf("Unable to prase %s because %v. Please ensure that is follows the Infisical Agent config structure", configPath, err)
return
}
errChan := make(chan error)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
switch configAuthType := agentConfig.Auth.Config.(type) {
case TokenAuthConfig:
content, err := ReadFile(configAuthType.TokenPath)
if err != nil {
log.Error().Msgf("unable to read initial token from file path %s because %v", configAuthType.TokenPath, err)
return
}
refreshToken := string(content)
go refreshTokenAndProcessTemplate(refreshToken, agentConfig, errChan)
case OAuthConfig:
// future auth types
default:
log.Error().Msgf("unknown auth config type. Only 'file' type is supported")
return
}
select {
case err := <-errChan:
log.Fatal().Msgf("agent stopped due to error: %v", err)
os.Exit(1)
case <-sigChan:
log.Info().Msg("agent is gracefully shutting...")
os.Exit(1)
}
},
}
func init() {
agentCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
command.Flags().MarkHidden("domain")
command.Parent().HelpFunc()(command, strings)
})
agentCmd.Flags().String("config", "agent-config.yaml", "The path to agent config yaml file")
rootCmd.AddCommand(agentCmd)
}

@ -152,46 +152,6 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
return plainTextSecrets, nil
}
func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(accessToken).
SetHeader("Accept", "application/json")
getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
IncludeImport: includeImports,
// TagSlugs: tagSlugs,
}
if secretsPath != "" {
getSecretsRequest.SecretPath = secretsPath
}
rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{WorkspaceId: workspaceId, SecretPath: environmentName, Environment: environmentName})
if err != nil {
return nil, err
}
plainTextSecrets := []models.SingleEnvironmentVariable{}
if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
for _, secret := range rawSecrets.Secrets {
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue})
}
// if includeImports {
// plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
// if err != nil {
// return nil, err
// }
// }
return plainTextSecrets, nil
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil

@ -1,5 +0,0 @@
{{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" }}
{{- range . }}
{{ .Key }}={{ .Value }}
{{- end }}
{{- end }}

@ -18,6 +18,7 @@ services:
redis:
image: redis
container_name: infisical-dev-redis
env_file: .env
environment:
- ALLOW_EMPTY_PASSWORD=yes
ports:

Binary file not shown.

Before

(image error) Size: 733 KiB

@ -1,93 +0,0 @@
---
title: "Infisical Agent"
---
Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical.
It eliminates the need to modify application logic by enabling clients to decide how they want their secrets rendered through the use of templates.
<img height="200" src="../images/agent/infisical-agent-diagram.png" />
### Key features:
- Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume
- Templating: Renders secrets via user provided templates to desired formats for applications to consume
### Token renewal
The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a Method, which is the authentication process suitable for your current setup, and Sinks, which are the places where the agent deposits the new access token whenever it receives updates.
When the Infisical Agent is started, it will attempts to obtain a valid access token using the authentication method you have configured. If the agent is unable to fetch a valid token, the agent will keep trying, increasing the time between each attempt.
Once a access token is successfully fetched, the agent will make sure the access token stays valid, continuing to renew it before it expires.
Every time the agent successfully retrieves a new access token, it writes the new token to the Sinks you've configured.
<Info>
Access tokens can be utilized with Infisical SDKs or directly in API requests to retrieve secrets from Infisical
</Info>
### Templating
The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path.
Templating process is done through the use of Go language's [text/template feature](https://pkg.go.dev/text/template). Multiple template definitions can be set in the agent configuration file to generate a variety of formatted secret files.
When the agent is started and templates are defined in the agent configuration file, the agent will attempt to acquire a valid access token using the set authentication method outlined in the agent's configuration.
If this initial attempt is unsuccessful, the agent will momentarily pauses before continuing to make more attempts.
Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it.
It then formats these secrets using the user provided templates and writes the formatted data to configured file paths.
## Agent configuration file
To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below.
While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional.
| Field | Description |
| ---------------------------- | ----------- |
| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. |
| `auth.type` | The type of authentication method used. Only `"token"` type is currently available |
| `auth.config.token-path` | The file path where the initial token for authentication is stored. |
| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. |
| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. |
| `templates[].source-path` | The path to the template file that should be used to render secrets. |
| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. |
## Quick start Infisical Agent
To install the Infisical agent, you must first install the [Infisical CLI](../cli/overview) in the desired environment where you'd like the agent to run. This is because the Infisical agent is a sub-command of the Infisical CLI.
Once you have the CLI installed, you will need to create a agent configuration file in yaml.
```yaml example-agent-config-file.yaml
infisical:
address: "https://app.infisical.com"
auth:
type: "token"
config:
token-path: "/path/to/initial/token"
sinks:
- type: "file"
config:
path: "/some/path/to/store/access-token/file-name"
templates:
- source-path: my-dot-ev-secret-template
destination-path: /some/path/.env
```
Above is an example agent configuration file that defines the token authentication method, one sink location (where to deposit access tokens after renewal) and a secret template.
```text my-dot-ev-secret-template
{{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" }}
{{- range . }}
{{ .Key }}={{ .Value }}
{{- end }}
{{- end }}
```
The secret template above will be used to render the secrets where the key and the value are separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets.
This function takes the following arguments: `secret "<project-id>" "<environment-slug>" "<secret-path>"`.
```bash
infisical agent --config example-agent-config-file.yaml
```
After defining the agent configuration file, run the command above pointing to the path where the agent configuration is located.

@ -195,12 +195,6 @@
"cli/faq"
]
},
{
"group": "Agent",
"pages": [
"infisical-agent/overview"
]
},
{
"group": "Integrations",
"pages": ["integrations/overview"]