Compare commits

..

94 Commits

Author SHA1 Message Date
1c3488f8db add reset infisical docs 2023-02-10 17:41:31 -08:00
20e536cec0 Remove printing pathToDir 2023-02-10 17:25:01 -08:00
e8b498ca6d Minor style tweaks 2023-02-10 16:45:31 -08:00
b82f8606a8 add ValidateEnvironmentName method 2023-02-10 15:08:12 -08:00
ab27fbccf7 add reset command 2023-02-10 14:19:04 -08:00
d50de9366b Add docs for generate-example-env command 2023-02-10 12:29:47 -08:00
4c56bca4e7 Remove newline after heading in .sample-env 2023-02-10 12:24:29 -08:00
a60774a3f4 Merge pull request #327 from Infisical/parameter-store
Add support and docs for AWS parameter store and secret manager
2023-02-11 01:52:29 +07:00
03426ee7f2 Fix lint errors 2023-02-11 01:49:53 +07:00
428022d1a2 Add support and docs for AWS parameter store and secret manager 2023-02-11 01:40:18 +07:00
081ef94399 hard code site url frontend 2023-02-09 22:49:58 -08:00
eebde3ad12 Updated env variables and emails 2023-02-09 22:27:30 -08:00
6ab6147ac8 Fixed service token bug 2023-02-09 13:40:33 -08:00
dd7e8d254b Merge branch 'main' of https://github.com/Infisical/infisical 2023-02-09 18:24:23 +07:00
2765f7e488 Fix Vercel get apps response encoding 2023-02-09 18:24:10 +07:00
2d3a276dc2 Merge pull request #309 from RashidUjang/fix/issue-308-sidebar-issue
fix: handle duplicate edge case for sidebar loading
2023-02-08 23:50:41 -08:00
55eddee6ce Returned back @RashidUjang's change with secretIds 2023-02-08 23:48:25 -08:00
ab751d0db3 Merge branch 'main' into fix/issue-308-sidebar-issue 2023-02-08 23:42:46 -08:00
b2bd0ba340 Merge branch 'main' of https://github.com/Infisical/infisical 2023-02-08 23:38:25 -08:00
224fa25fdf Minor style fixes 2023-02-08 23:38:00 -08:00
6115a311ad Merge pull request #317 from Infisical/gen-example-env-command
generate example .env file command
2023-02-08 18:51:23 -08:00
a685ac3e73 update regex to capature comment 2023-02-08 18:48:45 -08:00
9a22975732 When comments are empty, return empty byte 2023-02-08 17:29:35 -08:00
cd0b2e3a26 Change default secret comments 2023-02-08 14:36:56 -08:00
80a3c196ae Fixed errors with undefined tags 2023-02-08 14:32:57 -08:00
b0c541f8dc generate example .env file command 2023-02-08 13:46:57 -08:00
6188b04544 Switch azure integration off 2023-02-08 13:53:12 +07:00
8ba4f964d4 Switch Azure KV integration on 2023-02-08 13:42:49 +07:00
0d2caddb12 Merge pull request #292 from HasanMansoor4/auto-capitalization-toggle
Auto capitalization toggle for secrets
2023-02-07 21:55:05 -08:00
4570c35658 Merge pull request #313 from Infisical/debug-new-integrations
Fix more encoding issues with integrations
2023-02-08 12:38:49 +07:00
72f7d81b80 Fix more encoding issues with integrations 2023-02-08 12:38:15 +07:00
231fa61805 Merge branch 'main' into auto-capitalization-toggle 2023-02-07 21:32:29 -08:00
9f74affd3a Merge pull request #300 from kanhaiya38/feat/merge-env
feat(ui): allow user to merge secrets while uploading file
2023-02-07 21:29:38 -08:00
f58e1e1d6c Minor style changes 2023-02-07 21:27:21 -08:00
074cf695b2 Merge branch 'main' into feat/merge-env 2023-02-07 19:57:50 -08:00
65eb037020 Merge branch 'main' into auto-capitalization-toggle 2023-02-08 05:23:41 +03:00
c84add0a2a Merge pull request #312 from Infisical/secret-tagging
Added tags to secrets in the dashboard
2023-02-07 16:57:01 -08:00
ace0e9c56f Fixed the bug of wrong data structure 2023-02-07 16:54:13 -08:00
498705f330 Fixed the login error with tags 2023-02-07 16:47:05 -08:00
7892624709 Added tags to secrets in the dashboard 2023-02-07 16:29:15 -08:00
d8889beaf7 mark gitlab as complete 2023-02-07 12:58:39 -08:00
6e67304e92 Update wording of k8 2023-02-07 12:54:09 -08:00
8b23e89a64 add k8 diagram 2023-02-07 12:38:58 -08:00
7611b999fe Merge pull request #311 from Infisical/debug-new-integrations
Patch encoding header issue for some integrations for getting their apps
2023-02-08 01:30:02 +07:00
aba8feb985 Patch encoding header issue for some integrations for getting their apps 2023-02-08 01:28:46 +07:00
747cc1134c Merge pull request #310 from Infisical/refactor-integration-pages
Refactor integration pages into separate steps for authorization and integration creation.
2023-02-07 23:29:42 +07:00
db05412865 Fix incorrect imports, build errors 2023-02-07 23:27:21 +07:00
679b1d9c23 Move existing integration authorization and creation into separate steps 2023-02-07 23:10:31 +07:00
a37cf91702 fix: handle duplicate edge case for sidebar loading
This changes the SideBar's data prop to be filtered by id instead of key.

fixes issue #308
2023-02-07 21:35:13 +08:00
5ea5887146 Begin refactoring all integrations to separate integration pages by step 2023-02-07 11:48:17 +07:00
13838861fb Merge pull request #305 from Infisical/azure
Finish v1 Azure Key Vault integration
2023-02-06 18:15:57 +07:00
09c60322db Merge branch 'main' into azure 2023-02-06 18:15:44 +07:00
68bf0b9efe Finish v1 Azure Key Vault integration 2023-02-06 17:57:47 +07:00
3ec68daf2e Merge branch 'main' into auto-capitalization-toggle 2023-02-06 11:17:08 +03:00
9fafe02e16 Merge branch 'main' into feat/merge-env 2023-02-05 23:16:19 -08:00
56da34d343 Merge pull request #304 from Infisical/secret-tagging
Revamped the dashboard look
2023-02-05 20:36:49 -08:00
086dd621b5 Revamped the dashabord look 2023-02-05 20:29:27 -08:00
56a14925da Add githlab to integ overview 2023-02-05 19:23:52 -08:00
c13cb23942 Add gitlab integ docs 2023-02-05 19:21:07 -08:00
31df4a26fa Update cli docs to be more clear and consistent 2023-02-05 16:05:34 -08:00
9f9273bb02 Add tags support for secrets 2023-02-05 12:54:42 -08:00
86fd876850 change api from post to patch, fix spelling mistakes 2023-02-05 20:51:53 +03:00
b56d9287e4 feat(ui): allow user to merge secrets while uploading file 2023-02-05 18:07:54 +05:30
a35e235744 remove console log 2023-02-05 06:25:40 +03:00
77a44b4490 Refactor into component and use React Query 2023-02-05 06:21:58 +03:00
594f846943 Merge remote-tracking branch 'origin/main' into auto-capitalization-toggle 2023-02-05 03:19:06 +03:00
8ae43cdcf6 Merge pull request #296 from akhilmhdh/fix/ws-redirect
feat(ui): removed workspace context redirect and added redirect when ws is deleted
2023-02-04 10:50:23 -08:00
1d72d310e5 Add offline support to faq 2023-02-04 08:48:01 -08:00
e72e6cf2b7 feat(ui): removed workspace context redirect and added redirect when project is deleted 2023-02-04 14:24:10 +05:30
0ac40acc40 Merge pull request #295 from mocherfaoui/inf-compare-secrets
add new modal to compare secrets across environments
2023-02-03 23:55:17 -08:00
56710657bd Minor styling updates 2023-02-03 23:49:03 -08:00
92f4979715 Merge branch 'main' into inf-compare-secrets 2023-02-03 21:24:24 -08:00
1e9118df33 delete backup secrets when new user login 2023-02-03 21:14:56 -08:00
e16c0e53ff Add offline secrets fetch feature 2023-02-03 21:02:36 -08:00
0d57a26925 Add token flag to export command 2023-02-03 21:02:36 -08:00
1bd180596e Merge pull request #294 from akhilmhdh/feat/new-settings-page
New Project Settings Page
2023-02-03 20:11:30 -08:00
fca003dfd7 Minor typos fixed and style changes 2023-02-03 20:09:28 -08:00
f1ef23874c Add token flag to read secrets via service token 2023-02-03 16:55:40 -08:00
16883cf168 make some params optional 2023-02-03 22:34:18 +01:00
1781b71399 add new modal to compare secrets across environments 2023-02-03 22:33:39 +01:00
fb62fa4d32 feat(ui): updated select design due to rebase changes 2023-02-03 22:59:01 +05:30
ed148a542d feat(ui): implemented the new project settings page 2023-02-03 22:22:51 +05:30
a4f7843727 feat(ui): global workspace and subscription context 2023-02-03 22:21:14 +05:30
48cd84ce77 feat(ui): fine tuning components library with exiting app design 2023-02-03 22:21:09 +05:30
3859a7e09b feat(ui): added new react-query hooks for settings page 2023-02-03 22:20:04 +05:30
76d0127029 Add docs for PM2 integration, update Docker/Docker-Compose integration docs 2023-02-03 15:50:26 +07:00
a94cd8c85c Merge pull request #293 from Infisical/ip-address
Ip address
2023-02-03 12:49:32 +07:00
ee555f3f15 Rename loginSRPDetail file 2023-02-03 12:46:14 +07:00
bd230a8b7d Remove comment from loginSRPDetail 2023-02-03 12:40:35 +07:00
a4926d8833 Add back requestIp middleware 2023-02-03 12:32:54 +07:00
7560d2f673 Merge remote-tracking branch 'origin' into ip-address 2023-02-03 11:02:07 +07:00
75cd7a0f15 integrate frontend with backend for auto capitalization setting 2023-02-02 05:30:22 +03:00
4722bb8fcd add auto capitalization api controllers and routes with mongo schema updated 2023-02-02 05:27:07 +03:00
ca688764a3 Add login/logout logs 2023-02-01 11:56:38 +07:00
238 changed files with 11432 additions and 1879 deletions

View File

@ -171,7 +171,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td>
<td align="left" valign="middle">
🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
<a href="https://infisical.com/docs/integrations/cicd/gitlab">
✔️ GitLab CI/CD
</a>
</td>
<td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)

3408
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,18 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.267.0",
"@godaddy/terminus": "^4.11.2",
"@sentry/node": "^7.21.1",
"@sentry/tracing": "^7.21.1",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"axios": "^1.2.0",
"@types/libsodium-wrappers": "^0.7.10",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0",
"axios": "^1.1.3",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
@ -15,17 +22,26 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"jsonwebtoken": "^8.5.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"mongoose": "^6.7.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.1.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3"
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"name": "infisical-api",
"version": "1.0.0",
@ -102,47 +118,5 @@
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
},
"dependencies": {
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0",
"@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",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.2.2",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3",
"utility-types": "^3.10.0",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
}
}

View File

@ -50,6 +50,7 @@ import {
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import { healthCheck } from './routes/status';
@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);

View File

@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_ID_AZURE = process.env.CLIENT_ID_AZURE!;
const TENANT_ID_AZURE = process.env.TENANT_ID_AZURE!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_AZURE = process.env.CLIENT_SECRET_AZURE!;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
@ -60,10 +63,13 @@ export {
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_AZURE,
TENANT_ID_AZURE,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,

View File

@ -4,16 +4,21 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User } from '../../models';
import { User, LoginSRPDetail } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import {
ACTION_LOGIN,
ACTION_LOGOUT
} from '../../variables';
import {
NODE_ENV,
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_SECRET
} from '../../config';
import LoginSRPDetail from '../../models/LoginSRPDetail';
import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -116,6 +121,18 @@ export const login2 = async (req: Request, res: Response) => {
secure: NODE_ENV === 'production' ? true : false
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
@ -159,6 +176,19 @@ export const logout = async (req: Request, res: Response) => {
sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);

View File

@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
/***
* Return integration authorization with id [integrationAuthId]
*/
export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth;
try {
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
return res.status(200).send({
integrationAuth
});
}
export const getIntegrationOptions = async (
req: Request,
res: Response
@ -31,7 +56,6 @@ export const oAuthExchange = async (
) => {
try {
const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration');
@ -40,12 +64,16 @@ export const oAuthExchange = async (
throw new Error("Failed to get environments")
}
await IntegrationService.handleOAuthExchange({
const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
return res.status(200).send({
integrationAuth
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@ -53,14 +81,11 @@ export const oAuthExchange = async (
message: 'Failed to get OAuth2 code-token exchange'
});
}
return res.status(200).send({
message: 'Successfully enabled integration authorization'
});
};
/**
* Save integration access token as part of integration [integration] for workspace with id [workspaceId]
* Save integration access token and (optionally) access id as part of integration
* [integration] for workspace with id [workspaceId]
* @param req
* @param res
*/
@ -69,18 +94,29 @@ export const saveIntegrationAccessToken = async (
res: Response
) => {
// TODO: refactor
// TODO: check if access token is valid for each integration
let integrationAuth;
try {
const {
workspaceId,
accessId,
accessToken,
integration
}: {
workspaceId: string;
accessId: string | null;
accessToken: string;
integration: string;
} = req.body;
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId),
integration
@ -91,17 +127,11 @@ export const saveIntegrationAccessToken = async (
new: true,
upsert: true
});
const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId),
isActive: true
});
if (!bot) throw new Error('Bot must be enabled to save integration access token');
// encrypt and save integration access token
// encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken,
accessExpiresAt: undefined
});

View File

@ -1,4 +1,5 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Integration,
@ -18,15 +19,44 @@ import { eventPushSecrets } from '../../events';
export const createIntegration = async (req: Request, res: Response) => {
let integration;
try {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
isActive: false,
app: null,
environment: req.integrationAuth.workspace?.environments[0].slug,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: req.integrationAuth._id
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);

View File

@ -77,8 +77,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id
// [membershipOrgId]
// TODO
let membershipToChangeRole;
// try {
// } catch (err) {

View File

@ -4,12 +4,11 @@ import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, Token, BackupPrivateKey } from '../../models';
import { User, Token, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import LoginSRPDetail from '../../models/LoginSRPDetail';
import { BadRequestError } from '../../utils/errors';
/**

View File

@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
import * as tagController from './tagController';
export {
usersController,
@ -15,5 +16,6 @@ export {
apiKeyDataController,
secretController,
secretsController,
environmentController
environmentController,
tagController
}

View File

@ -79,24 +79,38 @@ export const createSecrets = async (req: Request, res: Response) => {
*/
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment } = req.body;
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
let toAdd;
let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets
toAdd = req.body.secrets;
listOfSecretsToCreate = req.body.secrets;
} else if (typeof req.body.secrets === 'object') {
// case: create 1 secret
toAdd = [req.body.secrets];
listOfSecretsToCreate = [req.body.secrets];
}
const newSecrets = await Secret.insertMany(
toAdd.map(({
type secretsToCreateType = {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[]
}
const newlyCreatedSecrets = await Secret.insertMany(
listOfSecretsToCreate.map(({
type,
secretKeyCiphertext,
secretKeyIV,
@ -104,15 +118,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
}: {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}: secretsToCreateType) => {
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
@ -124,7 +134,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
});
})
);
@ -140,7 +154,7 @@ export const createSecrets = async (req: Request, res: Response) => {
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({
secretVersions: newlyCreatedSecrets.map(({
_id,
version,
workspace,
@ -154,7 +168,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}) => ({
_id: new Types.ObjectId(),
secret: _id,
@ -171,21 +189,25 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}))
});
const addAction = await EELogService.createActionSecret({
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id.toString(),
workspaceId,
secretIds: newSecrets.map((n) => n._id)
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id)
});
// (EE) create (audit) log
addAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId,
workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
ipAddress: req.ip
@ -201,7 +223,7 @@ export const createSecrets = async (req: Request, res: Response) => {
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: toAdd.length,
numberOfSecrets: listOfSecretsToCreate.length,
environment,
workspaceId,
channel: channel,
@ -211,7 +233,7 @@ export const createSecrets = async (req: Request, res: Response) => {
}
return res.status(200).send({
secrets: newSecrets
secrets: newlyCreatedSecrets
});
}
@ -294,22 +316,22 @@ export const getSecrets = async (req: Request, res: Response) => {
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
).then())
).populate("tags").then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createActionSecret({
const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
userId: userId,
workspaceId: workspaceId as string,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: userId,
workspaceId: workspaceId as string,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip
@ -398,6 +420,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[]
}
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
@ -410,7 +433,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
secretCommentTag,
tags
} = secret;
return ({
@ -426,8 +450,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
...((
secretCommentCiphertext &&
secretCommentCiphertext !== undefined &&
secretCommentIV &&
secretCommentTag
) ? {
@ -460,6 +485,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
} = secretModificationsBySecretId[secret._id.toString()]
return ({
@ -477,6 +503,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
tags: tags ? tags : secret.tags
});
})
}
@ -505,17 +532,17 @@ export const updateSecrets = async (req: Request, res: Response) => {
});
}, 10000);
const updateAction = await EELogService.createActionSecret({
const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId: req.user._id.toString(),
workspaceId: key,
userId: req.user._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
updateAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: key,
workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
ipAddress: req.ip
@ -631,17 +658,17 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key
})
});
const deleteAction = await EELogService.createActionSecret({
const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user._id.toString(),
workspaceId: key,
userId: req.user._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
deleteAction && await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: key,
workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
ipAddress: req.ip

View File

@ -0,0 +1,66 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
} from '../../models';
import Tag, { ITag } from '../../models/tag';
import { Builder } from "builder-pattern"
import to from 'await-to-js';
import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
import { MongoError } from 'mongodb';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
export const createWorkspaceTag = async (req: Request, res: Response) => {
const { workspaceId } = req.params
const { name, slug } = req.body
const sanitizedTagToCreate = Builder<ITag>()
.name(name)
.workspace(new Types.ObjectId(workspaceId))
.slug(slug)
.user(new Types.ObjectId(req.user._id))
.build();
const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate))
if (err) {
if ((err as MongoError).code === 11000) {
throw BadRequestError({ message: "Tags must be unique in a workspace" })
}
throw err
}
res.json(createdTag)
}
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const { tagId } = req.params
const tagFromDB = await Tag.findById(tagId)
if (!tagFromDB) {
throw BadRequestError()
}
// can only delete if the request user is one that belongs to the same workspace as the tag
const membership = await Membership.findOne({
user: req.user,
workspace: tagFromDB.workspace
});
if (!membership) {
UnauthorizedRequestError({ message: 'Failed to validate membership' });
}
const result = await Tag.findByIdAndDelete(tagId);
res.json(result);
}
export const getWorkspaceTags = async (req: Request, res: Response) => {
const { workspaceId } = req.params
const workspaceTags = await Tag.find({ workspace: workspaceId })
return res.json({
workspaceTags
})
}

View File

@ -467,4 +467,42 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
return res.status(200).send({
membership
});
}
}
/**
* Change autoCapitilzation Rule of workspace
* @param req
* @param res
* @returns
*/
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
autoCapitalization
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change autoCapitalization setting'
});
}
return res.status(200).send({
message: 'Successfully changed autoCapitalization setting',
workspace
});
};

View File

@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate('secretVersions');
.populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');

View File

@ -1,39 +1,40 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { SecretVersion, Action } from '../models';
import { Action } from '../models';
import {
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
} from '../helpers/secretVersion';
import { ACTION_UPDATE_SECRETS } from '../../variables';
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
} from '../../variables';
/**
* Create an (audit) action for secrets including
* add, delete, update, and read actions.
* Create an (audit) action for updating secrets
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
* @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
* @returns {Action} action - new action
*/
const createActionSecretHelper = async ({
const createActionUpdateSecret = async ({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: string;
workspaceId: string;
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
let action;
let latestSecretVersions;
try {
if (name === ACTION_UPDATE_SECRETS) {
// case: action is updating secrets
// -> add old and new secret versions
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
@ -41,17 +42,7 @@ const createActionSecretHelper = async ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
} else {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
}
action = await new Action({
name,
user: userId,
@ -64,10 +55,148 @@ const createActionSecretHelper = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create update secret action');
}
return action;
}
/**
* Create an (audit) action for creating, reading, and deleting
* secrets
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
* @returns {Action} action - new action
*/
const createActionSecret = async ({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
let action;
try {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
action = await new Action({
name,
user: userId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action create/read/delete secret action');
}
return action;
}
/**
* Create an (audit) action for user with id [userId]
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action
* @returns
*/
const createActionUser = ({
name,
userId
}: {
name: string;
userId: Types.ObjectId;
}) => {
let action;
try {
action = new Action({
name,
user: userId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create user action');
}
return action;
}
/**
* Create an (audit) action.
* @param {Object} obj
* @param {Object} obj.name - name of action
* @param {Types.ObjectId} obj.userId - id of user associated with action
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with action
* @param {Types.ObjectId[]} obj.secretIds - ids of secrets associated with action
*/
const createActionHelper = async ({
name,
userId,
workspaceId,
secretIds,
}: {
name: string;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) => {
let action;
try {
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionUser({
name,
userId
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
}
return action;
}
export { createActionSecretHelper };
export {
createActionHelper
};

View File

@ -1,9 +1,19 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Log,
IAction
} from '../models';
/**
* Create an (audit) log
* @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user associated with the log
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with the log
* @param {IAction[]} obj.actions - actions to include in log
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
* @param {String} obj.ipAddress - ip address associated with the log
* @returns {Log} log - new audit log
*/
const createLogHelper = async ({
userId,
workspaceId,
@ -11,8 +21,8 @@ const createLogHelper = async ({
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
ipAddress: string;
@ -21,7 +31,7 @@ const createLogHelper = async ({
try {
log = await new Log({
user: userId,
workspace: workspaceId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,
channel,

View File

@ -1,10 +1,18 @@
import { Schema, model, Types } from 'mongoose';
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
export interface IAction {
name: string;
user?: Types.ObjectId,
workspace?: Types.ObjectId,
payload: {
payload?: {
secretVersions?: Types.ObjectId[]
}
}
@ -13,7 +21,15 @@ const actionSchema = new Schema<IAction>(
{
name: {
type: String,
required: true
required: true,
enum: [
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
]
},
user: {
type: Schema.Types.ObjectId,

View File

@ -1,5 +1,7 @@
import { Schema, model, Types } from 'mongoose';
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
@ -29,6 +31,8 @@ const logSchema = new Schema<ILog>(
actionNames: {
type: [String],
enum: [
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,

View File

@ -21,6 +21,7 @@ export interface ISecretVersion {
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
tags?: string[];
}
const secretVersionSchema = new Schema<ISecretVersion>(
@ -88,7 +89,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
},
secretValueHash: {
type: String
}
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
},
{
timestamps: true

View File

@ -1,14 +1,12 @@
import { Types } from 'mongoose';
import {
Log,
Action,
IAction
} from '../models';
import {
createLogHelper
} from '../helpers/log';
import {
createActionSecretHelper
createActionHelper
} from '../helpers/action';
import EELicenseService from './EELicenseService';
@ -33,8 +31,8 @@ class EELogService {
channel,
ipAddress
}: {
userId: string;
workspaceId: string;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
ipAddress: string;
@ -50,26 +48,26 @@ class EELogService {
}
/**
* Create an (audit) action for secrets including
* add, delete, update, and read actions.
* Create an (audit) action
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {ObjectId[]} obj.secretIds - secret ids
* @param {Types.ObjectId} obj.userId - id of user associated with the action
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with the action
* @param {ObjectId[]} obj.secretIds - ids of secrets associated with the action
* @returns {Action} action - new action
*/
static async createActionSecret({
static async createAction({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: string;
workspaceId: string;
secretIds: Types.ObjectId[];
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) {
if (!EELicenseService.isLicenseValid) return null;
return await createActionSecretHelper({
return await createActionHelper({
name,
userId,
workspaceId,

View File

@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
workspaceId,
@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
code: string;
environment: string;
}) => {
let action;
let integrationAuth;
try {
const bot = await Bot.findOne({
@ -94,25 +94,18 @@ const handleOAuthExchangeHelper = async ({
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
// initialize new integration after exchange
await new Integration({
workspace: workspaceId,
isActive: false,
app: null,
environment,
integration,
integrationAuth: integrationAuth._id
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
return integrationAuth;
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to
@ -146,7 +139,7 @@ const syncIntegrationsHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token
const accessToken = await getIntegrationAuthAccessHelper({
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString()
});
@ -155,7 +148,8 @@ const syncIntegrationsHelper = async ({
integration,
integrationAuth,
secrets,
accessToken
accessId: access.accessId,
accessToken: access.accessToken
});
}
} catch (err) {
@ -211,12 +205,12 @@ const syncIntegrationsHelper = async ({
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessId;
let accessToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
@ -240,6 +234,15 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
});
}
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
});
}
} catch (err) {
Sentry.setUser(null);
@ -250,7 +253,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
throw new Error('Failed to get integration access token');
}
return accessToken;
return ({
accessId,
accessToken
});
}
/**
@ -300,9 +306,9 @@ const setIntegrationAuthRefreshHelper = async ({
}
/**
* Encrypt access token [accessToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId] and store it along with [accessExpiresAt]
* Encrypt access token [accessToken] and (optionally) access id [accessId]
* using the bot's copy of the workspace key for workspace belonging to
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessToken - access token
@ -310,10 +316,12 @@ const setIntegrationAuthRefreshHelper = async ({
*/
const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
}) => {
@ -323,17 +331,28 @@ const setIntegrationAuthAccessHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessCiphertext: obj.ciphertext,
accessIV: obj.iv,
accessTag: obj.tag,
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt
}, {
new: true

View File

@ -406,10 +406,10 @@ const v2PushSecrets = async ({
secretIds: toDelete
});
const deleteAction = await EELogService.createActionSecret({
const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId,
workspaceId,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(userId),
secretIds: toDelete
});
@ -499,10 +499,10 @@ const v2PushSecrets = async ({
})
});
const updateAction = await EELogService.createActionSecret({
const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId,
workspaceId,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId),
secretIds: toUpdate.map((u) => u._id)
});
@ -536,10 +536,10 @@ const v2PushSecrets = async ({
})
});
const addAction = await EELogService.createActionSecret({
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId,
workspaceId,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId),
secretIds: newSecrets.map((n) => n._id)
});
addAction && actions.push(addAction);
@ -553,8 +553,8 @@ const v2PushSecrets = async ({
// (EE) create (audit) log
if (actions.length > 0) {
await EELogService.createLog({
userId,
workspaceId,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId),
actions,
channel,
ipAddress
@ -645,16 +645,16 @@ const pullSecrets = async ({
environment
})
const readAction = await EELogService.createActionSecret({
const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
userId,
workspaceId,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId,
workspaceId,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId),
actions: [readAction],
channel,
ipAddress

View File

@ -3,6 +3,9 @@ import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -40,6 +43,15 @@ const getApps = async ({
let apps: App[];
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = [];
break;
case INTEGRATION_AWS_PARAMETER_STORE:
apps = [];
break;
case INTEGRATION_AWS_SECRET_MANAGER:
apps = [];
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken
@ -131,7 +143,8 @@ const getAppsVercel = async ({
const res = (
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...( integrationAuth?.teamId ? {
params: {
@ -140,7 +153,7 @@ const getAppsVercel = async ({
} : {})
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name
}));
@ -170,7 +183,8 @@ const getAppsNetlify = async ({
const res = (
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
})
).data;
@ -247,7 +261,9 @@ const getAppsRender = async ({
const res = (
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
}
})
).data;
@ -257,6 +273,7 @@ const getAppsRender = async ({
name: a.service.name,
appId: a.service.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -296,7 +313,9 @@ const getAppsFlyio = async ({
url: INTEGRATION_FLYIO_API_URL,
method: 'post',
headers: {
'Authorization': 'Bearer ' + accessToken
'Authorization': 'Bearer ' + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
},
data: {
query,

View File

@ -1,10 +1,12 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@ -12,15 +14,27 @@ import {
} from '../variables';
import {
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
}
interface ExchangeCodeHerokuResponse {
token_type: string;
access_token: string;
@ -75,6 +89,11 @@ const exchangeCode = async ({
try {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code
});
break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
@ -105,6 +124,46 @@ const exchangeCode = async ({
return obj;
};
/**
* Return [accessToken] for Azure OAuth2 code-token exchange
* @param param0
*/
const exchangeCodeAzure = async ({
code
}: {
code: string;
}) => {
const accessExpiresAt = new Date();
let res: ExchangeCodeAzureResponse;
try {
res = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
scope: 'https://vault.azure.net/.default openid offline_access',
client_id: CLIENT_ID_AZURE,
client_secret: CLIENT_SECRET_AZURE,
redirect_uri: `${SITE_URL}/integrations/azure-key-vault/oauth2/callback`
} as any)
)).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err: any) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
* OAuth2 code-token exchange
@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel`
redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code,
client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify`
redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${SITE_URL}/github`
redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
},
headers: {
Accept: 'application/json'
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
}
})
).data;

View File

@ -1,13 +1,26 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import { INTEGRATION_HEROKU } from '../variables';
import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import {
CLIENT_SECRET_HEROKU
SITE_URL,
CLIENT_ID_AZURE,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU
} from '../config';
import {
INTEGRATION_HEROKU_TOKEN_URL
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL
} from '../variables';
interface RefreshTokenAzureResponse {
token_type: string;
scope: string;
expires_in: number;
ext_expires_in: 4871;
access_token: string;
refresh_token: string;
}
/**
* Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration]
@ -25,6 +38,11 @@ const exchangeRefresh = async ({
let accessToken;
try {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
accessToken = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({
refreshToken
@ -40,6 +58,38 @@ const exchangeRefresh = async ({
return accessToken;
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Azure integration
* @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for Azure
* @returns
*/
const exchangeRefreshAzure = async ({
refreshToken
}: {
refreshToken: string;
}) => {
try {
const res: RefreshTokenAzureResponse = (await axios.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: CLIENT_ID_AZURE,
scope: 'openid offline_access',
refresh_token: refreshToken,
grant_type: 'refresh_token',
client_secret: CLIENT_SECRET_AZURE
} as any)
)).data;
return res.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get refresh OAuth2 access token for Azure');
}
}
/**
* Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration
@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
}: {
refreshToken: string;
}) => {
let accessToken;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
let accessToken;
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU
} as any)
);
accessToken = res.data.access_token;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku');
throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
return accessToken;

View File

@ -1,11 +1,21 @@
import axios from 'axios';
import * as Sentry from '@sentry/node';
import _ from 'lodash';
import AWS from 'aws-sdk';
import {
SecretsManagerClient,
UpdateSecretCommand,
CreateSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException
} from '@aws-sdk/client-secrets-manager';
import { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -18,7 +28,6 @@ import {
INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL
} from '../variables';
import { access, appendFile } from 'fs';
/**
* Sync/push [secrets] to [app] in integration named [integration]
@ -26,21 +35,47 @@ import { access, appendFile } from 'fs';
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for integration
* @param {String} obj.accessToken - access token for integration
*/
const syncSecrets = async ({
integration,
integrationAuth,
secrets,
accessId,
accessToken
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
try {
switch (integration.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
await syncSecretsAzureKeyVault({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_AWS_PARAMETER_STORE:
await syncSecretsAWSParameterStore({
integration,
secrets,
accessId,
accessToken
});
break;
case INTEGRATION_AWS_SECRET_MANAGER:
await syncSecretsAWSSecretManager({
integration,
secrets,
accessId,
accessToken
});
break;
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
@ -93,6 +128,333 @@ const syncSecrets = async ({
}
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Azure Key Vault integration
*/
const syncSecretsAzureKeyVault = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
attributes: {
enabled: true,
created: number;
updated: number;
recoveryLevel: string;
recoverableDays: number;
}
}
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
key: string;
}
/**
* Return all secrets from Azure Key Vault by paginating through URL [url]
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
* @returns
*/
const paginateAzureKeyVaultSecrets = async (url: string) => {
let result: GetAzureKeyVaultSecret[] = [];
while (url) {
const res = await axios.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
result = result.concat(res.data.value);
url = res.data.nextLink;
}
return result;
}
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
let lastSlashIndex: number;
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
if (!lastSlashIndex) {
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
}
const azureKeyVaultSecret = await axios.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return ({
...azureKeyVaultSecret.data,
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
});
})))
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
const setSecrets: {
key: string;
value: string;
}[] = [];
Object.keys(secrets).forEach((key) => {
const hyphenatedKey = key.replace(/_/g, '-');
if (!(hyphenatedKey in res)) {
// case: secret has been created
setSecrets.push({
key: hyphenatedKey,
value: secrets[key]
});
} else {
if (secrets[key] !== res[hyphenatedKey].value) {
// case: secret has been updated
setSecrets.push({
key: hyphenatedKey,
value: secrets[key]
});
}
}
});
const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, '_');
if (!(underscoredKey in secrets)) {
deleteSecrets.push(res[key]);
}
});
// Sync/push set secrets
if (setSecrets.length > 0) {
setSecrets.forEach(async ({ key, value }) => {
await axios.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value
},
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret) => {
await axios.delete(`${integration.app}/secrets/${secret.key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Azure Key Vault');
}
};
/**
* Sync/push [secrets] to AWS parameter store
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS parameter store integration
* @param {String} obj.accessToken - access token for AWS parameter store integration
*/
const syncSecretsAWSParameterStore = async ({
integration,
secrets,
accessId,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
try {
if (!accessId) return;
AWS.config.update({
region: integration.region,
accessKeyId: accessId,
secretAccessKey: accessToken
});
const ssm = new AWS.SSM({
apiVersion: '2014-11-06',
region: integration.region
});
const params = {
Path: integration.path,
Recursive: true,
WithDecryption: true
};
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters
let awsParameterStoreSecretsObj: {
[key: string]: any // TODO: fix type
} = {};
if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({
...obj,
[secret.Name.split("/").pop()]: secret
}), {});
}
// Identify secrets to create
Object.keys(secrets).map(async (key) => {
if (!(key in awsParameterStoreSecretsObj)) {
// case: secret does not exist in AWS parameter store
// -> create secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: 'SecureString',
Value: secrets[key],
Overwrite: true
}).promise();
} else {
// case: secret exists in AWS parameter store
if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) {
// case: secret value doesn't match one in AWS parameter store
// -> update secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: 'SecureString',
Value: secrets[key],
Overwrite: true
}).promise();
}
}
});
// Identify secrets to delete
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
if (!(key in secrets)) {
// case:
// -> delete secret
await ssm.deleteParameter({
Name: awsParameterStoreSecretsObj[key].Name
}).promise();
}
});
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to AWS Parameter Store');
}
}
/**
* Sync/push [secrets] to AWS secret manager
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS secret manager integration
* @param {String} obj.accessToken - access token for AWS secret manager integration
*/
const syncSecretsAWSSecretManager = async ({
integration,
secrets,
accessId,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
let secretsManager;
try {
if (!accessId) return;
AWS.config.update({
region: integration.region,
accessKeyId: accessId,
secretAccessKey: accessToken
});
secretsManager = new SecretsManagerClient({
region: integration.region,
credentials: {
accessKeyId: accessId,
secretAccessKey: accessToken
}
});
const awsSecretManagerSecret = await secretsManager.send(
new GetSecretValueCommand({
SecretId: integration.app
})
);
let awsSecretManagerSecretObj: { [key: string]: any } = {};
if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
}
if (!_.isEqual(awsSecretManagerSecretObj, secrets)) {
await secretsManager.send(new UpdateSecretCommand({
SecretId: integration.app,
SecretString: JSON.stringify(secrets)
}));
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) {
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(new CreateSecretCommand({
Name: integration.app,
SecretString: JSON.stringify(secrets)
}));
} else {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to AWS Secret Manager');
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
}
}
/**
* Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj
@ -205,7 +567,7 @@ const syncSecretsVercel = async ({
...obj,
[secret.key]: secret
}), {});
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
@ -736,8 +1098,9 @@ const syncSecretsFlyio = async ({
method: 'post',
url: INTEGRATION_FLYIO_API_URL,
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
data: {
query: GetSecrets,

View File

@ -45,9 +45,10 @@ const requireIntegrationAuthorizationAuth = ({
req.integrationAuth = integrationAuth;
if (attachAccessToken) {
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
req.accessToken = access.accessToken;
}
return next();

View File

@ -1,23 +0,0 @@
import mongoose, { Schema, model } from 'mongoose';
const LoginSRPDetailSchema = new Schema(
{
clientPublicKey: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
serverBInt: { type: mongoose.Schema.Types.Buffer },
expireAt: { type: Date }
}
);
const LoginSRPDetail = model('LoginSRPDetail', LoginSRPDetailSchema);
// LoginSRPDetailSchema.index({ "expireAt": 1 }, { expireAfterSeconds: 0 });
export default LoginSRPDetail;

View File

@ -16,6 +16,7 @@ import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
export {
BackupPrivateKey,
@ -53,5 +54,7 @@ export {
ServiceTokenData,
IServiceTokenData,
APIKeyData,
IAPIKeyData
IAPIKeyData,
LoginSRPDetail,
ILoginSRPDetail
};

View File

@ -1,5 +1,8 @@
import { Schema, model, Types } from 'mongoose';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -17,7 +20,18 @@ export interface IIntegration {
owner: string;
targetEnvironment: string;
appId: string;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
path: string;
region: string;
integration:
| 'azure-key-vault'
| 'aws-parameter-store'
| 'aws-secret-manager'
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'render'
| 'flyio';
integrationAuth: Types.ObjectId;
}
@ -56,9 +70,22 @@ const integrationSchema = new Schema<IIntegration>(
type: String,
default: null
},
path: {
// aws-parameter-store-specific path
type: String,
default: null
},
region: {
// aws-parameter-store-specific path
type: String,
default: null
},
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,

View File

@ -1,20 +1,37 @@
import { Schema, model, Types } from 'mongoose';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables';
export interface IIntegrationAuth {
_id: Types.ObjectId;
workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio';
teamId: string;
accountId: string;
integration:
| 'azure-key-vault'
| 'aws-parameter-store'
| 'aws-secret-manager'
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'render'
| 'flyio';
teamId: string; // TODO: deprecate (vercel) -> move to accessId
accountId: string; // TODO: deprecate (netlify) -> move to accessId
refreshCiphertext?: string;
refreshIV?: string;
refreshTag?: string;
accessIdCiphertext?: string; // new
accessIdIV?: string; // new
accessIdTag?: string; // new
accessCiphertext?: string;
accessIV?: string;
accessTag?: string;
@ -31,10 +48,15 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
integration: {
type: String,
enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
],
required: true
},
@ -58,6 +80,18 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: String,
select: false
},
accessIdCiphertext: {
type: String,
select: false
},
accessIdIV: {
type: String,
select: false
},
accessIdTag: {
type: String,
select: false
},
accessCiphertext: {
type: String,
select: false

View File

@ -0,0 +1,29 @@
import mongoose, { Schema, model, Types } from 'mongoose';
export interface ILoginSRPDetail {
_id: Types.ObjectId;
clientPublicKey: string;
email: string;
serverBInt: mongoose.Schema.Types.Buffer;
expireAt: Date;
}
const loginSRPDetailSchema = new Schema<ILoginSRPDetail>(
{
clientPublicKey: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
serverBInt: { type: mongoose.Schema.Types.Buffer },
expireAt: { type: Date }
}
);
const LoginSRPDetail = model('LoginSRPDetail', loginSRPDetailSchema);
export default LoginSRPDetail;

View File

@ -23,6 +23,7 @@ export interface ISecret {
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
tags?: string[];
}
const secretSchema = new Schema<ISecret>(
@ -47,6 +48,11 @@ const secretSchema = new Schema<ISecret>(
type: Schema.Types.ObjectId,
ref: 'User'
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
environment: {
type: String,
required: true

49
backend/src/models/tag.ts Normal file
View File

@ -0,0 +1,49 @@
import { Schema, model, Types } from 'mongoose';
export interface ITag {
_id: Types.ObjectId;
name: string;
slug: string;
user: Types.ObjectId;
workspace: Types.ObjectId;
}
const tagSchema = new Schema<ITag>(
{
name: {
type: String,
required: true,
trim: true,
},
slug: {
type: String,
required: true,
trim: true,
lowercase: true,
validate: [
function (value: any) {
return value.indexOf(' ') === -1;
},
'slug cannot contain spaces'
]
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'
},
},
{
timestamps: true
}
);
tagSchema.index({ slug: 1, workspace: 1 }, { unique: true })
tagSchema.index({ workspace: 1 })
const Tag = model<ITag>('Tag', tagSchema);
export default Tag;

View File

@ -12,6 +12,7 @@ export interface IUser {
salt?: string;
verifier?: string;
refreshVersion?: number;
seenIps: [string];
}
const userSchema = new Schema<IUser>(
@ -54,7 +55,8 @@ const userSchema = new Schema<IUser>(
type: Number,
default: 0,
select: false
}
},
seenIps: [String]
},
{
timestamps: true

View File

@ -8,6 +8,7 @@ export interface IWorkspace {
name: string;
slug: string;
}>;
autoCapitalization: boolean;
}
const workspaceSchema = new Schema<IWorkspace>({
@ -15,6 +16,10 @@ const workspaceSchema = new Schema<IWorkspace>({
type: String,
required: true
},
autoCapitalization: {
type: Boolean,
default: true,
},
organization: {
type: Schema.Types.ObjectId,
ref: 'Organization',

View File

@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration
router.post( // new: add new integration for integration auth
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
@ -19,7 +19,15 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('integrationAuthId').exists().trim(),
body('integrationAuthId').exists().isString().trim(),
body('app').trim(),
body('isActive').exists().isBoolean(),
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('owner').trim(),
body('path').trim(),
body('region').trim(),
validateRequest,
integrationController.createIntegration
);

View File

@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions
);
router.get(
'/:integrationAuthId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
validateRequest,
integrationAuthController.getIntegrationAuth
);
router.post(
'/oauth-token',
requireAuth({
@ -44,6 +57,7 @@ router.post(
location: 'body'
}),
body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(),
body('integration').exists().trim().notEmpty(),
validateRequest,

View File

@ -6,6 +6,7 @@ import secrets from './secrets';
import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
import environment from "./environment"
import tags from "./tags"
export {
users,
@ -15,5 +16,6 @@ export {
secrets,
serviceTokenData,
apiKeyData,
environment
environment,
tags
}

View File

@ -0,0 +1,50 @@
import express, { Response, Request } from 'express';
const router = express.Router();
import { body, param } from 'express-validator';
import { tagController } from '../../controllers/v2';
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
router.get(
'/:workspaceId/tags',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
}),
param('workspaceId').exists().trim(),
validateRequest,
tagController.getWorkspaceTags
);
router.delete(
'/tags/:tagId',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
param('tagId').exists().trim(),
validateRequest,
tagController.deleteWorkspaceTag
);
router.post(
'/:workspaceId/tags',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
}),
param('workspaceId').exists().trim(),
body('name').exists().trim(),
body('slug').exists().trim(),
validateRequest,
tagController.createWorkspaceTag
);
export default router;

View File

@ -118,4 +118,19 @@ router.delete( // TODO - rewire dashboard to this route
workspaceController.deleteWorkspaceMembership
);
router.patch(
'/:workspaceId/auto-capitalization',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
body('autoCapitalization').exists().trim().notEmpty(),
validateRequest,
workspaceController.toggleAutoCapitalization
);
export default router;

View File

@ -1,7 +1,3 @@
import * as Sentry from '@sentry/node';
import {
Integration
} from '../models';
import {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
@ -10,7 +6,6 @@ import {
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
import { exchangeCode } from '../integrations';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
@ -26,11 +21,12 @@ class IntegrationService {
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
* - Add placeholder inactive integration
* - Create bot sequence for integration
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
* @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.environment - workspace environment
* @param {String} obj1.integration - name of integration
* @param {String} obj1.code - code
* @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/
static async handleOAuthExchange({
workspaceId,
@ -43,7 +39,7 @@ class IntegrationService {
code: string;
environment: string;
}) {
await handleOAuthExchangeHelper({
return await handleOAuthExchangeHelper({
workspaceId,
integration,
code,
@ -116,26 +112,30 @@ class IntegrationService {
}
/**
* Encrypt access token [accessToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* Encrypt access token [accessToken] and (optionally) access id using the
* bot's copy of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessId - access id
* @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
* @returns {IntegrationAuth} - updated integration auth
*/
static async setIntegrationAuthAccess({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
}) {
return await setIntegrationAuthAccessHelper({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
});

View File

@ -6,10 +6,9 @@
<title>Email Verification</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h2>{{code}}</h2>
<h1>{{code}}</h1>
<p>Questions about setting up Infisical? Email us at support@infisical.com</p>
</body>
</html>

View File

@ -7,12 +7,10 @@
<title>Organization Invitation</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Join your team on Infisical</h2>
<h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment
variables.</p>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
</html>

View File

@ -6,7 +6,6 @@
<title>Account Recovery</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Reset your password</h2>
<p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>

View File

@ -6,11 +6,10 @@
<title>Project Invitation</title>
</head>
<body>
<h2>Infisical</h2>
<h2>Join your team on Infisical</h2>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical workspace{{workspaceName}}</p>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical project{{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment variables.</p>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
</html>

View File

@ -1,9 +1,13 @@
const ACTION_LOGIN = 'login';
const ACTION_LOGOUT = 'logout';
const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets';
export {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,

View File

@ -6,6 +6,9 @@ import {
ENV_SET
} from './environment';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -14,6 +17,7 @@ import {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@ -35,6 +39,8 @@ import {
import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,
@ -56,6 +62,9 @@ export {
ENV_STAGING,
ENV_PROD,
ENV_SET,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -64,6 +73,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
@ -75,6 +85,8 @@ export {
INTEGRATION_FLYIO_API_URL,
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS,
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS,

View File

@ -1,3 +1,7 @@
import {
CLIENT_ID_AZURE,
TENANT_ID_AZURE
} from '../config';
import {
CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY,
@ -6,6 +10,9 @@ import {
} from '../config';
// integrations
const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault';
const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store';
const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager';
const INTEGRATION_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify';
@ -13,6 +20,7 @@ const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -25,6 +33,7 @@ const INTEGRATION_SET = new Set([
const INTEGRATION_OAUTH2 = 'oauth2';
// integration oauth endpoints
const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID_AZURE}/oauth2/v2.0/token`;
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token';
@ -95,6 +104,34 @@ const INTEGRATION_OPTIONS = [
clientId: '',
docsLink: ''
},
{
name: 'AWS Parameter Store',
slug: 'aws-parameter-store',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'AWS Secret Manager',
slug: 'aws-secret-manager',
image: 'Amazon Web Services.png',
isAvailable: true,
type: 'custom',
clientId: '',
docsLink: ''
},
{
name: 'Azure Key Vault',
slug: 'azure-key-vault',
image: 'Microsoft Azure.png',
isAvailable: false,
type: 'oauth',
clientId: CLIENT_ID_AZURE,
tenantId: TENANT_ID_AZURE,
docsLink: ''
},
{
name: 'Google Cloud Platform',
slug: 'gcp',
@ -104,24 +141,6 @@ const INTEGRATION_OPTIONS = [
clientId: '',
docsLink: ''
},
{
name: 'Amazon Web Services',
slug: 'aws',
image: 'Amazon Web Services.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
},
{
name: 'Microsoft Azure',
slug: 'azure',
image: 'Microsoft Azure.png',
isAvailable: false,
type: '',
clientId: '',
docsLink: ''
},
{
name: 'Travis CI',
slug: 'travisci',
@ -143,6 +162,9 @@ const INTEGRATION_OPTIONS = [
]
export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
@ -151,6 +173,7 @@ export {
INTEGRATION_FLYIO,
INTEGRATION_SET,
INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,

View File

@ -166,3 +166,24 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
return true
}
func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessibleEnvironmentsRequest) (GetAccessibleEnvironmentsResponse, error) {
var accessibleEnvironmentsResponse GetAccessibleEnvironmentsResponse
response, err := httpClient.
R().
SetResult(&accessibleEnvironmentsResponse).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v2/workspace/%s/environments", config.INFISICAL_URL, request.WorkspaceId))
log.Debugln(fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response))
if err != nil {
return GetAccessibleEnvironmentsResponse{}, err
}
if response.IsError() {
return GetAccessibleEnvironmentsResponse{}, fmt.Errorf("CallGetAccessibleEnvironments: Unsuccessful response: [response=%v]", response)
}
return accessibleEnvironmentsResponse, nil
}

View File

@ -201,21 +201,30 @@ type GetEncryptedSecretsV2Request struct {
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
} `json:"secrets"`
}
@ -241,3 +250,15 @@ type GetServiceTokenDetailsResponse struct {
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
}
type GetAccessibleEnvironmentsRequest struct {
WorkspaceId string `json:"workspaceId"`
}
type GetAccessibleEnvironmentsResponse struct {
AccessibleEnvironments []struct {
Name string `json:"name"`
Slug string `json:"slug"`
IsWriteDenied bool `json:"isWriteDenied"`
} `json:"accessibleEnvironments"`
}

View File

@ -56,7 +56,12 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(envName)
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@ -91,6 +96,7 @@ func init() {
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
}
// Format according to the format flag

View File

@ -101,6 +101,9 @@ var loginCmd = &cobra.Command{
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
}
// clear backed up secrets from prev account
util.DeleteBackupSecrets()
color.Green("Nice! You are logged in as: %v", email)
},

45
cli/packages/cmd/reset.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"os"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/spf13/cobra"
)
var resetCmd = &cobra.Command{
Use: "reset",
Short: "Used delete all Infisical related data on your machine",
DisableFlagsInUseLine: true,
Example: "infisical reset",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
toggleDebug(cmd, args)
},
Run: func(cmd *cobra.Command, args []string) {
// delete config
_, pathToDir, err := util.GetFullConfigFilePath()
if err != nil {
util.HandleError(err)
}
os.RemoveAll(pathToDir)
// delete keyring
keyringInstance, err := util.GetKeyRing()
if err != nil {
util.HandleError(err)
}
keyringInstance.Remove(util.KEYRING_SERVICE_NAME)
util.PrintSuccessMessage("Reset successful")
},
}
func init() {
rootCmd.AddCommand(resetCmd)
}

View File

@ -12,6 +12,7 @@ import (
"strings"
"syscall"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
@ -58,9 +59,10 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
// if !util.IsSecretEnvironmentValid(envName) {
// util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging")
// }
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
@ -72,7 +74,8 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(envName)
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
}
@ -140,6 +143,7 @@ var runCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")

View File

@ -6,6 +6,8 @@ package cmd
import (
"encoding/base64"
"fmt"
"regexp"
"sort"
"strings"
"unicode"
@ -22,7 +24,7 @@ import (
)
var secretsCmd = &cobra.Command{
Example: `infisical secrets"`,
Example: `infisical secrets`,
Short: "Used to create, read update and delete secrets",
Use: "secrets",
DisableFlagsInUseLine: true,
@ -34,12 +36,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil {
util.HandleError(err)
}
secrets, err := util.GetAllEnvironmentVariables(environmentName)
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err)
}
@ -62,6 +69,16 @@ var secretsGetCmd = &cobra.Command{
Run: getSecretsByNames,
}
var secretsGenerateExampleEnvCmd = &cobra.Command{
Example: `secrets generate-example-env > .example-env`,
Short: "Used to generate a example .env file",
Use: "generate-example-env",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
PreRun: toggleDebug,
Run: generateExampleEnv,
}
var secretsSetCmd = &cobra.Command{
Example: `secrets set <secretName=secretValue> <secretName=secretValue>..."`,
Short: "Used set secrets",
@ -111,7 +128,7 @@ var secretsSetCmd = &cobra.Command{
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets
secrets, err := util.GetAllEnvironmentVariables(environmentName)
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil {
util.HandleError(err, "unable to retrieve secrets")
}
@ -267,7 +284,7 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details")
}
secrets, err := util.GetAllEnvironmentVariables(environmentName)
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}
@ -309,30 +326,6 @@ var secretsDeleteCmd = &cobra.Command{
},
}
func init() {
secretsCmd.AddCommand(secretsGetCmd)
secretsGetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.AddCommand(secretsDeleteCmd)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.PersistentFlags().String("env", "dev", "Used to select 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 {
@ -344,7 +337,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(environmentName)
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
@ -371,6 +369,171 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
visualize.PrintAllSecretDetails(requestedSecrets)
}
func generateExampleEnv(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath()
if !workspaceFileExists {
util.HandleError(err, "Unable to parse flag")
}
infisicalToken, err := cmd.Flags().GetString("token")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
if err != nil {
util.HandleError(err, "To fetch all secrets")
}
tagsHashToSecretKey := make(map[string]int)
type TagsAndSecrets struct {
Secrets []models.SingleEnvironmentVariable
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
}
}
// sort secrets by associated tags (most number of tags to least tags)
sort.Slice(secrets, func(i, j int) bool {
return len(secrets[i].Tags) > len(secrets[j].Tags)
})
for _, secret := range secrets {
listOfTagSlugs := []string{}
for _, tag := range secret.Tags {
listOfTagSlugs = append(listOfTagSlugs, tag.Slug)
}
sort.Strings(listOfTagSlugs)
tagsHash := util.GetHashFromStringList(listOfTagSlugs)
tagsHashToSecretKey[tagsHash] += 1
}
finalTagHashToSecretKey := make(map[string]TagsAndSecrets)
for _, secret := range secrets {
listOfTagSlugs := []string{}
for _, tag := range secret.Tags {
listOfTagSlugs = append(listOfTagSlugs, tag.Slug)
}
// sort the slug so we get the same hash each time
sort.Strings(listOfTagSlugs)
tagsHash := util.GetHashFromStringList(listOfTagSlugs)
occurrence, exists := tagsHashToSecretKey[tagsHash]
if exists && occurrence > 0 {
value, exists2 := finalTagHashToSecretKey[tagsHash]
allSecretsForTags := append(value.Secrets, secret)
// sort the the secrets by keys so that they can later be sorted by the first item in the secrets array
sort.Slice(allSecretsForTags, func(i, j int) bool {
return allSecretsForTags[i].Key < allSecretsForTags[j].Key
})
if exists2 {
finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{
Tags: secret.Tags,
Secrets: allSecretsForTags,
}
} else {
finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{
Tags: secret.Tags,
Secrets: []models.SingleEnvironmentVariable{secret},
}
}
tagsHashToSecretKey[tagsHash] -= 1
}
}
// sort the fianl result by secret key fo consistent print order
listOfsecretDetails := make([]TagsAndSecrets, 0, len(finalTagHashToSecretKey))
for _, secretDetails := range finalTagHashToSecretKey {
listOfsecretDetails = append(listOfsecretDetails, secretDetails)
}
// sort the order of the headings by the order of the secrets
sort.Slice(listOfsecretDetails, func(i, j int) bool {
return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags)
})
for _, secretDetails := range listOfsecretDetails {
listOfKeyValue := []string{}
for _, secret := range secretDetails.Secrets {
re := regexp.MustCompile(`(?s)(.*)DEFAULT:(.*)`)
match := re.FindStringSubmatch(secret.Comment)
defaultValue := ""
comment := secret.Comment
// Case: Only has default value
if len(match) == 2 {
defaultValue = strings.TrimSpace(match[1])
}
// Case: has a comment and a default value
if len(match) == 3 {
comment = match[1]
defaultValue = match[2]
}
row := ""
if comment != "" {
comment = addHash(comment)
row = fmt.Sprintf("%s \n%s=%s", strings.TrimSpace(comment), strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue))
} else {
row = fmt.Sprintf("%s=%s", strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue))
}
// each secret row to be added to the file
listOfKeyValue = append(listOfKeyValue, row)
}
listOfTagNames := []string{}
for _, tag := range secretDetails.Tags {
listOfTagNames = append(listOfTagNames, tag.Name)
}
heading := CenterString(strings.Join(listOfTagNames, " & "), 80)
if len(listOfTagNames) == 0 {
fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n"))
} else {
fmt.Printf("\n\n\n%s \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n"))
}
}
}
func CenterString(s string, numStars int) string {
stars := strings.Repeat("*", numStars)
padding := (numStars - len(s)) / 2
cenetredTextWithStar := stars[:padding] + " " + strings.ToUpper(s) + " " + stars[padding:]
hashes := strings.Repeat("#", len(cenetredTextWithStar)+2)
return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes)
}
func addHash(input string) string {
lines := strings.Split(input, "\n")
for i, line := range lines {
lines[i] = "# " + line
}
return strings.Join(lines, "\n")
}
func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
secretMapByName := make(map[string]models.SingleEnvironmentVariable)
@ -380,3 +543,29 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
return secretMapByName
}
func init() {
secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.AddCommand(secretsGenerateExampleEnvCmd)
secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.AddCommand(secretsGetCmd)
secretsCmd.AddCommand(secretsSetCmd)
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.AddCommand(secretsDeleteCmd)
secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
}
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select 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)
}

View File

@ -12,6 +12,11 @@ import (
// will decrypt cipher text to plain text using iv and tag
func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) {
// Case: empty string
if len(cipherText) == 0 && len(tag) == 0 && len(iv) == 0 {
return []byte{}, nil
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err

View File

@ -1,6 +1,8 @@
package models
import "github.com/99designs/keyring"
import (
"github.com/99designs/keyring"
)
type UserCredentials struct {
Email string `json:"email"`
@ -19,6 +21,13 @@ type SingleEnvironmentVariable struct {
Value string `json:"value"`
Type string `json:"type"`
ID string `json:"_id"`
Tags []struct {
ID string `json:"_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Workspace string `json:"workspace"`
} `json:"tags"`
Comment string `json:"comment"`
}
type Workspace struct {
@ -34,7 +43,12 @@ type WorkspaceConfigFile struct {
}
type SymmetricEncryptionResult struct {
CipherText []byte
Nonce []byte
AuthTag []byte
CipherText []byte `json:"CipherText"`
Nonce []byte `json:"Nonce"`
AuthTag []byte `json:"AuthTag"`
}
type GetAllSecretsParameters struct {
Environment string
InfisicalToken string
}

View File

@ -2,6 +2,7 @@ package util
import (
"fmt"
"net/http"
"os"
)
@ -19,3 +20,11 @@ func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) erro
return nil
}
func CheckIsConnectedToInternet() (ok bool) {
_, err := http.Get("http://clients3.google.com/generate_204")
if err != nil {
return false
}
return true
}

View File

@ -1,6 +1,7 @@
package util
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
@ -98,3 +99,14 @@ func RequireLocalWorkspaceFile() {
PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]")
}
}
func GetHashFromStringList(list []string) string {
hash := sha256.New()
for _, item := range list {
hash.Write([]byte(item))
}
sum := sha256.Sum256(hash.Sum(nil))
return fmt.Sprintf("%x", sum)
}

View File

@ -24,7 +24,11 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
}
func PrintWarning(message string) {
color.Yellow("Warning: %v", message)
color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message)
}
func PrintSuccessMessage(message string) {
color.New(color.FgGreen).Println(message)
}
func PrintMessageAndExit(messages ...string) {

View File

@ -2,6 +2,8 @@ package util
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
@ -97,13 +99,26 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
return plainTextSecrets, nil
}
func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVariable, error) {
infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME)
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string
if params.InfisicalToken == "" {
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
} else {
infisicalToken = params.InfisicalToken
}
isConnected := CheckIsConnectedToInternet()
var secretsToReturn []models.SingleEnvironmentVariable
var errorToReturn error
if infisicalToken == "" {
RequireLocalWorkspaceFile()
RequireLogin()
log.Debug("Trying to fetch secrets using logged in details")
if isConnected {
log.Debug("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
RequireLocalWorkspaceFile()
RequireLogin()
}
log.Debug("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil {
@ -115,13 +130,63 @@ func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVaria
return nil, err
}
secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName)
return secrets, err
// Verify environment
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
if err != nil {
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err)
}
secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment)
log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
if errorToReturn == nil {
WriteBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey, secretsToReturn)
}
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
if !isConnected {
backedSecrets, err := ReadBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey)
if len(backedSecrets) > 0 {
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
secretsToReturn = backedSecrets
errorToReturn = err
}
}
} else {
log.Debug("Trying to fetch secrets using service token")
return GetPlainTextSecretsViaServiceToken(infisicalToken)
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken)
}
return secretsToReturn, errorToReturn
}
func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error {
httpClient := resty.New()
httpClient.SetAuthToken(userLoggedInDetails.JTWToken).
SetHeader("Accept", "application/json")
response, err := api.CallGetAccessibleEnvironments(httpClient, api.GetAccessibleEnvironmentsRequest{WorkspaceId: workspaceId})
if err != nil {
return err
}
listOfEnvSlugs := []string{}
mapOfEnvSlugs := make(map[string]interface{})
for _, environment := range response.AccessibleEnvironments {
listOfEnvSlugs = append(listOfEnvSlugs, environment.Slug)
mapOfEnvSlugs[environment.Slug] = environment
}
_, exists := mapOfEnvSlugs[environmentName]
if !exists {
HandleError(fmt.Errorf("the environment [%s] does not exist in project with [id=%s]. Only [%s] are available", environmentName, workspaceId, strings.Join(listOfEnvSlugs, ",")))
}
return nil
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
@ -283,11 +348,34 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
}
// Decrypt comment
comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret value")
}
comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
}
comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret comment")
}
plainTextSecret := models.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
Tags: secret.Tags,
Comment: string(plainTextComment),
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
@ -295,3 +383,100 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
return plainTextSecrets, nil
}
func WriteBackupSecrets(workspace string, environment string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
if err != nil {
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
}
// create secrets backup directory
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
if err != nil {
return err
}
}
var encryptedSecrets []models.SymmetricEncryptionResult
for _, secret := range secrets {
marshaledSecrets, _ := json.Marshal(secret)
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
if err != nil {
return err
}
encryptedSecrets = append(encryptedSecrets, result)
}
listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets)
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, os.ModePerm)
if err != nil {
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
}
return nil
}
func ReadBackupSecrets(workspace string, environment string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment)
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
if err != nil {
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
}
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
return nil, nil
}
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
if err != nil {
return nil, err
}
var listOfEncryptedBackupSecrets []models.SymmetricEncryptionResult
_ = json.Unmarshal(encryptedBackupSecretsAsBytes, &listOfEncryptedBackupSecrets)
var plainTextSecrets []models.SingleEnvironmentVariable
for _, encryptedSecret := range listOfEncryptedBackupSecrets {
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedSecret.CipherText, encryptedSecret.AuthTag, encryptedSecret.Nonce)
if err != nil {
return nil, err
}
var plainTextSecret models.SingleEnvironmentVariable
err = json.Unmarshal(result, &plainTextSecret)
if err != nil {
return nil, err
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
}
return plainTextSecrets, nil
}
func DeleteBackupSecrets() error {
secrets_backup_folder_name := "secrets-backup"
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
if err != nil {
return fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
}
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
return os.RemoveAll(fullPathToSecretsBackupFolder)
}

View File

@ -9,6 +9,8 @@ infisical init
## Description
Link a local project to the platform
Link a local project to your Infisical project. Once connected, you can then access the secrets locally from the connected Infisical project.
The command creates a `infisical.json` file containing your Project ID.
<Info>
This command creates a `infisical.json` file containing your Project ID.
</Info>

View File

@ -0,0 +1,11 @@
---
title: "infisical reset"
description: "Reset Infisical"
---
```bash
infisical reset
```
## Description
This command provides a way to clear all Infisical-generated configuration data, effectively resetting the software to its default settings. This can be an effective way to address any persistent issues that arise while using the CLI.

View File

@ -25,13 +25,58 @@ description: "The command that injects your secrets into local environment"
## Description
Inject environment variables from the platform into an application process.
Inject secrets from Infisical into your application process.
## Options
| Option | Description | Default value |
| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |
| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` |
## Subcommands & flags
<Accordion title="infisical run" defaultOpen="true">
Use this command to inject secrets into your applications process
```bash
$ infisical run -- <your application command>
# Example
$ infisical run -- npm run dev
```
### flags
<Accordion title="--command">
Pass secrets into multiple commands at once
```bash
# Example
infisical run --command="npm run build && npm run dev; more-commands..."
```
</Accordion>
<Accordion title="--token">
If you are using a [service token](../../getting-started/dashboard/token) to authenticate, you can pass the token as a flag
```bash
# Example
infisical run --token="st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec" -- npm run start
```
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag
</Accordion>
<Accordion title="--expand">
Turn on or off the shell parameter expansion in your secrets. If you have used shell parameters in your secret(s), activating this feature will populate them before injecting them into your application process.
Default value: `true`
</Accordion>
<Accordion title="--env">
This is used to specify the environment from which secrets should be retrieved. The accepted values are the environment slugs defined for your project, such as `dev`, `staging`, `test`, and `prod`.
Default value: `dev`
</Accordion>
<Accordion title="--secret-overriding">
Prioritizes personal secrets with the same name over shared secrets
Default value: `true`
</Accordion>
</Accordion>

View File

@ -14,17 +14,8 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
<Accordion title="infisical secrets" defaultOpen="true">
Use this command to print out all of the secrets in your project
```
```bash
$ infisical secrets
## Example
$ infisical secrets
┌─────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├─────────────┼──────────────┼─────────────┤
│ DOMAIN │ example.com │ shared │
│ HASH │ jebhfbwe │ shared │
└─────────────┴──────────────┴─────────────┘
```
### flags
@ -45,16 +36,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
<Accordion title="infisical secrets get">
This command allows you selectively print the requested secrets by name
```
```bash
$ infisical secrets get <secret-name-a> <secret-name-b> ...
# Example
$ infisical secrets get DOMAIN
┌─────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├─────────────┼──────────────┼─────────────┤
│ DOMAIN │ example.com │ shared │
└─────────────┴──────────────┴─────────────┘
```
@ -70,18 +56,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
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.
```
```bash
$ 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
@ -95,12 +74,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
<Accordion title="infisical secrets delete">
This command allows you to delete secrets by their name(s).
```
```bash
$ 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
@ -109,4 +87,25 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
Default value: `dev`
</Accordion>
</Accordion>
</Accordion>
<Accordion title="infisical secrets generate-example-env">
This command allows you to generate an example .env file from your secrets and with their associated comments and tags. This is useful when you would like to let
others who work on the project but do not use Infisical become aware of the required environment variables and their intended values.
To place default values in your example .env file, you can simply include the syntax `DEFAULT:<value>` within your secret's comment in Infisical. This will result in the specified value being extracted and utilized as the default.
```bash
$ infisical secrets generate-example-env
## Example
$ infisical secrets generate-example-env > .example-env
```
### Flags
<Accordion title="--env">
Used to select the environment name on which actions should be taken on
Default value: `dev`
</Accordion>
</Accordion>

View File

@ -13,4 +13,9 @@ If none of the available stores work for you, you can try using the `file` store
If you are still experiencing trouble, please seek support.
[Learn more about vault command](./commands/vault)
</Accordion>
<Accordion title="Can I fetch secrets with Infisical if I am offline?">
Yes. If you have previously retrieved secrets for a specific project and environment (such as dev, staging, or prod), the `run`/`secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts.
</Accordion>

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 421 KiB

BIN
docs/images/k8-diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,34 @@
---
title: "Gitlab Pipeline"
---
To integrate Infisical secrets into your Gitlab CI/CD setup, three steps are required.
## Generate service token
To expose Infisical secrets in Gitlab CI/CD, you must generate a service token for the specific project and environment in Infisical. For instructions on how to generate a service token, refer to [this page](../../getting-started/dashboard/token)
## Set Infisical service token in Gitlab
To provide Infisical CLI with the service token generated in the previous step, go to **Settings > CI/CD > Variables** in Gitlab and create a new **INFISICAL_TOKEN** variable. Enter the generated service token as its value.
## Configure Infisical in your pipeline
Edit your .gitlab-ci.yml to include the installation of the Infisical CLI. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process.
#### Example
```yaml
image: ubuntu
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- apt update && apt install -y curl
- curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash
- apt-get update && apt-get install -y infisical
- infisical run -- npm run build
...
```

View File

@ -0,0 +1,75 @@
---
title: "AWS Parameter Store"
description: "How to automatically sync secrets from Infisical to your AWS Parameter Store."
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Set up AWS and have/create an IAM user
## Grant the IAM user permissions to access AWS Parameter Store
Navigate to your IAM user permissions and add a permission policy to grant access to AWS Parameter Store.
![integration IAM 1](../../images/integrations-aws-iam-1.png)
![integration IAM 2](../../images/integrations-aws-parameter-store-iam-2.png)
![integrations IAM 3](../../images/integrations-aws-parameter-store-iam-3.png)
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Parameter Store for the IAM user that you can use:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSSMAccess",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParametersByPath",
"ssm:DeleteParameters"
],
"Resource": "*"
}
]
}
```
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for AWS Parameter store
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
![access key 1](../../images/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations-aws-access-key-2.png)
![access key 3](../../images/integrations-aws-access-key-3.png)
Press on the AWS Parameter Store tile and input your AWS access key ID and secret access key from the previous step.
![integration auth](../../images/integrations-aws-parameter-store-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store.
![integration create](../../images/integrations-aws-parameter-store-create.png)
<Tip>
Infisical requires you to add a path for your secrets to be stored in AWS
Parameter Store and recommends setting the path structure to
`/[project_name]/[environment]/` according to best practices. This enables a
secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS
Parameter Store.
</Tip>

View File

@ -0,0 +1,73 @@
---
title: "AWS Secret Manager"
description: "How to automatically sync secrets from Infisical to your AWS Secret Manager."
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Set up AWS and have/create an IAM user
## Grant the IAM user permissions to access AWS Secret Manager
Navigate to your IAM user permissions and add a permission policy to grant access to AWS Secret Manager.
![integration IAM 1](../../images/integrations-aws-iam-1.png)
![integration IAM 2](../../images/integrations-aws-secret-manager-iam-2.png)
![integrations IAM 3](../../images/integrations-aws-secret-manager-iam-3.png)
For better security, here's a custom policy containing the minimum permissions required by Infisical to sync secrets to AWS Secret Manager for the IAM user that you can use:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSecretsManagerAccess",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret"
],
"Resource": "*"
}
]
}
```
## Navigate to your project's integrations tab
![integrations](../../images/integrations.png)
## Authorize Infisical for AWS Secret Manager
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
![access key 1](../../images/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations-aws-access-key-2.png)
![access key 3](../../images/integrations-aws-access-key-3.png)
Press on the AWS Secret Manager tile and input your AWS access key ID and secret access key from the previous step.
![integration auth](../../images/integrations-aws-secret-manager-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
## Start integration
Select which Infisical environment secrets you want to sync to which AWS Secret Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secret Manager.
![integration create](../../images/integrations-aws-secret-manager-create.png)
<Info>
Infisical currently syncs environment variables to AWS Secret Manager as
key-value pairs under one secret. We're actively exploring ways to help users
group environment variable key-pairs under multiple secrets for greater
control.
</Info>

View File

@ -31,6 +31,7 @@ Press on the Fly.io tile and input your Fly.io access token to grant Infisical a
## Start integration
Select which Infisical environment secrets you want to sync to which Fly.io app and press start integration to start syncing secrets to Fly.io.
Select which Infisical environment secrets you want to sync to which Fly.io app and press create integration to start syncing secrets to Fly.io.
![integrations fly](../../images/integrations-flyio-create.png)
![integrations fly](../../images/integrations-flyio.png)

Some files were not shown because too many files have changed in this diff Show More