Merge branch 'main' into activity-logs

This commit is contained in:
mv-turtle
2023-01-04 21:12:24 -08:00
committed by GitHub
29 changed files with 1257 additions and 72 deletions

View File

@ -15,6 +15,7 @@
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
@ -39,6 +40,7 @@
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
@ -3744,6 +3746,14 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/await-to-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/axios": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
@ -11521,6 +11531,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -14962,6 +14980,11 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"await-to-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="
},
"axios": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
@ -17175,9 +17198,9 @@
"dev": true
},
"json5": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true
},
"jsonwebtoken": {
@ -20632,6 +20655,11 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
"integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg=="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -6,6 +6,7 @@
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
@ -30,6 +31,7 @@
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},

View File

@ -98,13 +98,13 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter);
app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{
if(res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`}))
app.use((req, res, next) => {
if (res.headersSent) return next();
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
})
//* Error Handling Middleware (must be after all routing logic)

View File

@ -1,9 +1,11 @@
import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
export {
export {
workspaceController,
serviceTokenDataController,
apiKeyDataController
apiKeyDataController,
secretController
}

View File

@ -0,0 +1,168 @@
import to from "await-to-js";
import { Request, Response } from "express";
import mongoose, { Types } from "mongoose";
import Secret, { ISecret } from "../../models/secret";
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret/types";
const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
export const batchCreateSecrets = async (req: Request, res: Response) => {
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environmentName } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
secretsToCreate.forEach(rawSecret => {
const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV,
secretKeyTag: rawSecret.secretKeyTag,
secretKeyHash: rawSecret.secretKeyHash,
secretValueCiphertext: rawSecret.secretValueCiphertext,
secretValueIV: rawSecret.secretValueIV,
secretValueTag: rawSecret.secretValueTag,
secretValueHash: rawSecret.secretValueHash,
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
secretCommentIV: rawSecret.secretCommentIV,
secretCommentTag: rawSecret.secretCommentTag,
secretCommentHash: rawSecret.secretCommentHash,
workspace: new Types.ObjectId(workspaceId),
environment: environmentName,
type: rawSecret.type,
user: new Types.ObjectId(req.user._id)
}
sanitizedSecretesToCreate.push(safeUpdateFields)
})
const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
if (bulkCreateError) {
if (bulkCreateError instanceof ValidationError) {
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
}
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
}
res.status(200).send()
}
export const createSingleSecret = async (req: Request, res: Response) => {
try {
const secretFromDB = await Secret.findById(req.params.secretId)
return res.status(200).send(secretFromDB);
} catch (e) {
throw BadRequestError({ message: "Unable to find the requested secret" })
}
}
export const batchDeleteSecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdsUserCanDeleteError) {
throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` })
}
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
secretIdsToDelete.forEach(secretIdToDelete => {
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
deleteOperationsToPerform.push(deleteOperation)
} else {
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
}
})
const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then())
if (bulkDeleteError) {
if (bulkDeleteError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack })
}
throw InternalServerError()
}
res.status(200).send()
}
export const batchModifySecrets = async (req: Request, res: Response) => {
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdsUserCanModifyError) {
throw InternalServerError({ message: "Unable to fetch secrets you own" })
}
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
const updateOperationsToPerform: any = []
secretsModificationsRequested.forEach(userModifiedSecret => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
secretKeyIV: userModifiedSecret.secretKeyIV,
secretKeyTag: userModifiedSecret.secretKeyTag,
secretKeyHash: userModifiedSecret.secretKeyHash,
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
secretValueIV: userModifiedSecret.secretValueIV,
secretValueTag: userModifiedSecret.secretValueTag,
secretValueHash: userModifiedSecret.secretValueHash,
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash,
}
const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } }
updateOperationsToPerform.push(updateOperation)
} else {
throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" })
}
})
const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then())
if (bulkModificationInfoError) {
if (bulkModificationInfoError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack })
}
throw InternalServerError()
}
return res.status(200).send()
}
export const fetchAllSecrets = async (req: Request, res: Response) => {
const { environment } = req.query;
const { workspaceId } = req.params;
let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in
if (req.user) {
userId = req.user._id.toString();
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
}
const [retriveAllSecretsError, allSecrets] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [{ user: userId }, { user: { $exists: false } }],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).then())
if (retriveAllSecretsError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack })
}
return res.json(allSecrets)
}

View File

@ -15,9 +15,7 @@ import {
* @param res
* @returns
*/
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).send({
serviceTokenData: req.serviceTokenData
});
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
/**
* Create new service token data for workspace with id [workspaceId] and
@ -29,9 +27,9 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData;
try {
const {
const {
name,
workspaceId,
workspaceId,
environment,
encryptedKey,
iv,
@ -41,10 +39,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
@ -56,12 +54,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
iv,
tag
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
} catch (err) {
@ -90,7 +88,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@ -98,7 +96,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
message: 'Failed to delete service token data'
});
}
return res.status(200).send({
serviceTokenData
});

View File

@ -1,7 +1,14 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Key,
Workspace,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
Key,
IUser,
ServiceToken,
ServiceTokenData
} from '../../models';
import {
@ -68,7 +75,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
@ -115,7 +122,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let userId;
if (req.user) {
userId = req.user._id.toString();
@ -130,7 +137,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
@ -170,7 +177,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
workspace: workspaceId,
receiver: req.user._id
}).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
@ -180,9 +187,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
});
}
return res.status(200).send({
key
});
return res.status(200).json(key);
}
export const getWorkspaceServiceTokenData = async (
req: Request,
@ -205,7 +210,7 @@ export const getWorkspaceServiceTokenData = async (
message: 'Failed to get workspace service token data'
});
}
return res.status(200).send({
serviceTokenData
});

View File

@ -4,26 +4,33 @@ import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
import { NODE_ENV } from "../config";
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => {
if(res.headersSent) return next();
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
if (res.headersSent) return next();
if (NODE_ENV !== "production") {
/* eslint-disable no-console */
console.log(error)
/* eslint-enable no-console */
}
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if(!(error instanceof RequestError)){
error = InternalServerError({context: {exception: error.message}, stack: error.stack})
if (!(error instanceof RequestError)) {
error = InternalServerError({ context: { exception: error.message }, stack: error.stack })
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}
//* Set Sentry user identification if req.user is populated
if(req.user !== undefined && req.user !== null){
if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: req.user.email })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){
if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)) {
Sentry.captureException(error)
}
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}

View File

@ -25,28 +25,28 @@ declare module 'jsonwebtoken' {
* @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st)
* @returns
*/
const requireAuth = ({
const requireAuth = ({
acceptedAuthModes = ['jwt']
}: {
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null)
return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null)
return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null)
return next(BadRequestError({ message: `Missing Authorization Header in the request header.` }))
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }))
if (AUTH_TOKEN_VALUE === null)
return next(BadRequestError({ message: 'Missing Authorization Body in the request header' }))
// validate auth token against
const authMode = validateAuthMode({
authTokenValue: AUTH_TOKEN_VALUE,
acceptedAuthModes
});
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
// attach auth payloads
switch (authMode) {
case 'serviceToken':

View File

@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
/**
* Validate intended inputs on [req] via express-validator
@ -15,12 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return next(BadRequestError({context: {errors: errors.array}}))
return next(ValidationError({ context: { errors: `One or more of your parameters are invalid [error(s)=${(JSON.stringify(errors))}]` } }))
}
return next();
} catch (err) {
return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'}))
return next(UnauthorizedRequestError({ message: 'Unauthenticated requests are not allowed. Try logging in' }))
}
};

View File

@ -33,7 +33,8 @@ const secretSchema = new Schema<ISecret>(
{
version: {
type: Number,
required: true
required: true,
default: 1
},
workspace: {
type: Schema.Types.ObjectId,

View File

@ -1,4 +1,83 @@
import express from 'express';
import express, { Request, Response } from 'express';
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
import { body, param, query } from 'express-validator';
import { ADMIN, MEMBER } from '../../variables';
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types';
import { secretController } from '../../controllers/v2';
import { fetchAllSecrets } from '../../controllers/v2/secretController';
const router = express.Router();
/**
* Create many secrets for a given workspace and environmentName
*/
router.post(
'/batch-create/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
validateRequest,
secretController.batchCreateSecrets
);
/**
* Get all secrets for a given environment and workspace id
*/
router.get(
'/workspace/:workspaceId',
param('workspaceId').exists().trim(),
query("environment").exists(),
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
fetchAllSecrets
);
/**
* Batch delete secrets in a given workspace and environment name
*/
router.delete(
'/batch/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
body('secretIds').exists().isArray().custom(array => array.length > 0),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.batchDeleteSecrets
);
/**
* Apply modifications to many existing secrets in a given workspace and environment
*/
router.patch(
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0),
param('workspaceId').exists().isMongoId().trim(),
param('environmentName').exists().trim(),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
validateRequest,
secretController.batchModifySecrets
);
export default router;

View File

@ -42,15 +42,15 @@ router.get(
);
router.get(
'/:workspaceId/key',
'/:workspaceId/encrypted-key',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
}),
param('workspaceId').exists().trim(),
validateRequest,
validateRequest,
workspaceController.getWorkspaceKey
);

View File

@ -0,0 +1,14 @@
import { Assign, Omit } from 'utility-types';
import { ISecret } from '../../models';
// Everything is required, except the omitted types
export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
// Omit the listed properties, then make everything optional and then make _id required
export type ModifySecretRequestBody = Assign<Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>, { _id: string }>;
// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading
export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>;
// Everything is required, except the omitted types
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;

View File

@ -1,7 +1,9 @@
{
"compilerOptions": {
"target": "es2016",
"lib": ["es6"],
"lib": [
"es6"
],
"module": "commonjs",
"rootDir": "src",
"resolveJsonModule": true,
@ -13,8 +15,15 @@
"strict": true,
"noImplicitAny": true,
"skipLibCheck": true,
"typeRoots": ["./src/types", "./node_modules/@types"]
"typeRoots": [
"./src/types",
"./node_modules/@types"
]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}

View File

@ -13,6 +13,7 @@ require (
require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/Luzifer/go-openssl/v4 v4.1.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
@ -34,6 +35,7 @@ require (
)
require (
github.com/Luzifer/go-openssl v2.0.0+incompatible
github.com/go-resty/resty/v2 v2.7.0
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible

View File

@ -2,6 +2,10 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/Luzifer/go-openssl v2.0.0+incompatible h1:EpNNxrPDji4rRzE0KeOeIeV7pHyKe8zF9oNnAXy4mBY=
github.com/Luzifer/go-openssl v2.0.0+incompatible/go.mod h1:t2qnLjT8WQ3usGU1R8uAqjY4T7CK7eMg9vhQ3l9Ue/Y=
github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM=
github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -96,15 +100,20 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -116,6 +125,7 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

440
cli/packages/cmd/secrets.go Normal file
View File

@ -0,0 +1,440 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"encoding/base64"
"fmt"
"strings"
"unicode"
"crypto/sha256"
"github.com/Infisical/infisical-merge/packages/http"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var secretsCmd = &cobra.Command{
Example: `infisical secrets"`,
Short: "Used to create, read update and delete secrets",
Use: "secrets",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
log.Errorln("Unable to parse the environment name flag")
log.Debugln(err)
return
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
log.Errorln("Unable to parse the substitute flag")
log.Debugln(err)
return
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
return
}
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
if shouldExpandSecrets {
secrets = util.SubstituteSecrets(secrets)
}
if err != nil {
log.Debugln(err)
return
}
visualize.PrintAllSecretDetails(secrets)
},
}
var secretsGetCmd = &cobra.Command{
Example: `secrets get <secret name A> <secret name B>..."`,
Short: "Used to retrieve secrets by name",
Use: "get [secrets]",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
PreRun: toggleDebug,
Run: getSecretsByNames,
}
var secretsSetCmd = &cobra.Command{
Example: `secrets set <secretName=secretValue> <secretName=secretValue>..."`,
Short: "Used set secrets",
Use: "set [secrets]",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// secretType, err := cmd.Flags().GetString("type")
// if err != nil {
// log.Errorln("Unable to parse the secret type flag")
// log.Debugln(err)
// return
// }
// if !util.IsSecretTypeValid(secretType) {
// log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType)
// return
// }
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
log.Errorln("Unable to parse the environment name flag")
log.Debugln(err)
return
}
if !util.IsSecretEnvironmentValid(environmentName) {
log.Errorln("You have entered a invalid environment name. Environment names can only be prod, dev, test or staging")
return
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
return
}
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
log.Error(err)
return
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil {
log.Error(err)
return
}
if !loggedInUserDetails.IsUserLoggedIn {
log.Error("You are not logged in yet. Please run [infisical login] then try again")
return
}
if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired {
log.Error("Your login has expired. Please run [infisical login] then try again")
return
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
request := models.GetEncryptedWorkspaceKeyRequest{
WorkspaceId: workspaceFile.WorkspaceId,
}
workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request)
if err != nil {
log.Errorf("unable to get your encrypted workspace key. [err=%v]", err)
return
}
encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.EncryptedKey)
encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Sender.PublicKey)
encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Nonce)
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
// decrypt workspace key
plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
if err != nil {
log.Error("unable to retrieve secrets. Run with -d to see full logs")
log.Debug(err)
}
type SecretSetOperation struct {
SecretKey string
SecretValue string
SecretOperation string
}
secretsToCreate := []models.Secret{}
secretsToModify := []models.Secret{}
secretOperations := []SecretSetOperation{}
secretByKey := getSecretsByKeys(secrets)
for _, arg := range args {
splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" {
log.Error("ensure that each secret has a none empty key and value. Modify the input and try again")
return
}
if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) {
log.Error("keys of secrets cannot start with a number. Modify the key name(s) and try again")
return
}
// Key and value from argument
key := strings.ToUpper(splitKeyValueFromArg[0])
value := splitKeyValueFromArg[1]
hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key)))
encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey))
if err != nil {
log.Errorf("unable to encrypt your secrets [err=%v]", err)
}
hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value)))
encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey))
if err != nil {
log.Errorf("unable to encrypt your secrets [err=%v]", err)
}
if existingSecret, ok := secretByKey[key]; ok {
// case: secret exists in project so it needs to be modified
encryptedSecretDetails := models.Secret{
ID: existingSecret.ID,
SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText),
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
}
// Only add to modifications if the value is different
if existingSecret.Value != value {
secretsToModify = append(secretsToModify, encryptedSecretDetails)
secretOperations = append(secretOperations, SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET VALUE MODIFIED",
})
} else {
// Current value is same as exisitng so no change
secretOperations = append(secretOperations, SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET VALUE UNCHANGED",
})
}
} else {
// case: secret doesn't exist in project so it needs to be created
encryptedSecretDetails := models.Secret{
SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText),
SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce),
SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag),
SecretKeyHash: hashedKey,
SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText),
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
SecretValueHash: hashedValue,
Type: util.SECRET_TYPE_SHARED,
}
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET CREATED",
})
}
}
if len(secretsToCreate) > 0 {
batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
Secrets: secretsToCreate,
}
err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest)
if err != nil {
log.Errorf("Unable to process new secret creations because %v", err)
return
}
}
if len(secretsToModify) > 0 {
batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
Secrets: secretsToModify,
}
err = http.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest)
if err != nil {
log.Errorf("Unable to process the modifications to your secrets because %v", err)
return
}
}
// Print secret operations
headers := []string{"SECRET NAME", "SECRET VALUE", "STATUS"}
rows := [][]string{}
for _, secretOperation := range secretOperations {
rows = append(rows, []string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation})
}
visualize.Table(headers, rows)
},
}
var secretsDeleteCmd = &cobra.Command{
Example: `secrets delete <secret name A> <secret name B>..."`,
Short: "Used to delete secrets by name",
Use: "delete [secrets]",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
log.Errorln("Unable to parse the environment name flag")
log.Debugln(err)
return
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil {
log.Error(err)
return
}
if !loggedInUserDetails.IsUserLoggedIn {
log.Error("You are not logged in yet. Please run [infisical login] then try again")
return
}
if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired {
log.Error("Your login has expired. Please run [infisical login] then try again")
return
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
return
}
workspaceFile, err := util.GetWorkSpaceFromFile()
if err != nil {
log.Error(err)
return
}
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
if err != nil {
log.Error("Unable to retrieve secrets. Run with -d to see full logs")
log.Debug(err)
}
secretByKey := getSecretsByKeys(secrets)
validSecretIdsToDelete := []string{}
invalidSecretNamesThatDoNotExist := []string{}
for _, secretKeyFromArg := range args {
if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok {
validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID)
} else {
invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg)
}
}
if len(invalidSecretNamesThatDoNotExist) != 0 {
log.Errorf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", "))
return
}
request := models.BatchDeleteSecretsBySecretIdsRequest{
WorkspaceId: workspaceFile.WorkspaceId,
EnvironmentName: environmentName,
SecretIds: validSecretIdsToDelete,
}
httpClient := resty.New().
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
err = http.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request)
if err != nil {
log.Errorf("Unable to complete your request because %v", err)
return
}
log.Infof("secret name(s) [%v] have been deleted from your project", strings.Join(args, ", "))
},
}
func init() {
secretsCmd.AddCommand(secretsGetCmd)
// secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets")
secretsCmd.AddCommand(secretsSetCmd)
secretsCmd.AddCommand(secretsDeleteCmd)
secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on")
secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
rootCmd.AddCommand(secretsCmd)
}
func getSecretsByNames(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
log.Errorln("Unable to parse the environment name flag")
log.Debugln(err)
return
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]")
return
}
secrets, err := util.GetAllEnvironmentVariables("", environmentName)
if err != nil {
log.Error("Unable to retrieve secrets. Run with -d to see full logs")
log.Debug(err)
}
requestedSecrets := []models.SingleEnvironmentVariable{}
secretsMap := make(map[string]models.SingleEnvironmentVariable)
for _, secret := range secrets {
secretsMap[secret.Key] = secret
}
for _, secretKeyFromArg := range args {
if value, ok := secretsMap[strings.ToUpper(secretKeyFromArg)]; ok {
requestedSecrets = append(requestedSecrets, value)
} else {
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
Key: secretKeyFromArg,
Type: "*not found*",
Value: "*not found*",
})
}
}
visualize.PrintAllSecretDetails(requestedSecrets)
}
func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
secretMapByName := make(map[string]models.SingleEnvironmentVariable)
for _, secret := range secrets {
secretMapByName[secret.Key] = secret
}
return secretMapByName
}

102
cli/packages/http/api.go Normal file
View File

@ -0,0 +1,102 @@
package http
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
)
func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchModifySecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Patch(endpoint)
if err != nil {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchCreateSecretsByWorkspaceAndEnvRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Post(endpoint)
if err != nil {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchDeleteSecretsBySecretIdsRequest) error {
endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName)
response, err := httpClient.
R().
SetBody(request).
Delete(endpoint)
if err != nil {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response)
}
return nil
}
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEncryptedWorkspaceKeyRequest) (models.GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v1/key/%v/latest", util.INFISICAL_URL, request.WorkspaceId)
var result models.GetEncryptedWorkspaceKeyResponse
response, err := httpClient.
R().
SetResult(&result).
Get(endpoint)
if err != nil {
return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
}
return result, nil
}
func CallGetEncryptedSecretsByWorkspaceIdAndEnv(httpClient resty.Client, request models.GetSecretsByWorkspaceIdAndEnvironmentRequest) (models.PullSecretsResponse, error) {
var pullSecretsRequestResponse models.PullSecretsResponse
response, err := httpClient.
R().
SetQueryParam("environment", request.EnvironmentName).
SetQueryParam("channel", "cli").
SetResult(&pullSecretsRequestResponse).
Get(fmt.Sprintf("%v/v1/secret/%v", util.INFISICAL_URL, request.WorkspaceId))
if err != nil {
return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unsuccessful response: [response=%s]", response)
}
return pullSecretsRequestResponse, nil
}

View File

@ -128,3 +128,71 @@ type Workspace struct {
V int `json:"__v"`
Organization string `json:"organization,omitempty"`
}
type Secret struct {
SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"`
SecretKeyIV string `json:"secretKeyIV,omitempty"`
SecretKeyTag string `json:"secretKeyTag,omitempty"`
SecretKeyHash string `json:"secretKeyHash,omitempty"`
SecretValueCiphertext string `json:"secretValueCiphertext,omitempty"`
SecretValueIV string `json:"secretValueIV,omitempty"`
SecretValueTag string `json:"secretValueTag,omitempty"`
SecretValueHash string `json:"secretValueHash,omitempty"`
SecretCommentCiphertext string `json:"secretCommentCiphertext,omitempty"`
SecretCommentIV string `json:"secretCommentIV,omitempty"`
SecretCommentTag string `json:"secretCommentTag,omitempty"`
SecretCommentHash string `json:"secretCommentHash,omitempty"`
Type string `json:"type,omitempty"`
ID string `json:"_id,omitempty"`
}
type BatchCreateSecretsByWorkspaceAndEnvRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchModifySecretsByWorkspaceAndEnvRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
Secrets []Secret `json:"secrets"`
}
type BatchDeleteSecretsBySecretIdsRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
SecretIds []string `json:"secretIds"`
}
type GetEncryptedWorkspaceKeyRequest struct {
WorkspaceId string `json:"workspaceId"`
}
type GetEncryptedWorkspaceKeyResponse struct {
LatestKey struct {
ID string `json:"_id"`
EncryptedKey string `json:"encryptedKey"`
Nonce string `json:"nonce"`
Sender struct {
ID string `json:"_id"`
Email string `json:"email"`
RefreshVersion int `json:"refreshVersion"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
PublicKey string `json:"publicKey"`
} `json:"sender"`
Receiver string `json:"receiver"`
Workspace string `json:"workspace"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"latestKey"`
}
type GetSecretsByWorkspaceIdAndEnvironmentRequest struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
}

View File

@ -18,8 +18,15 @@ type SingleEnvironmentVariable struct {
Key string `json:"key"`
Value string `json:"value"`
Type string `json:"type"`
ID string `json:"_id"`
}
type WorkspaceConfigFile struct {
WorkspaceId string `json:"workspaceId"`
}
type SymmetricEncryptionResult struct {
CipherText []byte
Nonce []byte
AuthTag []byte
}

View File

@ -10,6 +10,8 @@ const (
CONFIG_FOLDER_NAME = ".infisical"
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
SECRET_TYPE_PERSONAL = "personal"
SECRET_TYPE_SHARED = "shared"
)
var INFISICAL_URL = "https://app.infisical.com/api"

View File

@ -12,6 +12,12 @@ import (
const SERVICE_NAME = "infisical"
type LoggedInUserDetails struct {
IsUserLoggedIn bool
LoginExpired bool
UserCredentials models.UserCredentials
}
// To do: what happens if the user doesn't have a keyring in their system?
func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error {
userCredMarshalled, err := json.Marshal(userCred)
@ -102,3 +108,50 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) {
return false, "", nil
}
}
func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
if ConfigFileExists() {
configFile, err := GetConfigFile()
if err != nil {
return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get logged in user from config file [err=%s]", err)
}
if configFile.LoggedInUserEmail == "" {
return LoggedInUserDetails{}, nil
}
userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail)
if err != nil {
return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to your credentials from Keyring [err=%s]", err)
}
// check to to see if the JWT is still valid
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json")
response, err := httpClient.
R().
Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL))
if err != nil {
return LoggedInUserDetails{}, err
}
if response.StatusCode() > 299 {
return LoggedInUserDetails{
IsUserLoggedIn: true,
LoginExpired: true,
UserCredentials: userCreds,
}, nil
}
return LoggedInUserDetails{
IsUserLoggedIn: true,
LoginExpired: false,
UserCredentials: userCreds,
}, nil
} else {
return LoggedInUserDetails{}, nil
}
}

View File

@ -3,21 +3,27 @@ package util
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
"github.com/Infisical/infisical-merge/packages/models"
"golang.org/x/crypto/nacl/box"
)
func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) {
// will decrypt cipher text to plain text using iv and tag
func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV))
aesgcm, err := cipher.NewGCMWithNonceSize(block, len(iv))
if err != nil {
return nil, err
}
var nonce = IV
var ciphertext = append(encryptedPrivateKey, tag...)
var nonce = iv
var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
@ -26,3 +32,50 @@ func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []b
return plaintext, nil
}
func GenerateNewKey() (newKey []byte, keyErr error) {
key := make([]byte, 16) // block size defaults to 16 so this is fine
_, err := rand.Read(key)
return key, err
}
// Will encrypt a plain text with the provided key
func EncryptSymmetric(plaintext []byte, key []byte) (result models.SymmetricEncryptionResult, err error) {
block, err := aes.NewCipher(key)
if err != nil {
return models.SymmetricEncryptionResult{}, err
}
aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4
if err != nil {
return models.SymmetricEncryptionResult{}, err
}
// create a nonce
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
panic(err)
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it
authTag := ciphertext[len(ciphertext)-16:]
return models.SymmetricEncryptionResult{
CipherText: ciphertextOnly,
AuthTag: authTag,
Nonce: nonce,
}, nil
}
func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) {
plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey))
return plainTextToReturn
}
func EncryptAssymmetric(message []byte, nonce []byte, publicKey []byte, privateKey []byte) (encryptedMessage []byte) {
encryptedPlainText := box.Seal(nil, message, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey))
return encryptedPlainText
}

View File

@ -11,7 +11,6 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/nacl/box"
)
const PERSONAL_SECRET_TYPE_NAME = "personal"
@ -56,7 +55,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string,
}
// log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey)
workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey)
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsRequestResponse.Secrets {
@ -82,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string,
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
}
listOfEnv = append(listOfEnv, env)
@ -166,7 +166,8 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
return nil, err
}
workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
// workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey)
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets {
@ -192,6 +193,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
}
listOfEnv = append(listOfEnv, env)
@ -223,6 +225,7 @@ func GetAllEnvironmentVariables(projectId string, envName string) ([]models.Sing
return nil, err
}
// TODO: Should be based on flag. I.e only get all workspaces if desired, otherwise only get the one in the current root of project
workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath()
if err != nil {
return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME)
@ -385,3 +388,17 @@ func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []m
return secretsToReturn
}
func IsSecretEnvironmentValid(env string) bool {
if env == "prod" || env == "dev" || env == "test" || env == "staging" {
return true
}
return false
}
func IsSecretTypeValid(s string) bool {
if s == "personal" || s == "shared" {
return true
}
return false
}

View File

@ -0,0 +1,14 @@
package visualize
import "github.com/Infisical/infisical-merge/packages/models"
func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) {
rows := [][]string{}
for _, secret := range secrets {
rows = append(rows, []string{secret.Key, secret.Value, secret.Type})
}
headers := []string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"}
Table(headers, rows)
}

View File

@ -6,13 +6,23 @@ import (
"github.com/jedib0t/go-pretty/table"
)
type TableOptions struct {
Title string
}
// func GetDefaultTableOptions() TableOptions{
// return TableOptions{
// Title: "",
// }
// }
// Given headers and rows, this function will print out a table
func Table(headers []string, rows [][]string) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight)
// t.SetTitle("Title")
// t.SetTitle(tableOptions.Title)
t.Style().Options.DrawBorder = true
t.Style().Options.SeparateHeader = true
t.Style().Options.SeparateColumns = true

View File

@ -0,0 +1,93 @@
---
title: "infisical secrets"
---
```
infisical secrets
```
## Description
This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment.
### Sub-commands
<Accordion title="infisical secrets">
Use this command to print out all of the secrets in your project
```
$ infisical secrets
## Example
$ infisical secrets
┌─────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├─────────────┼──────────────┼─────────────┤
│ DOMAIN │ example.com │ shared │
│ HASH │ jebhfbwe │ shared │
└─────────────┴──────────────┴─────────────┘
```
### flags
<Accordion title="--expand">
Parse shell parameter expansions in your secrets
Default value: `true`
</Accordion>
</Accordion>
<Accordion title="infisical secrets get">
This command allows you selectively print the requested secrets by name
```
$ infisical secrets get <secret-name-a> <secret-name-b> ...
# Example
$ infisical secrets get DOMAIN
┌─────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├─────────────┼──────────────┼─────────────┤
│ DOMAIN │ example.com │ shared │
└─────────────┴──────────────┴─────────────┘
```
### Flags
None
</Accordion>
<Accordion title="infisical secrets set">
This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value.
If the secret key does not exist, a new secret will be created using both the key and value provided.
```
$ infisical secrets set <key1=value1> <key2=value2>...
## Example
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe
┌────────────────┬───────────────┬────────────────────────┐
│ SECRET NAME │ SECRET VALUE │ STATUS │
├────────────────┼───────────────┼────────────────────────┤
│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │
│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │
│ HASH │ jebhfbwe │ SECRET CREATED │
└────────────────┴───────────────┴────────────────────────┘
```
### Flags
None
</Accordion>
<Accordion title="infisical secrets delete">
This command allows you to delete secrets by their name(s).
```
$ infisical secrets delete <keyName1> <keyName2>...
## Example
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project
```
### Flags
None
</Accordion>

View File

@ -97,6 +97,7 @@
"cli/commands/login",
"cli/commands/init",
"cli/commands/run",
"cli/commands/secrets",
"cli/commands/export",
"cli/commands/vault"
]