mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-27 09:40:45 +00:00
Compare commits
5 Commits
infisica-a
...
default-no
Author | SHA1 | Date | |
---|---|---|---|
caa0dc6e3f | |||
4d28a66572 | |||
7346b2ff34 | |||
ab361b1315 | |||
94f893017b |
backend/src
cli
docker-compose.ymldocs
@ -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
|
||||
});
|
||||
|
42
backend/src/controllers/v4/environmentsController.ts
Normal file
42
backend/src/controllers/v4/environmentsController.ts
Normal file
@ -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
|
||||
}
|
87
backend/src/controllers/v4/foldersController.ts
Normal file
87
backend/src/controllers/v4/foldersController.ts
Normal file
@ -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
|
||||
}
|
9
backend/src/controllers/v4/index.ts
Normal file
9
backend/src/controllers/v4/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as secretsController from "./secretsController";
|
||||
import * as environmentsController from "./environmentsController";
|
||||
import * as foldersController from "./foldersController";
|
||||
|
||||
export {
|
||||
secretsController,
|
||||
environmentsController,
|
||||
foldersController
|
||||
}
|
353
backend/src/controllers/v4/secretsController.ts
Normal file
353
backend/src/controllers/v4/secretsController.ts
Normal file
@ -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: {
|
||||
|
39
backend/src/routes/v4/environments.ts
Normal file
39
backend/src/routes/v4/environments.ts
Normal file
@ -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;
|
39
backend/src/routes/v4/folders.ts
Normal file
39
backend/src/routes/v4/folders.ts
Normal file
@ -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;
|
5
backend/src/routes/v4/index.ts
Normal file
5
backend/src/routes/v4/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import secrets from "./secrets";
|
||||
|
||||
export {
|
||||
secrets
|
||||
}
|
46
backend/src/routes/v4/secrets.ts
Normal file
46
backend/src/routes/v4/secrets.ts
Normal file
@ -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"]
|
||||
|
Reference in New Issue
Block a user