mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 23:28:45 +00:00
Finish preliminary /v2/secrets routes for batch/single CRUD secrets endpoints
This commit is contained in:
+6
-4
@@ -38,6 +38,7 @@ import {
|
|||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
secret as v2SecretRouter,
|
secret as v2SecretRouter,
|
||||||
|
secrets as v2SecretsRouter,
|
||||||
workspace as v2WorkspaceRouter,
|
workspace as v2WorkspaceRouter,
|
||||||
serviceTokenData as v2ServiceTokenDataRouter,
|
serviceTokenData as v2ServiceTokenDataRouter,
|
||||||
apiKeyData as v2APIKeyDataRouter,
|
apiKeyData as v2APIKeyDataRouter,
|
||||||
@@ -91,16 +92,17 @@ app.use('/api/v1/membership', v1MembershipRouter);
|
|||||||
app.use('/api/v1/key', v1KeyRouter);
|
app.use('/api/v1/key', v1KeyRouter);
|
||||||
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||||
app.use('/api/v1/secret', v1SecretRouter);
|
app.use('/api/v1/secret', v1SecretRouter);
|
||||||
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
|
app.use('/api/v1/service-token', v1ServiceTokenRouter); // stop supporting
|
||||||
app.use('/api/v1/password', v1PasswordRouter);
|
app.use('/api/v1/password', v1PasswordRouter);
|
||||||
app.use('/api/v1/stripe', v1StripeRouter);
|
app.use('/api/v1/stripe', v1StripeRouter);
|
||||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
|
||||||
// v2 routes
|
// v2 routes
|
||||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/secret', v2SecretRouter);
|
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
|
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||||
|
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import * as workspaceController from './workspaceController';
|
|||||||
import * as serviceTokenDataController from './serviceTokenDataController';
|
import * as serviceTokenDataController from './serviceTokenDataController';
|
||||||
import * as apiKeyDataController from './apiKeyDataController';
|
import * as apiKeyDataController from './apiKeyDataController';
|
||||||
import * as secretController from './secretController';
|
import * as secretController from './secretController';
|
||||||
|
import * as secretsController from './secretsController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
workspaceController,
|
workspaceController,
|
||||||
serviceTokenDataController,
|
serviceTokenDataController,
|
||||||
apiKeyDataController,
|
apiKeyDataController,
|
||||||
secretController
|
secretController,
|
||||||
|
secretsController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ const { ValidationError } = mongoose.Error;
|
|||||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||||
import { AnyBulkWriteOperation } from 'mongodb';
|
import { AnyBulkWriteOperation } from 'mongodb';
|
||||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||||
import { validateMembership } from "../../helpers/membership";
|
|
||||||
import { ADMIN, MEMBER } from '../../variables';
|
|
||||||
import { postHogClient } from '../../services';
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
export const createSingleSecret = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecret = async (req: Request, res: Response) => {
|
||||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environment } = req.params
|
||||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||||
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
||||||
secretKeyIV: secretToCreate.secretKeyIV,
|
secretKeyIV: secretToCreate.secretKeyIV,
|
||||||
@@ -28,13 +31,13 @@ export const createSingleSecret = async (req: Request, res: Response) => {
|
|||||||
secretCommentTag: secretToCreate.secretCommentTag,
|
secretCommentTag: secretToCreate.secretCommentTag,
|
||||||
secretCommentHash: secretToCreate.secretCommentHash,
|
secretCommentHash: secretToCreate.secretCommentHash,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment: environmentName,
|
environment,
|
||||||
type: secretToCreate.type,
|
type: secretToCreate.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const [error, newlyCreatedSecret] = await to(Secret.create(sanitizedSecret).then())
|
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
throw RouteValidationError({ message: error.message, stack: error.stack })
|
throw RouteValidationError({ message: error.message, stack: error.stack })
|
||||||
}
|
}
|
||||||
@@ -45,20 +48,27 @@ export const createSingleSecret = async (req: Request, res: Response) => {
|
|||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
environment: environmentName,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
// #TODO: why does this route have no channel?
|
environment,
|
||||||
// channel: channel ? channel : 'cli'
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send()
|
res.status(200).send({
|
||||||
|
secret
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const batchCreateSecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Create many secrets for workspace wiht id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environment } = req.params
|
||||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||||
|
|
||||||
secretsToCreate.forEach(rawSecret => {
|
secretsToCreate.forEach(rawSecret => {
|
||||||
@@ -76,7 +86,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
|||||||
secretCommentTag: rawSecret.secretCommentTag,
|
secretCommentTag: rawSecret.secretCommentTag,
|
||||||
secretCommentHash: rawSecret.secretCommentHash,
|
secretCommentHash: rawSecret.secretCommentHash,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment: environmentName,
|
environment,
|
||||||
type: rawSecret.type,
|
type: rawSecret.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id)
|
||||||
}
|
}
|
||||||
@@ -84,7 +94,7 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
|||||||
sanitizedSecretesToCreate.push(safeUpdateFields)
|
sanitizedSecretesToCreate.push(safeUpdateFields)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
||||||
if (bulkCreateError) {
|
if (bulkCreateError) {
|
||||||
if (bulkCreateError instanceof ValidationError) {
|
if (bulkCreateError instanceof ValidationError) {
|
||||||
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
|
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
|
||||||
@@ -99,18 +109,25 @@ export const batchCreateSecrets = async (req: Request, res: Response) => {
|
|||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: (secretsToCreate ?? []).length,
|
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||||
environment: environmentName,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
// #TODO: why does this route have no channel?
|
environment,
|
||||||
// channel: channel ? channel : 'cli'
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send()
|
res.status(200).send({
|
||||||
|
secrets
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretIdsToDelete: string[] = req.body.secretIds
|
const secretIdsToDelete: string[] = req.body.secretIds
|
||||||
|
|
||||||
@@ -149,8 +166,8 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
|||||||
numberOfSecrets: numSecretsDeleted,
|
numberOfSecrets: numSecretsDeleted,
|
||||||
environment: environmentName,
|
environment: environmentName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
// #TODO: why does this route have no channel?
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// channel: channel ? channel : 'cli'
|
userAgent: req.headers?.['user-agent']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,26 +175,13 @@ export const batchDeleteSecrets = async (req: Request, res: Response) => {
|
|||||||
res.status(200).send()
|
res.status(200).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteSingleSecret = async (req: Request, res: Response) => {
|
/**
|
||||||
const { secretId } = req.params;
|
* Delete secret with id [secretId]
|
||||||
|
* @param req
|
||||||
const [error, singleSecretRetrieved] = await to(Secret.findById(secretId).then())
|
* @param res
|
||||||
if (error instanceof ValidationError) {
|
*/
|
||||||
throw RouteValidationError({ message: "Unable to get secret, please try again", stack: error.stack })
|
export const deleteSecret = async (req: Request, res: Response) => {
|
||||||
}
|
await Secret.findByIdAndDelete(req._secret._id)
|
||||||
|
|
||||||
if (singleSecretRetrieved) {
|
|
||||||
const [membershipValidationError, membership] = await to(validateMembership({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId: singleSecretRetrieved.workspace._id.toString(),
|
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (membershipValidationError || !membership) {
|
|
||||||
throw UnauthorizedRequestError()
|
|
||||||
}
|
|
||||||
|
|
||||||
await Secret.findByIdAndDelete(secretId)
|
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
@@ -185,22 +189,26 @@ export const deleteSingleSecret = async (req: Request, res: Response) => {
|
|||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
// #TODO: how do we get env name ans project id?
|
workspaceId: req._secret.workspace.toString(),
|
||||||
// environment: environmentName,
|
environment: req._secret.environment,
|
||||||
// workspaceId,
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// #TODO: why does this route have no channel?
|
userAgent: req.headers?.['user-agent']
|
||||||
// channel: channel ? channel : 'cli'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send()
|
res.status(200).send({
|
||||||
} else {
|
secret: req._secret
|
||||||
throw BadRequestError()
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const batchModifySecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||||
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
@@ -252,8 +260,8 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
|
|||||||
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||||
environment: environmentName,
|
environment: environmentName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
// #TODO: why does this route have no channel?
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// channel: channel ? channel : 'cli'
|
userAgent: req.headers?.['user-agent']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -261,8 +269,13 @@ export const batchModifySecrets = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send()
|
return res.status(200).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// #TODO: I assume this should be '...Secret'?
|
/**
|
||||||
export const modifySingleSecrets = async (req: Request, res: Response) => {
|
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSecret = async (req: Request, res: Response) => {
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||||
|
|
||||||
@@ -299,8 +312,8 @@ export const modifySingleSecrets = async (req: Request, res: Response) => {
|
|||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
environment: environmentName,
|
environment: environmentName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
// #TODO: why does this route have no channel?
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// channel: channel ? channel : 'cli'
|
userAgent: req.headers?.['user-agent']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -308,11 +321,18 @@ export const modifySingleSecrets = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send(singleModificationUpdate)
|
return res.status(200).send(singleModificationUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchAllSecrets = async (req: Request, res: Response) => {
|
/**
|
||||||
|
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
||||||
|
* with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
const { environment } = req.query;
|
const { environment } = req.query;
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in
|
let userId: string | undefined = undefined // used for getting personal secrets for user
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
userId = req.user._id.toString();
|
userId = req.user._id.toString();
|
||||||
}
|
}
|
||||||
@@ -321,7 +341,7 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
|
|||||||
userId = req.serviceTokenData.user._id
|
userId = req.serviceTokenData.user._id
|
||||||
}
|
}
|
||||||
|
|
||||||
const [retriveAllSecretsError, allSecrets] = await to(Secret.find(
|
const [err, secrets] = await to(Secret.find(
|
||||||
{
|
{
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment,
|
environment,
|
||||||
@@ -330,8 +350,8 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
).then())
|
).then())
|
||||||
|
|
||||||
if (retriveAllSecretsError instanceof ValidationError) {
|
if (err) {
|
||||||
throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack })
|
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
@@ -339,56 +359,40 @@ export const fetchAllSecrets = async (req: Request, res: Response) => {
|
|||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: (allSecrets ?? []).length,
|
numberOfSecrets: (secrets ?? []).length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
// #TODO: why does this route have no channel?
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// channel: channel ? channel : 'cli'
|
userAgent: req.headers?.['user-agent']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(allSecrets)
|
return res.json(secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchSingleSecret = async (req: Request, res: Response) => {
|
/**
|
||||||
const { secretId } = req.params;
|
* Return secret with id [secretId]
|
||||||
|
* @param req
|
||||||
const [error, singleSecretRetrieved] = await to(Secret.findById(secretId).then())
|
* @param res
|
||||||
|
* @returns
|
||||||
if (error instanceof ValidationError) {
|
*/
|
||||||
throw RouteValidationError({ message: "Unable to get secret, please try again", stack: error.stack })
|
export const getSecret = async (req: Request, res: Response) => {
|
||||||
}
|
|
||||||
|
|
||||||
if (singleSecretRetrieved) {
|
|
||||||
const [membershipValidationError, membership] = await to(validateMembership({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId: singleSecretRetrieved.workspace._id.toString(),
|
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (membershipValidationError || !membership) {
|
|
||||||
throw UnauthorizedRequestError()
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(singleSecretRetrieved)
|
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
// #TODO: how do we get environment and workspace here? Do we need that? When is this route used?
|
workspaceId: req._secret.workspace.toString(),
|
||||||
// environment,
|
environment: req._secret.environment,
|
||||||
// workspaceId,
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
// #TODO: why does this route have no channel?
|
userAgent: req.headers?.['user-agent']
|
||||||
// channel: channel ? channel : 'cli'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
return res.status(200).send({
|
||||||
throw BadRequestError()
|
secret: req._secret
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
import to from 'await-to-js';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ISecret, Secret } from '../../models';
|
||||||
|
import {
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
SECRET_SHARED,
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
} from '../../variables';
|
||||||
|
import { ValidationError } from '../../utils/errors';
|
||||||
|
import { EESecretService, EELogService } from '../../ee/services';
|
||||||
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
|
let toAdd;
|
||||||
|
if (Array.isArray(req.body.secrets)) {
|
||||||
|
// case: create multiple secrets
|
||||||
|
toAdd = req.body.secrets;
|
||||||
|
} else if (typeof req.body.secrets === 'object') {
|
||||||
|
// case: create 1 secret
|
||||||
|
toAdd = [req.body.secrets];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSecrets = await Secret.insertMany(
|
||||||
|
toAdd.map(({
|
||||||
|
type,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
}) => ({
|
||||||
|
version: 1,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
type,
|
||||||
|
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// (EE) add secret versions for new secrets
|
||||||
|
EESecretService.addSecretVersions({
|
||||||
|
secretVersions: newSecrets.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const addAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_ADD_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: newSecrets.map((n) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
addAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
actions: [addAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: toAdd.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: newSecrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret(s) for workspace with id [workspaceId], environment [environment] and user
|
||||||
|
* with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environment } = req.query;
|
||||||
|
|
||||||
|
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||||
|
if (req.user) {
|
||||||
|
userId = req.user._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.serviceTokenData) {
|
||||||
|
userId = req.serviceTokenData.user._id
|
||||||
|
}
|
||||||
|
|
||||||
|
const [err, secrets] = await to(Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
$or: [
|
||||||
|
{ user: userId },
|
||||||
|
{ user: { $exists: false } }
|
||||||
|
],
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
|
).then())
|
||||||
|
|
||||||
|
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||||
|
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
|
||||||
|
const readAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_READ_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: workspaceId as string,
|
||||||
|
secretIds: secrets.map((n: any) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
readAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: workspaceId as string,
|
||||||
|
actions: [readAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secret(s) in workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
|
// TODO: move type
|
||||||
|
interface PatchSecret {
|
||||||
|
id: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext: string;
|
||||||
|
secretCommentIV: string;
|
||||||
|
secretCommentTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops = req.body.secrets.map((secret: PatchSecret) => {
|
||||||
|
const {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} = secret;
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: { _id: new Types.ObjectId(secret.id) },
|
||||||
|
update: {
|
||||||
|
$inc: {
|
||||||
|
version: 1
|
||||||
|
},
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
...((
|
||||||
|
secretCommentCiphertext &&
|
||||||
|
secretCommentIV &&
|
||||||
|
secretCommentTag
|
||||||
|
) ? {
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const b = await Secret.bulkWrite(ops);
|
||||||
|
|
||||||
|
let newSecretsObj: { [key: string]: PatchSecret } = {};
|
||||||
|
req.body.secrets.forEach((secret: PatchSecret) => {
|
||||||
|
newSecretsObj[secret.id] = secret;
|
||||||
|
});
|
||||||
|
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: req.secrets.map((secret: ISecret) => {
|
||||||
|
const {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} = newSecretsObj[secret._id.toString()]
|
||||||
|
return ({
|
||||||
|
secret: secret._id,
|
||||||
|
version: secret.version + 1,
|
||||||
|
workspace: secret.workspace,
|
||||||
|
type: secret.type,
|
||||||
|
environment: secret.environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
...((
|
||||||
|
secretCommentCiphertext &&
|
||||||
|
secretCommentIV &&
|
||||||
|
secretCommentTag
|
||||||
|
) ? {
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} : {
|
||||||
|
secretCommentCiphertext: '',
|
||||||
|
secretCommentIV: '',
|
||||||
|
secretCommentTag: ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// group secrets into workspaces so updated secrets can
|
||||||
|
// be logged and snapshotted separately for each workspace
|
||||||
|
let workspaceSecretObj: any = {};
|
||||||
|
req.secrets.forEach((s: any) => {
|
||||||
|
if (s.workspace.toString() in workspaceSecretObj) {
|
||||||
|
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||||
|
} else {
|
||||||
|
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
|
const updateAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_DELETE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
updateAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
actions: [updateAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
|
workspaceId: key,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_UPDATE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: req.secrets.map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
updateAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
actions: [updateAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: req.secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: req.secrets.map((secret: ISecret) => secret._id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secret(s) in workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
const toDelete = req.secrets.map((s: any) => s._id);
|
||||||
|
|
||||||
|
await Secret.deleteMany({
|
||||||
|
_id: {
|
||||||
|
$in: toDelete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: toDelete
|
||||||
|
});
|
||||||
|
|
||||||
|
// group secrets into workspaces so deleted secrets can
|
||||||
|
// be logged and snapshotted separately for each workspace
|
||||||
|
let workspaceSecretObj: any = {};
|
||||||
|
req.secrets.forEach((s: any) => {
|
||||||
|
if (s.workspace.toString() in workspaceSecretObj) {
|
||||||
|
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||||
|
} else {
|
||||||
|
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
|
const deleteAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_DELETE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
deleteAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
actions: [deleteAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
|
workspaceId: key,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
secrets: req.secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -86,8 +86,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretKeyHash: {
|
secretKeyHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
secretValueCiphertext: {
|
secretValueCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -102,8 +101,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretValueHash: {
|
secretValueHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
|
Membership
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
EESecretService,
|
EESecretService,
|
||||||
@@ -20,6 +21,47 @@ import {
|
|||||||
ACTION_READ_SECRETS
|
ACTION_READ_SECRETS
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] can modify secrets with ids [secretIds]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.userId - id of user to validate
|
||||||
|
* @param {Object} obj.secretIds - secret ids
|
||||||
|
* @returns {Secret[]} secrets
|
||||||
|
*/
|
||||||
|
const validateSecrets = async ({
|
||||||
|
userId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
secretIds: string[];
|
||||||
|
}) =>{
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
secrets = await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: secretIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaceIdsSet = new Set((await Membership.find({
|
||||||
|
user: userId
|
||||||
|
}, 'workspace'))
|
||||||
|
.map((m) => m.workspace.toString()));
|
||||||
|
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
if (!workspaceIdsSet.has(secret.workspace.toString())) {
|
||||||
|
throw new Error('Failed to validate secret');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new Error('Failed to validate secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
interface V1PushSecret {
|
interface V1PushSecret {
|
||||||
ciphertextKey: string;
|
ciphertextKey: string;
|
||||||
ivKey: string;
|
ivKey: string;
|
||||||
@@ -258,6 +300,7 @@ const v1PushSecrets = async ({
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash
|
secretValueHash
|
||||||
}) => ({
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
secret: _id,
|
secret: _id,
|
||||||
version,
|
version,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -280,7 +323,7 @@ const v1PushSecrets = async ({
|
|||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -527,6 +570,7 @@ const v1PushSecrets = async ({
|
|||||||
environment: string;
|
environment: string;
|
||||||
}): Promise<ISecret[]> => {
|
}): Promise<ISecret[]> => {
|
||||||
let secrets: any; // TODO: FIX any
|
let secrets: any; // TODO: FIX any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get shared workspace secrets
|
// get shared workspace secrets
|
||||||
const sharedSecrets = await Secret.find({
|
const sharedSecrets = await Secret.find({
|
||||||
@@ -655,6 +699,7 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
validateSecrets,
|
||||||
v1PushSecrets,
|
v1PushSecrets,
|
||||||
v2PushSecrets,
|
v2PushSecrets,
|
||||||
pullSecrets,
|
pullSecrets,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizati
|
|||||||
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
import requireServiceTokenAuth from './requireServiceTokenAuth';
|
||||||
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
|
||||||
import requireSecretAuth from './requireSecretAuth';
|
import requireSecretAuth from './requireSecretAuth';
|
||||||
|
import requireSecretsAuth from './requireSecretsAuth';
|
||||||
import validateRequest from './validateRequest';
|
import validateRequest from './validateRequest';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -21,5 +22,6 @@ export {
|
|||||||
requireServiceTokenAuth,
|
requireServiceTokenAuth,
|
||||||
requireServiceTokenDataAuth,
|
requireServiceTokenDataAuth,
|
||||||
requireSecretAuth,
|
requireSecretAuth,
|
||||||
|
requireSecretsAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
validateMembership
|
validateMembership
|
||||||
} from '../helpers/membership';
|
} from '../helpers/membership';
|
||||||
|
|
||||||
|
// note: used for old /v1/secret and /v2/secret routes.
|
||||||
|
// newer /v2/secrets routes use [requireSecretsAuth] middleware
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request has proper membership to modify secret.
|
* Validate if user on request has proper membership to modify secret.
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -34,7 +37,7 @@ const requireSecretAuth = ({
|
|||||||
acceptedRoles
|
acceptedRoles
|
||||||
});
|
});
|
||||||
|
|
||||||
req.secret = secret as any;
|
req._secret = secret;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
import { Secret, Membership } from '../models';
|
||||||
|
import { validateSecrets } from '../helpers/secret';
|
||||||
|
|
||||||
|
// TODO: make this work for delete route
|
||||||
|
|
||||||
|
const requireSecretsAuth = ({
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
if (Array.isArray(req.body.secrets)) {
|
||||||
|
// case: validate multiple secrets
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: req.body.secrets.map((s: any) => s.id)
|
||||||
|
});
|
||||||
|
} else if (typeof req.body.secrets === 'object') { // change this to check for object
|
||||||
|
// case: validate 1 secret
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: req.body.secrets.id
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(req.body.secretIds)) {
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: req.body.secretIds
|
||||||
|
});
|
||||||
|
} else if (typeof req.body.secretIds === 'string') {
|
||||||
|
// case: validate secretIds
|
||||||
|
secrets = await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: [req.body.secretIds]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.secrets = secrets;
|
||||||
|
return next();
|
||||||
|
} catch (err) {
|
||||||
|
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireSecretsAuth;
|
||||||
@@ -69,8 +69,7 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretKeyHash: {
|
secretKeyHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
secretValueCiphertext: {
|
secretValueCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -85,8 +84,7 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
secretValueHash: {
|
secretValueHash: {
|
||||||
type: String,
|
type: String
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
secretCommentCiphertext: {
|
secretCommentCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import secret from './secret';
|
import secret from './secret'; // stop-supporting
|
||||||
|
import secrets from './secrets';
|
||||||
import workspace from './workspace';
|
import workspace from './workspace';
|
||||||
import serviceTokenData from './serviceTokenData';
|
import serviceTokenData from './serviceTokenData';
|
||||||
import apiKeyData from './apiKeyData';
|
import apiKeyData from './apiKeyData';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
secret,
|
secret,
|
||||||
|
secrets,
|
||||||
workspace,
|
workspace,
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
apiKeyData
|
apiKeyData
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express from 'express';
|
||||||
import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware';
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireSecretAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../middleware';
|
||||||
import { body, param, query } from 'express-validator';
|
import { body, param, query } from 'express-validator';
|
||||||
import { ADMIN, MEMBER } from '../../variables';
|
import { ADMIN, MEMBER } from '../../variables';
|
||||||
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret';
|
||||||
import { secretController } from '../../controllers/v2';
|
import { secretController } from '../../controllers/v2';
|
||||||
import { fetchAllSecrets, fetchSingleSecret } from '../../controllers/v2/secretController';
|
|
||||||
|
// note to devs: stop supporting
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
/**
|
|
||||||
* Create many secrets for a given workspace and environmentName
|
|
||||||
*/
|
|
||||||
router.post(
|
router.post(
|
||||||
'/batch-create/workspace/:workspaceId/environment/:environmentName',
|
'/batch-create/workspace/:workspaceId/environment/:environment',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
@@ -20,17 +23,15 @@ router.post(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().isMongoId().trim(),
|
param('workspaceId').exists().isMongoId().trim(),
|
||||||
param('environmentName').exists().trim(),
|
param('environment').exists().trim(),
|
||||||
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
|
body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')),
|
||||||
|
body('channel'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.batchCreateSecrets
|
secretController.createSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Create single secret for a given workspace and environmentName
|
|
||||||
*/
|
|
||||||
router.post(
|
router.post(
|
||||||
'/workspace/:workspaceId/environment/:environmentName',
|
'/workspace/:workspaceId/environment/:environment',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
@@ -38,15 +39,13 @@ router.post(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().isMongoId().trim(),
|
param('workspaceId').exists().isMongoId().trim(),
|
||||||
param('environmentName').exists().trim(),
|
param('environment').exists().trim(),
|
||||||
body('secret').exists().isObject(),
|
body('secret').exists().isObject(),
|
||||||
|
body('channel'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.createSingleSecret
|
secretController.createSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all secrets for a given environment and workspace id
|
|
||||||
*/
|
|
||||||
router.get(
|
router.get(
|
||||||
'/workspace/:workspaceId',
|
'/workspace/:workspaceId',
|
||||||
param('workspaceId').exists().trim(),
|
param('workspaceId').exists().trim(),
|
||||||
@@ -57,25 +56,23 @@ router.get(
|
|||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
|
query('channel'),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
fetchAllSecrets
|
secretController.getSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Get single secret by id
|
|
||||||
*/
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:secretId',
|
'/:secretId',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||||
}),
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
fetchSingleSecret
|
secretController.getSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch delete secrets in a given workspace and environment name
|
|
||||||
*/
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/batch/workspace/:workspaceId/environment/:environmentName',
|
'/batch/workspace/:workspaceId/environment/:environmentName',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -88,26 +85,22 @@ router.delete(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.batchDeleteSecrets
|
secretController.deleteSecrets
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* delete single secret by id
|
|
||||||
*/
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/:secretId',
|
'/:secretId',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ['jwt']
|
||||||
}),
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
param('secretId').isMongoId(),
|
param('secretId').isMongoId(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.deleteSingleSecret
|
secretController.deleteSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply modifications to many existing secrets in a given workspace and environment
|
|
||||||
*/
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
'/batch-modify/workspace/:workspaceId/environment/:environmentName',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -120,12 +113,10 @@ router.patch(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.batchModifySecrets
|
secretController.updateSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply modifications to single existing secret in a given workspace and environment
|
|
||||||
*/
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/workspace/:workspaceId/environment/:environmentName',
|
'/workspace/:workspaceId/environment/:environmentName',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -138,7 +129,7 @@ router.patch(
|
|||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
secretController.modifySingleSecrets
|
secretController.updateSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
requireSecretsAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../middleware';
|
||||||
|
import { query, check, body } from 'express-validator';
|
||||||
|
import { secretsController } from '../../controllers/v2';
|
||||||
|
import {
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
SECRET_SHARED
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
body('workspaceId').exists().isString().trim(),
|
||||||
|
body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
||||||
|
body('secrets')
|
||||||
|
.exists()
|
||||||
|
.custom((value) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// case: create multiple secrets
|
||||||
|
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
||||||
|
for (const secret of value) {
|
||||||
|
if (
|
||||||
|
!secret.type ||
|
||||||
|
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
|
||||||
|
!secret.secretKeyCiphertext ||
|
||||||
|
!secret.secretKeyIV ||
|
||||||
|
!secret.secretKeyTag ||
|
||||||
|
!secret.secretValueCiphertext ||
|
||||||
|
!secret.secretValueIV ||
|
||||||
|
!secret.secretValueTag
|
||||||
|
) {
|
||||||
|
throw new Error('secrets array must contain objects that conform to the Secret interface');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
// case: update 1 secret
|
||||||
|
if (
|
||||||
|
!value.type ||
|
||||||
|
!(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) ||
|
||||||
|
!value.secretKeyCiphertext ||
|
||||||
|
!value.secretKeyIV ||
|
||||||
|
!value.secretKeyTag ||
|
||||||
|
!value.secretValueCiphertext ||
|
||||||
|
!value.secretValueIV ||
|
||||||
|
!value.secretValueTag
|
||||||
|
) {
|
||||||
|
throw new Error('secrets array must contain objects that conform to the Secret interface');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('secrets must be an object or an array of objects')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
location: 'body'
|
||||||
|
}),
|
||||||
|
secretsController.createSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
query('workspaceId').exists().trim(),
|
||||||
|
query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
location: 'query'
|
||||||
|
}),
|
||||||
|
secretsController.getSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/',
|
||||||
|
body('workspaceId').exists().trim(),
|
||||||
|
body('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
||||||
|
body('secrets')
|
||||||
|
.exists()
|
||||||
|
.custom((value) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// case: update multiple secrets
|
||||||
|
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
||||||
|
for (const secret of value) {
|
||||||
|
if (
|
||||||
|
!secret.id ||
|
||||||
|
!secret.secretKeyCiphertext ||
|
||||||
|
!secret.secretKeyIV ||
|
||||||
|
!secret.secretKeyTag ||
|
||||||
|
!secret.secretValueCiphertext ||
|
||||||
|
!secret.secretValueIV ||
|
||||||
|
!secret.secretValueTag
|
||||||
|
) {
|
||||||
|
throw new Error('secrets array must contain objects that conform to the Secret interface');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
// case: update 1 secret
|
||||||
|
if (
|
||||||
|
!value.id ||
|
||||||
|
!value.secretKeyCiphertext ||
|
||||||
|
!value.secretKeyIV ||
|
||||||
|
!value.secretKeyTag ||
|
||||||
|
!value.secretValueCiphertext ||
|
||||||
|
!value.secretValueIV ||
|
||||||
|
!value.secretValueTag
|
||||||
|
) {
|
||||||
|
throw new Error('secrets array must contain objects that conform to the Secret interface');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('secrets must be an object or an array of objects')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
location: 'body'
|
||||||
|
}),
|
||||||
|
requireSecretsAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
secretsController.updateSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/',
|
||||||
|
[
|
||||||
|
check('secretIds')
|
||||||
|
.exists()
|
||||||
|
.custom((value) => {
|
||||||
|
// case: delete 1 secret
|
||||||
|
if (typeof value === 'string') return true;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// case: delete multiple secrets
|
||||||
|
if (value.length === 0) throw new Error('secrets cannot be an empty array');
|
||||||
|
return value.every((id: string) => typeof id === 'string')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('secretIds must be a string or an array of strings');
|
||||||
|
})
|
||||||
|
.not()
|
||||||
|
.isEmpty()
|
||||||
|
],
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireSecretsAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
secretsController.deleteSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Vendored
+3
-1
@@ -1,4 +1,5 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
import { ISecret } from '../../models';
|
||||||
|
|
||||||
// TODO: fix (any) types
|
// TODO: fix (any) types
|
||||||
declare global {
|
declare global {
|
||||||
@@ -12,7 +13,8 @@ declare global {
|
|||||||
integration: any;
|
integration: any;
|
||||||
integrationAuth: any;
|
integrationAuth: any;
|
||||||
bot: any;
|
bot: any;
|
||||||
secret: any;
|
_secret: any;
|
||||||
|
secrets: any;
|
||||||
secretSnapshot: any;
|
secretSnapshot: any;
|
||||||
serviceToken: any;
|
serviceToken: any;
|
||||||
accessToken: any;
|
accessToken: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user