diff --git a/backend/package-lock.json b/backend/package-lock.json index fc03ec328..6b1c1f97a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 0bcb36e49..56009e3e9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" }, diff --git a/backend/src/app.ts b/backend/src/app.ts index 34ed89997..1e1bbb593 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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) diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index e867f7692..2c5cce660 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -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 } diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index e69de29bb..472859de9 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -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 = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + + 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 = 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) +} \ No newline at end of file diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index d8f4d4eea..83c8fea94 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -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 }); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 562efeeab..0dbdfa076 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -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 }); diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 36f1dce49..50044387e 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -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((error).levelName.toLowerCase(), (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((error).level)){ + if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)) { Sentry.captureException(error) } - + res.status((error).statusCode).json((error).format(req)) next() } \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 5d95883a5..3be8418ee 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -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': diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 484b02cab..1b0364766 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -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' })) } }; diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index d36e32b91..bbaaff8c3 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -33,7 +33,8 @@ const secretSchema = new Schema( { version: { type: Number, - required: true + required: true, + default: 1 }, workspace: { type: Schema.Types.ObjectId, diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 17a91d39c..95ce3e6b0 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -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; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index eaa8e58a3..c90834d6d 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -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 ); diff --git a/backend/src/types/secret/types.ts b/backend/src/types/secret/types.ts new file mode 100644 index 000000000..177df8c0f --- /dev/null +++ b/backend/src/types/secret/types.ts @@ -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; + +// Omit the listed properties, then make everything optional and then make _id required +export type ModifySecretRequestBody = Assign>, { _id: string }>; + +// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading +export type SanitizedSecretModify = Partial>; + +// Everything is required, except the omitted types +export type SanitizedSecretForCreate = Omit; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 0bfe3c372..98ea0808a 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/cli/go.mod b/cli/go.mod index c48e3e2f9..956e9bb29 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -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 diff --git a/cli/go.sum b/cli/go.sum index d169d7b89..2b8836515 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -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= diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go new file mode 100644 index 000000000..5e6c4ca16 --- /dev/null +++ b/cli/packages/cmd/secrets.go @@ -0,0 +1,440 @@ +/* +Copyright © 2022 NAME HERE +*/ +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 ..."`, + 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 ..."`, + 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 ..."`, + 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 +} diff --git a/cli/packages/http/api.go b/cli/packages/http/api.go new file mode 100644 index 000000000..01b8a8a40 --- /dev/null +++ b/cli/packages/http/api.go @@ -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 +} diff --git a/cli/packages/models/api.go b/cli/packages/models/api.go index 552c7b9bc..d17200b87 100644 --- a/cli/packages/models/api.go +++ b/cli/packages/models/api.go @@ -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"` +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 8ba1c4627..70484de81 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -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 +} diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index f3ee274b3..44f14a12b 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -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" diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 80c98fa9a..bd0e67deb 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -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 + } +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index c6eee2d0c..23e117f4b 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -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 +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f88aed96b..66d2b56e3 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -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 +} diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go new file mode 100644 index 000000000..e9ac5d297 --- /dev/null +++ b/cli/packages/visualize/secrets.go @@ -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) +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 85d946a32..5658197ee 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -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 diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx new file mode 100644 index 000000000..32ddb2255 --- /dev/null +++ b/docs/cli/commands/secrets.mdx @@ -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 + + 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 + + Parse shell parameter expansions in your secrets + + Default value: `true` + + + + + + This command allows you selectively print the requested secrets by name + + ``` + $ infisical secrets get ... + + # Example + $ infisical secrets get DOMAIN + ┌─────────────┬──────────────┬─────────────┐ + │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ + ├─────────────┼──────────────┼─────────────┤ + │ DOMAIN │ example.com │ shared │ + └─────────────┴──────────────┴─────────────┘ + + ``` + + ### Flags + None + + + +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 ... + +## 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 + + + + This command allows you to delete secrets by their name(s). + + ``` + $ infisical secrets delete ... + + ## 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 + \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index bba08787c..05ae42747 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -97,6 +97,7 @@ "cli/commands/login", "cli/commands/init", "cli/commands/run", + "cli/commands/secrets", "cli/commands/export", "cli/commands/vault" ]