mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge crud cli api and cli changes
This commit is contained in:
34
backend/package-lock.json
generated
34
backend/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -0,0 +1,138 @@
|
||||
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';
|
||||
|
||||
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()
|
||||
}
|
@ -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':
|
||||
|
@ -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' }))
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -33,7 +33,8 @@ const secretSchema = new Schema<ISecret>(
|
||||
{
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
default: 1
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
|
@ -1,4 +1,75 @@
|
||||
import express from 'express';
|
||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
|
||||
import { body, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types';
|
||||
import { secretController } from '../../controllers/v2';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Create many secrets for a given workspace and environmentName
|
||||
*/
|
||||
router.post(
|
||||
'/batch-create/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
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 a single secret by secret id
|
||||
*/
|
||||
router.get(
|
||||
'/:secretId', requireAuth, param('secretId').exists().trim(),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.createSingleSecret
|
||||
);
|
||||
|
||||
/**
|
||||
* Batch delete secrets in a given workspace and environment name
|
||||
*/
|
||||
router.delete(
|
||||
'/batch/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth,
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
body('secretIds').exists().isArray().custom(array => array.length > 0),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.batchDeleteSecrets
|
||||
|
||||
);
|
||||
|
||||
/**
|
||||
* Apply modifications to many existing secrets in a given workspace and environment
|
||||
*/
|
||||
router.patch(
|
||||
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
||||
requireAuth,
|
||||
body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0),
|
||||
param('workspaceId').exists().isMongoId().trim(),
|
||||
param('environmentName').exists().trim(),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
validateRequest,
|
||||
secretController.batchModifySecrets
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
14
backend/src/types/secret/types.ts
Normal file
14
backend/src/types/secret/types.ts
Normal 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">;
|
@ -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"
|
||||
]
|
||||
}
|
@ -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
|
||||
|
10
cli/go.sum
10
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=
|
||||
|
440
cli/packages/cmd/secrets.go
Normal file
440
cli/packages/cmd/secrets.go
Normal 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
102
cli/packages/http/api.go
Normal 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
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
14
cli/packages/visualize/secrets.go
Normal file
14
cli/packages/visualize/secrets.go
Normal 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)
|
||||
}
|
@ -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
|
||||
|
93
docs/cli/commands/secrets.mdx
Normal file
93
docs/cli/commands/secrets.mdx
Normal 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>
|
@ -94,6 +94,7 @@
|
||||
"cli/commands/login",
|
||||
"cli/commands/init",
|
||||
"cli/commands/run",
|
||||
"cli/commands/secrets",
|
||||
"cli/commands/export",
|
||||
"cli/commands/vault"
|
||||
]
|
||||
|
Reference in New Issue
Block a user