Update package.json

This commit is contained in:
Tuan Dang
2023-02-11 16:07:32 +07:00
309 changed files with 13900 additions and 2982 deletions
+5 -7
View File
@@ -45,9 +45,8 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true push: true
context: backend context: backend
tags: | tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:${{ steps.commit.outputs.short }} infisical/backend:latest
infisical/backend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
@@ -95,9 +94,8 @@ jobs:
push: true push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend context: frontend
tags: | tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:${{ steps.commit.outputs.short }} infisical/frontend:latest
infisical/frontend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -137,4 +135,4 @@ jobs:
exit 1 exit 1
else else
echo "Helm upgrade was successful" echo "Helm upgrade was successful"
fi fi
+3 -2
View File
@@ -19,6 +19,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- run: echo "Ref name ${{github.ref_name}}"
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: '>=1.19.3' go-version: '>=1.19.3'
@@ -33,11 +34,11 @@ jobs:
run: | run: |
mkdir ../../osxcross mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v2 - uses: goreleaser/goreleaser-action@v4
with: with:
distribution: goreleaser distribution: goreleaser
version: latest version: latest
args: release --rm-dist args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
+20 -1
View File
@@ -14,6 +14,9 @@ before:
builds: builds:
- id: darwin-build - id: darwin-build
binary: infisical binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
flags:
- -trimpath
env: env:
- CGO_ENABLED=1 - CGO_ENABLED=1
- CC=/home/runner/work/osxcross/target/bin/o64-clang - CC=/home/runner/work/osxcross/target/bin/o64-clang
@@ -24,10 +27,14 @@ builds:
- goos: darwin - goos: darwin
goarch: "386" goarch: "386"
dir: ./cli dir: ./cli
- id: all-other-builds - id: all-other-builds
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
binary: infisical binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
flags:
- -trimpath
goos: goos:
- freebsd - freebsd
- linux - linux
@@ -65,8 +72,10 @@ release:
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:
name_template: "{{ incpatch .Version }}" name_template: "{{ incpatch .Version }}-devel"
changelog: changelog:
sort: asc sort: asc
filters: filters:
@@ -80,6 +89,7 @@ changelog:
# - infisical # - infisical
# dir: "{{ dir .ArtifactPath }}" # dir: "{{ dir .ArtifactPath }}"
# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/ # cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
brews: brews:
- name: infisical - name: infisical
tap: tap:
@@ -91,6 +101,13 @@ brews:
folder: Formula folder: Formula
homepage: "https://infisical.com" homepage: "https://infisical.com"
description: "The official Infisical CLI" description: "The official Infisical CLI"
install: |-
bin.install "infisical"
bash_completion.install "completions/infisical.bash" => "infisical"
zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz"
nfpms: nfpms:
- id: infisical - id: infisical
package_name: infisical package_name: infisical
@@ -116,6 +133,7 @@ nfpms:
dst: /usr/share/zsh/site-functions/_infisical dst: /usr/share/zsh/site-functions/_infisical
- src: ./manpages/infisical.1.gz - src: ./manpages/infisical.1.gz
dst: /usr/share/man/man1/infisical.1.gz dst: /usr/share/man/man1/infisical.1.gz
scoop: scoop:
bucket: bucket:
owner: Infisical owner: Infisical
@@ -126,6 +144,7 @@ scoop:
homepage: "https://infisical.com" homepage: "https://infisical.com"
description: "The official Infisical CLI" description: "The official Infisical CLI"
license: MIT license: MIT
aurs: aurs:
- -
name: infisical-bin name: infisical-bin
+24 -13
View File
File diff suppressed because one or more lines are too long
+3374 -200
View File
File diff suppressed because it is too large Load Diff
+45 -40
View File
@@ -1,4 +1,48 @@
{ {
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.267.0",
"@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",
"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",
"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"
},
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",
"main": "src/index.js", "main": "src/index.js",
@@ -36,6 +80,7 @@
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.2.4", "@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
@@ -73,45 +118,5 @@
"suiteNameTemplate": "{filepath}", "suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}", "classNameTemplate": "{classname}",
"titleTemplate": "{title}" "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",
"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",
"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"
} }
} }
+2
View File
@@ -52,6 +52,7 @@ import {
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter, apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter, environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2'; } from './routes/v2';
import { healthCheck } from './routes/status'; import { healthCheck } from './routes/status';
@@ -116,6 +117,7 @@ app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter); app.use('/api/v2/users', v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter); app.use('/api/v2/organizations', v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter); app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecated app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter);
+6
View File
@@ -13,10 +13,13 @@ const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production'; const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true; const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined; 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_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!; 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_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
@@ -60,10 +63,13 @@ export {
NODE_ENV, NODE_ENV,
VERBOSE_ERROR_OUTPUT, VERBOSE_ERROR_OUTPUT,
LOKI_HOST, LOKI_HOST,
CLIENT_ID_AZURE,
TENANT_ID_AZURE,
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL, CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU, CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
+32 -2
View File
@@ -4,16 +4,21 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User } from '../../models'; import { User, LoginSRPDetail } from '../../models';
import { createToken, issueTokens, clearTokens } from '../../helpers/auth'; import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import {
ACTION_LOGIN,
ACTION_LOGOUT
} from '../../variables';
import { import {
NODE_ENV, NODE_ENV,
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_SECRET JWT_REFRESH_SECRET
} from '../../config'; } from '../../config';
import LoginSRPDetail from '../../models/LoginSRPDetail';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -116,6 +121,18 @@ export const login2 = async (req: Request, res: Response) => {
secure: NODE_ENV === 'production' ? true : false 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 (access) token in response
return res.status(200).send({ return res.status(200).send({
token: tokens.token, token: tokens.token,
@@ -159,6 +176,19 @@ export const logout = async (req: Request, res: Response) => {
sameSite: 'strict', sameSite: 'strict',
secure: NODE_ENV === 'production' ? true : false 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) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -10,6 +10,31 @@ import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services'; import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations'; 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 ( export const getIntegrationOptions = async (
req: Request, req: Request,
res: Response res: Response
@@ -31,7 +56,6 @@ export const oAuthExchange = async (
) => { ) => {
try { try {
const { workspaceId, code, integration } = req.body; const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration)) if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration'); throw new Error('Failed to validate integration');
@@ -40,12 +64,16 @@ export const oAuthExchange = async (
throw new Error("Failed to get environments") throw new Error("Failed to get environments")
} }
await IntegrationService.handleOAuthExchange({ const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId, workspaceId,
integration, integration,
code, code,
environment: environments[0].slug, environment: environments[0].slug,
}); });
return res.status(200).send({
integrationAuth
});
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -53,14 +81,11 @@ export const oAuthExchange = async (
message: 'Failed to get OAuth2 code-token exchange' 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 req
* @param res * @param res
*/ */
@@ -69,18 +94,29 @@ export const saveIntegrationAccessToken = async (
res: Response res: Response
) => { ) => {
// TODO: refactor // TODO: refactor
// TODO: check if access token is valid for each integration
let integrationAuth; let integrationAuth;
try { try {
const { const {
workspaceId, workspaceId,
accessId,
accessToken, accessToken,
integration integration
}: { }: {
workspaceId: string; workspaceId: string;
accessId: string | null;
accessToken: string; accessToken: string;
integration: string; integration: string;
} = req.body; } = 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({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration integration
@@ -91,17 +127,11 @@ export const saveIntegrationAccessToken = async (
new: true, new: true,
upsert: 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({ integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId,
accessToken, accessToken,
accessExpiresAt: undefined accessExpiresAt: undefined
}); });
@@ -1,4 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Integration, Integration,
@@ -18,15 +19,44 @@ import { eventPushSecrets } from '../../events';
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { 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 // initialize new integration after saving integration access token
integration = await new Integration({ integration = await new Integration({
workspace: req.integrationAuth.workspace._id, workspace: req.integrationAuth.workspace._id,
isActive: false, environment: sourceEnvironment,
app: null, isActive,
environment: req.integrationAuth.workspace?.environments[0].slug, app,
appId,
targetEnvironment,
owner,
path,
region,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: req.integrationAuth._id integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key } from '../../models'; import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models';
import { import {
findMembership, findMembership,
deleteMembership as deleteMember deleteMembership as deleteMember
@@ -230,4 +230,4 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
invitee, invitee,
latestKey latestKey
}); });
}; };
@@ -76,8 +76,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id // change role for (target) organization membership with id
// [membershipOrgId] // [membershipOrgId]
// TODO
let membershipToChangeRole; let membershipToChangeRole;
// try { // try {
// } catch (err) { // } catch (err) {
@@ -3,14 +3,13 @@ import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
import { User, BackupPrivateKey } from '../../models'; import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config'; import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import { BadRequestError } from '../../utils/errors';
const clientPublicKeys: any = {};
/** /**
* Password reset step 1: Send email verification link to email [email] * Password reset step 1: Send email verification link to email [email]
@@ -53,9 +52,9 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to send email for account recovery' message: 'Failed to send email for account recovery'
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: `Sent an email for account recovery to ${email}` message: `Sent an email for account recovery to ${email}`
}); });
@@ -71,7 +70,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token; let user, token;
try { try {
const { email, code } = req.body; const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey'); user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or // case: user doesn't exist with email [email] or
@@ -86,7 +85,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
email, email,
token: code token: code
}); });
// generate temporary password-reset token // generate temporary password-reset token
token = createToken({ token = createToken({
payload: { payload: {
@@ -100,7 +99,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed email verification for password reset' message: 'Failed email verification for password reset'
}); });
} }
return res.status(200).send({ return res.status(200).send({
@@ -123,7 +122,7 @@ export const srp1 = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
@@ -132,13 +131,15 @@ export const srp1 = async (req: Request, res: Response) => {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
() => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
clientPublicKeys[req.user.email] = {
clientPublicKey, await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
serverBInt: bigintConversion.bigintToBuf(server.bInt) email: req.user.email,
}; clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
@@ -183,17 +184,21 @@ export const changePassword = async (req: Request, res: Response) => {
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: clientPublicKeys[req.user.email].serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey( server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
clientPublicKeys[req.user.email].clientPublicKey
);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
@@ -256,16 +261,22 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: clientPublicKeys[req.user.email].serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey( server.setClientPublicKey(
clientPublicKeys[req.user.email].clientPublicKey loginSRPDetailFromDB.clientPublicKey
); );
// compare server and client shared keys // compare server and client shared keys
@@ -318,16 +329,16 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
backupPrivateKey = await BackupPrivateKey.findOne({ backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id user: req.user._id
}).select('+encryptedPrivateKey +iv +tag'); }).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error('Failed to find backup private key'); if (!backupPrivateKey) throw new Error('Failed to find backup private key');
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email}); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get backup private key' message: 'Failed to get backup private key'
}); });
} }
return res.status(200).send({ return res.status(200).send({
backupPrivateKey backupPrivateKey
}); });
@@ -362,15 +373,15 @@ export const resetPassword = async (req: Request, res: Response) => {
{ {
new: true new: true
} }
); );
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email}); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to get backup private key' message: 'Failed to get backup private key'
}); });
} }
return res.status(200).send({ return res.status(200).send({
message: 'Successfully reset password' message: 'Successfully reset password'
}); });
@@ -6,8 +6,12 @@ import {
Workspace, Workspace,
Integration, Integration,
ServiceTokenData, ServiceTokenData,
Membership,
} from '../../models'; } from '../../models';
import { SecretVersion } from '../../ee/models'; import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors';
import _ from 'lodash';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
/** /**
* Create new workspace environment named [environmentName] under workspace with id * Create new workspace environment named [environmentName] under workspace with id
@@ -120,6 +124,15 @@ export const renameWorkspaceEnvironment = async (
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
); );
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -188,6 +201,11 @@ export const deleteWorkspaceEnvironment = async (
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -202,3 +220,42 @@ export const deleteWorkspaceEnvironment = async (
environment: environmentSlug, environment: environmentSlug,
}); });
}; };
export const getAllAccessibleEnvironmentsOfWorkspace = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const workspacesUserIsMemberOf = await Membership.findOne({
workspace: workspaceId,
user: req.user
})
if (!workspacesUserIsMemberOf) {
throw BadRequestError()
}
const accessibleEnvironments: any = []
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions
const relatedWorkspace = await Workspace.findById(workspaceId)
if (!relatedWorkspace) {
throw BadRequestError()
}
relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
if (isReadBlocked) {
return
} else {
accessibleEnvironments.push({
name: environment.name,
slug: environment.slug,
isWriteDenied: isWriteBlocked
})
}
})
res.json({ accessibleEnvironments })
};
+3 -1
View File
@@ -8,6 +8,7 @@ import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretsController from './secretsController'; import * as secretsController from './secretsController';
import * as environmentController from './environmentController'; import * as environmentController from './environmentController';
import * as tagController from './tagController';
export { export {
authController, authController,
@@ -19,5 +20,6 @@ export {
apiKeyDataController, apiKeyDataController,
secretController, secretController,
secretsController, secretsController,
environmentController environmentController,
tagController
} }
+92 -51
View File
@@ -1,7 +1,7 @@
import to from 'await-to-js'; import to from 'await-to-js';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ISecret, Secret } from '../../models'; import { ISecret, Membership, Secret, Workspace } from '../../models';
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
@@ -10,13 +10,14 @@ import {
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS ACTION_DELETE_SECRETS
} from '../../variables'; } from '../../variables';
import { ValidationError } from '../../utils/errors'; import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services'; import { EESecretService, EELogService } from '../../ee/services';
import { postHogClient } from '../../services'; import { postHogClient } from '../../services';
import { BadRequestError } from '../../utils/errors';
import { getChannelFromUserAgent } from '../../utils/posthog'; import { getChannelFromUserAgent } from '../../utils/posthog';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
/** /**
* Create secret(s) for workspace with id [workspaceId] and environment [environment] * Create secret(s) for workspace with id [workspaceId] and environment [environment]
@@ -76,20 +77,40 @@ export const createSecrets = async (req: Request, res: Response) => {
} }
} }
*/ */
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment } = req.body;
let toAdd; const channel = getChannelFromUserAgent(req.headers['user-agent'])
if (Array.isArray(req.body.secrets)) { const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
// case: create multiple secrets
toAdd = req.body.secrets; const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
} else if (typeof req.body.secrets === 'object') { if (!hasAccess) {
// case: create 1 secret throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
toAdd = [req.body.secrets];
} }
const newSecrets = await Secret.insertMany( let listOfSecretsToCreate;
toAdd.map(({ if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets
listOfSecretsToCreate = req.body.secrets;
} else if (typeof req.body.secrets === 'object') {
// case: create 1 secret
listOfSecretsToCreate = [req.body.secrets];
}
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, type,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -97,15 +118,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
}: { secretCommentCiphertext,
type: string; secretCommentIV,
secretKeyCiphertext: string; secretCommentTag,
secretKeyIV: string; tags
secretKeyTag: string; }: secretsToCreateType) => {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}) => {
return ({ return ({
version: 1, version: 1,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
@@ -117,7 +134,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}); });
}) })
); );
@@ -133,7 +154,7 @@ export const createSecrets = async (req: Request, res: Response) => {
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({ secretVersions: newlyCreatedSecrets.map(({
_id, _id,
version, version,
workspace, workspace,
@@ -147,7 +168,11 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}) => ({ }) => ({
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
secret: _id, secret: _id,
@@ -164,21 +189,25 @@ export const createSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
})) }))
}); });
const addAction = await EELogService.createActionSecret({ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId: req.user._id.toString(), userId: req.user._id,
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
secretIds: newSecrets.map((n) => n._id) secretIds: newlyCreatedSecrets.map((n) => n._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
addAction && await EELogService.createLog({ addAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction], actions: [addAction],
channel, channel,
ipAddress: req.ip ipAddress: req.ip
@@ -194,7 +223,7 @@ export const createSecrets = async (req: Request, res: Response) => {
event: 'secrets added', event: 'secrets added',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: toAdd.length, numberOfSecrets: listOfSecretsToCreate.length,
environment, environment,
workspaceId, workspaceId,
channel: channel, channel: channel,
@@ -204,7 +233,7 @@ export const createSecrets = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
secrets: newSecrets secrets: newlyCreatedSecrets
}); });
} }
@@ -269,6 +298,14 @@ export const getSecrets = async (req: Request, res: Response) => {
userEmail = req.serviceTokenData.user.email; userEmail = req.serviceTokenData.user.email;
} }
// none service token case as service tokens are already scoped
if (!req.serviceTokenData) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const [err, secrets] = await to(Secret.find( const [err, secrets] = await to(Secret.find(
{ {
workspace: workspaceId, workspace: workspaceId,
@@ -279,22 +316,22 @@ export const getSecrets = async (req: Request, res: Response) => {
], ],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
} }
).then()) ).populate("tags").then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
const channel = getChannelFromUserAgent(req.headers['user-agent']) const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createActionSecret({ const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
userId: userId, userId: new Types.ObjectId(userId),
workspaceId: workspaceId as string, workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id) secretIds: secrets.map((n: any) => n._id)
}); });
readAction && await EELogService.createLog({ readAction && await EELogService.createLog({
userId: userId, userId: new Types.ObjectId(userId),
workspaceId: workspaceId as string, workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress: req.ip ipAddress: req.ip
@@ -371,7 +408,6 @@ export const updateSecrets = async (req: Request, res: Response) => {
*/ */
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type // TODO: move type
interface PatchSecret { interface PatchSecret {
id: string; id: string;
@@ -384,6 +420,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: string; secretCommentCiphertext: string;
secretCommentIV: string; secretCommentIV: string;
secretCommentTag: string; secretCommentTag: string;
tags: string[]
} }
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
@@ -396,7 +433,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag secretCommentTag,
tags
} = secret; } = secret;
return ({ return ({
@@ -412,8 +450,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
tags,
...(( ...((
secretCommentCiphertext && secretCommentCiphertext !== undefined &&
secretCommentIV && secretCommentIV &&
secretCommentTag secretCommentTag
) ? { ) ? {
@@ -446,6 +485,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
tags
} = secretModificationsBySecretId[secret._id.toString()] } = secretModificationsBySecretId[secret._id.toString()]
return ({ return ({
@@ -463,6 +503,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
tags: tags ? tags : secret.tags
}); });
}) })
} }
@@ -491,17 +532,17 @@ export const updateSecrets = async (req: Request, res: Response) => {
}); });
}, 10000); }, 10000);
const updateAction = await EELogService.createActionSecret({ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
userId: req.user._id.toString(), userId: req.user._id,
workspaceId: key, workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
updateAction && await EELogService.createLog({ updateAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: key, workspaceId: new Types.ObjectId(key),
actions: [updateAction], actions: [updateAction],
channel, channel,
ipAddress: req.ip ipAddress: req.ip
@@ -617,17 +658,17 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: key workspaceId: key
}) })
}); });
const deleteAction = await EELogService.createActionSecret({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user._id.toString(), userId: req.user._id,
workspaceId: key, workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
}); });
// (EE) create (audit) log // (EE) create (audit) log
deleteAction && await EELogService.createLog({ deleteAction && await EELogService.createLog({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: key, workspaceId: new Types.ObjectId(key),
actions: [deleteAction], actions: [deleteAction],
channel, channel,
ipAddress: req.ip ipAddress: req.ip
@@ -8,6 +8,8 @@ import {
import { import {
SALT_ROUNDS SALT_ROUNDS
} from '../../config'; } from '../../config';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import { ABILITY_READ } from '../../variables/organization';
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
@@ -37,6 +39,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
expiresIn expiresIn
} = req.body; } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
const secret = crypto.randomBytes(16).toString('hex'); const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
@@ -100,4 +107,8 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData
}); });
} }
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}
@@ -0,0 +1,72 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership, Secret,
} 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);
// remove the tag from secrets
await Secret.updateMany(
{ tags: { $in: [tagId] } },
{ $pull: { tags: 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
})
}
@@ -467,4 +467,42 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
return res.status(200).send({ return res.status(200).send({
membership 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
});
};
+3 -1
View File
@@ -3,11 +3,13 @@ import * as secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController'; import * as secretSnapshotController from './secretSnapshotController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as actionController from './actionController'; import * as actionController from './actionController';
import * as membershipController from './membershipController';
export { export {
stripeController, stripeController,
secretController, secretController,
secretSnapshotController, secretSnapshotController,
workspaceController, workspaceController,
actionController actionController,
membershipController
} }
@@ -0,0 +1,63 @@
import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models";
import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
import { Builder } from "builder-pattern"
import _ from "lodash";
export const denyMembershipPermissions = async (req: Request, res: Response) => {
const { membershipId } = req.params;
const { permissions } = req.body;
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) {
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
}
return Builder<IMembershipPermission>()
.environmentSlug(permission.environmentSlug)
.ability(permission.ability)
.build();
})
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
const membershipToModify = await Membership.findById(membershipId)
if (!membershipToModify) {
throw BadRequestError({ message: "Unable to locate resource" })
}
// check if the user making the request is a admin of this project
if (![ADMIN, MEMBER].includes(membershipToModify.role)) {
throw UnauthorizedRequestError()
}
// check if the requested slugs are indeed a part of this related workspace
const relatedWorkspace = await Workspace.findById(membershipToModify.workspace)
if (!relatedWorkspace) {
throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
}
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, 'slug')));
sanitizedMembershipPermissionsUnique.forEach(permission => {
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
throw BadRequestError({ message: "Unknown environment slug reference" })
}
})
// update the permissions
const updatedMembershipWithPermissions = await Membership.findByIdAndUpdate(
{ _id: membershipToModify._id },
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
{ new: true }
)
if (!updatedMembershipWithPermissions) {
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
}
res.send({
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions
})
}
@@ -15,7 +15,13 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId) .findById(secretSnapshotId)
.populate('secretVersions'); .populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
+155 -26
View File
@@ -1,39 +1,40 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { SecretVersion, Action } from '../models'; import { Action } from '../models';
import { import {
getLatestSecretVersionIds, getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds getLatestNSecretSecretVersionIds
} from '../helpers/secretVersion'; } 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 * Create an (audit) action for updating secrets
* add, delete, update, and read actions.
* @param {Object} obj * @param {Object} obj
* @param {String} obj.name - name of action * @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 * @returns {Action} action - new action
*/ */
const createActionSecretHelper = async ({ const createActionUpdateSecret = async ({
name, name,
userId, userId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: string; userId: Types.ObjectId;
workspaceId: string; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
let action; let action;
let latestSecretVersions;
try { try {
if (name === ACTION_UPDATE_SECRETS) { const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
// case: action is updating secrets
// -> add old and new secret versions
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds, secretIds,
n: 2 n: 2
})) }))
@@ -41,17 +42,7 @@ const createActionSecretHelper = async ({
oldSecretVersion: s.versions[0]._id, oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._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({ action = await new Action({
name, name,
user: userId, user: userId,
@@ -64,10 +55,148 @@ const createActionSecretHelper = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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'); throw new Error('Failed to create action');
} }
return action; return action;
} }
export { createActionSecretHelper }; export {
createActionHelper
};
@@ -0,0 +1,18 @@
import _ from "lodash";
import { Membership } from "../../models";
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) {
return false
}
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: action });
if (isDisallowed) {
return false
}
return true
}
+14 -4
View File
@@ -1,9 +1,19 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { import {
Log, Log,
IAction IAction
} from '../models'; } 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 ({ const createLogHelper = async ({
userId, userId,
workspaceId, workspaceId,
@@ -11,8 +21,8 @@ const createLogHelper = async ({
channel, channel,
ipAddress ipAddress
}: { }: {
userId: string; userId: Types.ObjectId;
workspaceId: string; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
ipAddress: string; ipAddress: string;
@@ -21,7 +31,7 @@ const createLogHelper = async ({
try { try {
log = await new Log({ log = await new Log({
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name), actionNames: actions.map((a) => a.name),
actions, actions,
channel, channel,
+18 -2
View File
@@ -1,10 +1,18 @@
import { Schema, model, Types } from 'mongoose'; 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 { export interface IAction {
name: string; name: string;
user?: Types.ObjectId, user?: Types.ObjectId,
workspace?: Types.ObjectId, workspace?: Types.ObjectId,
payload: { payload?: {
secretVersions?: Types.ObjectId[] secretVersions?: Types.ObjectId[]
} }
} }
@@ -13,7 +21,15 @@ const actionSchema = new Schema<IAction>(
{ {
name: { name: {
type: String, type: String,
required: true required: true,
enum: [
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS
]
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
+4
View File
@@ -1,5 +1,7 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
@@ -29,6 +31,8 @@ const logSchema = new Schema<ILog>(
actionNames: { actionNames: {
type: [String], type: [String],
enum: [ enum: [
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_READ_SECRETS, ACTION_READ_SECRETS,
+7 -1
View File
@@ -21,6 +21,7 @@ export interface ISecretVersion {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
tags?: string[];
} }
const secretVersionSchema = new Schema<ISecretVersion>( const secretVersionSchema = new Schema<ISecretVersion>(
@@ -88,7 +89,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
}, },
secretValueHash: { secretValueHash: {
type: String type: String
} },
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
}, },
{ {
timestamps: true timestamps: true
+12 -14
View File
@@ -1,14 +1,12 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Log,
Action,
IAction IAction
} from '../models'; } from '../models';
import { import {
createLogHelper createLogHelper
} from '../helpers/log'; } from '../helpers/log';
import { import {
createActionSecretHelper createActionHelper
} from '../helpers/action'; } from '../helpers/action';
import EELicenseService from './EELicenseService'; import EELicenseService from './EELicenseService';
@@ -33,8 +31,8 @@ class EELogService {
channel, channel,
ipAddress ipAddress
}: { }: {
userId: string; userId: Types.ObjectId;
workspaceId: string; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
ipAddress: string; ipAddress: string;
@@ -50,26 +48,26 @@ class EELogService {
} }
/** /**
* Create an (audit) action for secrets including * Create an (audit) action
* add, delete, update, and read actions.
* @param {Object} obj * @param {Object} obj
* @param {String} obj.name - name of action * @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 * @returns {Action} action - new action
*/ */
static async createActionSecret({ static async createAction({
name, name,
userId, userId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: string; userId: Types.ObjectId;
workspaceId: string; workspaceId?: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) { }) {
if (!EELicenseService.isLicenseValid) return null; return await createActionHelper({
return await createActionSecretHelper({
name, name,
userId, userId,
workspaceId, workspaceId,
+42 -23
View File
@@ -30,6 +30,7 @@ interface Update {
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
const handleOAuthExchangeHelper = async ({ const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
@@ -42,7 +43,6 @@ const handleOAuthExchangeHelper = async ({
code: string; code: string;
environment: string; environment: string;
}) => { }) => {
let action;
let integrationAuth; let integrationAuth;
try { try {
const bot = await Bot.findOne({ const bot = await Bot.findOne({
@@ -94,25 +94,18 @@ const handleOAuthExchangeHelper = async ({
// set integration auth access token // set integration auth access token
await setIntegrationAuthAccessHelper({ await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken, accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt 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) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange') throw new Error('Failed to handle OAuth2 code-token exchange')
} }
return integrationAuth;
} }
/** /**
* Sync/push environment variables in workspace with id [workspaceId] to * 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'); if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token // get integration auth access token
const accessToken = await getIntegrationAuthAccessHelper({ const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString() integrationAuthId: integration.integrationAuth.toString()
}); });
@@ -155,7 +148,8 @@ const syncIntegrationsHelper = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessId: access.accessId,
accessToken: access.accessToken
}); });
} }
} catch (err) { } catch (err) {
@@ -211,12 +205,12 @@ const syncIntegrationsHelper = async ({
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessId;
let accessToken; let accessToken;
try { try {
const integrationAuth = await IntegrationAuth const integrationAuth = await IntegrationAuth
.findById(integrationAuthId) .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'}); 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) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -250,7 +253,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
throw new Error('Failed to get integration access token'); 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 * Encrypt access token [accessToken] and (optionally) access id [accessId]
* of the workspace key for workspace belonging to integration auth * using the bot's copy of the workspace key for workspace belonging to
* with id [integrationAuthId] and store it along with [accessExpiresAt] * integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessToken - access token * @param {String} obj.accessToken - access token
@@ -310,10 +316,12 @@ const setIntegrationAuthRefreshHelper = async ({
*/ */
const setIntegrationAuthAccessHelper = async ({ const setIntegrationAuthAccessHelper = async ({
integrationAuthId, integrationAuthId,
accessId,
accessToken, accessToken,
accessExpiresAt accessExpiresAt
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null;
accessToken: string; accessToken: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
@@ -323,17 +331,28 @@ const setIntegrationAuthAccessHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({ const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace.toString(),
plaintext: accessToken plaintext: accessToken
}); });
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId _id: integrationAuthId
}, { }, {
accessCiphertext: obj.ciphertext, accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIV: obj.iv, accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessTag: obj.tag, accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt accessExpiresAt
}, { }, {
new: true new: true
+77 -62
View File
@@ -12,14 +12,17 @@ import {
import { import {
IAction IAction
} from '../ee/models'; } from '../ee/models';
import { import {
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS ACTION_READ_SECRETS
} from '../variables'; } from '../variables';
import _ from 'lodash';
import { ABILITY_WRITE } from '../variables/organization';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/** /**
* Validate that user with id [userId] can modify secrets with ids [secretIds] * Validate that user with id [userId] can modify secrets with ids [secretIds]
@@ -34,7 +37,7 @@ const validateSecrets = async ({
}: { }: {
userId: string; userId: string;
secretIds: string[]; secretIds: string[];
}) =>{ }) => {
let secrets; let secrets;
try { try {
secrets = await Secret.find({ secrets = await Secret.find({
@@ -42,20 +45,31 @@ const validateSecrets = async ({
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
} }
}); });
const workspaceIdsSet = new Set((await Membership.find({ if (secrets.length != secretIds.length) {
user: userId throw BadRequestError({ message: 'Unable to validate some secrets' })
}, 'workspace')) }
.map((m) => m.workspace.toString()));
const userMemberships = await Membership.find({ user: userId })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => { secrets.forEach((secret: ISecret) => {
if (!workspaceIdsSet.has(secret.workspace.toString())) { if (workspaceIdsSet.has(secret.workspace.toString())) {
throw new Error('Failed to validate secret'); const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
if (isDisallowed) {
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
}
} else {
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
} }
}); });
} catch (err) { } catch (err) {
throw new Error('Failed to validate secrets'); throw BadRequestError({ message: 'Unable to validate secrets' })
} }
return secrets; return secrets;
@@ -127,13 +141,13 @@ const v1PushSecrets = async ({
workspaceId, workspaceId,
environment environment
}); });
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {}); , {});
const newSecretsObj: any = secrets.reduce((accumulator, s) => const newSecretsObj: any = secrets.reduce((accumulator, s) =>
({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
, {}); , {});
// handle deleting secrets // handle deleting secrets
const toDelete = oldSecrets const toDelete = oldSecrets
@@ -150,12 +164,12 @@ const v1PushSecrets = async ({
secretIds: toDelete secretIds: toDelete
}); });
} }
const toUpdate = oldSecrets const toUpdate = oldSecrets
.filter((s) => { .filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value or comment changed // case: filter secrets where value or comment changed
return true; return true;
} }
@@ -165,7 +179,7 @@ const v1PushSecrets = async ({
return true; return true;
} }
} }
return false; return false;
}); });
@@ -217,7 +231,7 @@ const v1PushSecrets = async ({
}; };
}); });
await Secret.bulkWrite(operations as any); await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets // (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: toUpdate.map(({ secretVersions: toUpdate.map(({
@@ -245,7 +259,7 @@ const v1PushSecrets = async ({
secretValueTag: newSecret.tagValue, secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue secretValueHash: newSecret.hashValue
}) })
}) })
}); });
// handle adding new secrets // handle adding new secrets
@@ -319,7 +333,7 @@ const v1PushSecrets = async ({
})) }))
}); });
} }
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId workspaceId
@@ -344,7 +358,7 @@ const v1PushSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets * @param {String} obj.ipAddress - ip address of request to push secrets
*/ */
const v2PushSecrets = async ({ const v2PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -362,20 +376,20 @@ const v1PushSecrets = async ({
// TODO: clean up function and fix up types // TODO: clean up function and fix up types
try { try {
const actions: IAction[] = []; const actions: IAction[] = [];
// construct useful data structures // construct useful data structures
const oldSecrets = await getSecrets({ const oldSecrets = await getSecrets({
userId, userId,
workspaceId, workspaceId,
environment environment
}); });
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {}); , {});
const newSecretsObj: any = secrets.reduce((accumulator, s) => const newSecretsObj: any = secrets.reduce((accumulator, s) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {}); , {});
// handle deleting secrets // handle deleting secrets
const toDelete = oldSecrets const toDelete = oldSecrets
@@ -391,22 +405,22 @@ const v1PushSecrets = async ({
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: toDelete secretIds: toDelete
}); });
const deleteAction = await EELogService.createActionSecret({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId, userId: new Types.ObjectId(userId),
workspaceId, workspaceId: new Types.ObjectId(userId),
secretIds: toDelete secretIds: toDelete
}); });
deleteAction && actions.push(deleteAction); deleteAction && actions.push(deleteAction);
} }
const toUpdate = oldSecrets const toUpdate = oldSecrets
.filter((s) => { .filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
// case: filter secrets where value or comment changed // case: filter secrets where value or comment changed
return true; return true;
} }
@@ -416,7 +430,7 @@ const v1PushSecrets = async ({
return true; return true;
} }
} }
return false; return false;
}); });
@@ -469,7 +483,7 @@ const v1PushSecrets = async ({
}; };
}); });
await Secret.bulkWrite(operations as any); await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets // (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({ await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => { secretVersions: toUpdate.map((s) => {
@@ -482,13 +496,13 @@ const v1PushSecrets = async ({
environment: s.environment, environment: s.environment,
isDeleted: false isDeleted: false
}) })
}) })
}); });
const updateAction = await EELogService.createActionSecret({ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
userId, userId: new Types.ObjectId(userId),
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
secretIds: toUpdate.map((u) => u._id) secretIds: toUpdate.map((u) => u._id)
}); });
@@ -507,29 +521,30 @@ const v1PushSecrets = async ({
workspace: workspaceId, workspace: workspaceId,
type: toAdd[idx].type, type: toAdd[idx].type,
environment, environment,
...( toAdd[idx].type === 'personal' ? { user: userId } : {}) ...(toAdd[idx].type === 'personal' ? { user: userId } : {})
})) }))
); );
// (EE) add secret versions for new secrets // (EE) add secret versions for new secrets
EESecretService.addSecretVersions({ EESecretService.addSecretVersions({
secretVersions: newSecrets.map((secretDocument) => { secretVersions: newSecrets.map((secretDocument) => {
return { return {
...secretDocument.toObject(), ...secretDocument.toObject(),
secret: secretDocument._id, secret: secretDocument._id,
isDeleted: false isDeleted: false
}}) }
})
}); });
const addAction = await EELogService.createActionSecret({ const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS, name: ACTION_ADD_SECRETS,
userId, userId: new Types.ObjectId(userId),
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
secretIds: newSecrets.map((n) => n._id) secretIds: newSecrets.map((n) => n._id)
}); });
addAction && actions.push(addAction); addAction && actions.push(addAction);
} }
// (EE) take a secret snapshot // (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId workspaceId
@@ -538,8 +553,8 @@ const v1PushSecrets = async ({
// (EE) create (audit) log // (EE) create (audit) log
if (actions.length > 0) { if (actions.length > 0) {
await EELogService.createLog({ await EELogService.createLog({
userId, userId: new Types.ObjectId(userId),
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
actions, actions,
channel, channel,
ipAddress ipAddress
@@ -560,7 +575,7 @@ const v1PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
*/ */
const getSecrets = async ({ const getSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment environment
@@ -570,7 +585,7 @@ const v1PushSecrets = async ({
environment: string; environment: string;
}): Promise<ISecret[]> => { }): Promise<ISecret[]> => {
let secrets: any; // TODO: FIX any let secrets: any; // TODO: FIX any
try { try {
// get shared workspace secrets // get shared workspace secrets
const sharedSecrets = await Secret.find({ const sharedSecrets = await Secret.find({
@@ -622,7 +637,7 @@ const pullSecrets = async ({
ipAddress: string; ipAddress: string;
}): Promise<ISecret[]> => { }): Promise<ISecret[]> => {
let secrets: any; let secrets: any;
try { try {
secrets = await getSecrets({ secrets = await getSecrets({
userId, userId,
@@ -630,16 +645,16 @@ const pullSecrets = async ({
environment environment
}) })
const readAction = await EELogService.createActionSecret({ const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS, name: ACTION_READ_SECRETS,
userId, userId: new Types.ObjectId(userId),
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
secretIds: secrets.map((n: any) => n._id) secretIds: secrets.map((n: any) => n._id)
}); });
readAction && await EELogService.createLog({ readAction && await EELogService.createLog({
userId, userId: new Types.ObjectId(userId),
workspaceId, workspaceId: new Types.ObjectId(workspaceId),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress ipAddress
+24 -5
View File
@@ -3,6 +3,9 @@ import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models'; import { IIntegrationAuth } from '../models';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -40,6 +43,15 @@ const getApps = async ({
let apps: App[]; let apps: App[];
try { try {
switch (integrationAuth.integration) { 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: case INTEGRATION_HEROKU:
apps = await getAppsHeroku({ apps = await getAppsHeroku({
accessToken accessToken
@@ -131,7 +143,8 @@ const getAppsVercel = async ({
const res = ( const res = (
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}, },
...( integrationAuth?.teamId ? { ...( integrationAuth?.teamId ? {
params: { params: {
@@ -140,7 +153,7 @@ const getAppsVercel = async ({
} : {}) } : {})
}) })
).data; ).data;
apps = res.projects.map((a: any) => ({ apps = res.projects.map((a: any) => ({
name: a.name name: a.name
})); }));
@@ -170,7 +183,8 @@ const getAppsNetlify = async ({
const res = ( const res = (
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
} }
}) })
).data; ).data;
@@ -247,7 +261,9 @@ const getAppsRender = async ({
const res = ( const res = (
await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json'
} }
}) })
).data; ).data;
@@ -257,6 +273,7 @@ const getAppsRender = async ({
name: a.service.name, name: a.service.name,
appId: a.service.id appId: a.service.id
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -296,7 +313,9 @@ const getAppsFlyio = async ({
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
method: 'post', method: 'post',
headers: { headers: {
'Authorization': 'Bearer ' + accessToken 'Authorization': 'Bearer ' + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
}, },
data: { data: {
query, query,
+64 -4
View File
@@ -1,10 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
@@ -12,15 +14,27 @@ import {
} from '../variables'; } from '../variables';
import { import {
SITE_URL, SITE_URL,
CLIENT_ID_AZURE,
CLIENT_ID_VERCEL, CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB, CLIENT_ID_GITHUB,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU, CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB CLIENT_SECRET_GITHUB
} from '../config'; } 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 { interface ExchangeCodeHerokuResponse {
token_type: string; token_type: string;
access_token: string; access_token: string;
@@ -75,6 +89,11 @@ const exchangeCode = async ({
try { try {
switch (integration) { switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code
});
break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({ obj = await exchangeCodeHeroku({
code code
@@ -105,6 +124,46 @@ const exchangeCode = async ({
return obj; 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 * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
* OAuth2 code-token exchange * OAuth2 code-token exchange
@@ -168,7 +227,7 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code: code, code: code,
client_id: CLIENT_ID_VERCEL, client_id: CLIENT_ID_VERCEL,
client_secret: CLIENT_SECRET_VERCEL, client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel` redirect_uri: `${SITE_URL}/integrations/vercel/oauth2/callback`
} as any) } as any)
) )
).data; ).data;
@@ -208,7 +267,7 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code: code, code: code,
client_id: CLIENT_ID_NETLIFY, client_id: CLIENT_ID_NETLIFY,
client_secret: CLIENT_SECRET_NETLIFY, client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify` redirect_uri: `${SITE_URL}/integrations/netlify/oauth2/callback`
} as any) } as any)
) )
).data; ).data;
@@ -260,10 +319,11 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
client_id: CLIENT_ID_GITHUB, client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB, client_secret: CLIENT_SECRET_GITHUB,
code: code, code: code,
redirect_uri: `${SITE_URL}/github` redirect_uri: `${SITE_URL}/integrations/github/oauth2/callback`
}, },
headers: { headers: {
Accept: 'application/json' 'Accept': 'application/json',
'Accept-Encoding': 'application/json'
} }
}) })
).data; ).data;
+65 -15
View File
@@ -1,13 +1,26 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { INTEGRATION_HEROKU } from '../variables'; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU } from '../variables';
import { import {
CLIENT_SECRET_HEROKU SITE_URL,
CLIENT_ID_AZURE,
CLIENT_SECRET_AZURE,
CLIENT_SECRET_HEROKU
} from '../config'; } from '../config';
import { import {
INTEGRATION_HEROKU_TOKEN_URL INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL
} from '../variables'; } 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 * Return new access token by exchanging refresh token [refreshToken] for integration
* named [integration] * named [integration]
@@ -25,6 +38,11 @@ const exchangeRefresh = async ({
let accessToken; let accessToken;
try { try {
switch (integration) { switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
accessToken = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({ accessToken = await exchangeRefreshHeroku({
refreshToken refreshToken
@@ -40,6 +58,38 @@ const exchangeRefresh = async ({
return accessToken; 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 * Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration * Heroku integration
@@ -52,23 +102,23 @@ const exchangeRefreshHeroku = async ({
}: { }: {
refreshToken: string; 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 let accessToken;
try { try {
const res = await axios.post( const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: refreshToken, refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU client_secret: CLIENT_SECRET_HEROKU
} as any) } as any)
); );
accessToken = res.data.access_token; accessToken = res.data.access_token;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); 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; return accessToken;
+369 -6
View File
@@ -1,11 +1,21 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; 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 { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers'; import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models'; import { IIntegration, IIntegrationAuth } from '../models';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -18,7 +28,6 @@ import {
INTEGRATION_RENDER_API_URL, INTEGRATION_RENDER_API_URL,
INTEGRATION_FLYIO_API_URL INTEGRATION_FLYIO_API_URL
} from '../variables'; } from '../variables';
import { access, appendFile } from 'fs';
/** /**
* Sync/push [secrets] to [app] in integration named [integration] * 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 {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 {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 * @param {String} obj.accessToken - access token for integration
*/ */
const syncSecrets = async ({ const syncSecrets = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessId,
accessToken accessToken
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
secrets: any; secrets: any;
accessId: string | null;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
switch (integration.integration) { 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: case INTEGRATION_HEROKU:
await syncSecretsHeroku({ await syncSecretsHeroku({
integration, 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] * Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj * @param {Object} obj
@@ -205,7 +567,7 @@ const syncSecretsVercel = async ({
...obj, ...obj,
[secret.key]: secret [secret.key]: secret
}), {}); }), {});
const updateSecrets: VercelSecret[] = []; const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = [];
@@ -736,8 +1098,9 @@ const syncSecretsFlyio = async ({
method: 'post', method: 'post',
url: INTEGRATION_FLYIO_API_URL, url: INTEGRATION_FLYIO_API_URL,
headers: { headers: {
'Authorization': 'Bearer ' + accessToken, 'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
}, },
data: { data: {
query: GetSecrets, query: GetSecrets,
@@ -45,9 +45,10 @@ const requireIntegrationAuthorizationAuth = ({
req.integrationAuth = integrationAuth; req.integrationAuth = integrationAuth;
if (attachAccessToken) { if (attachAccessToken) {
req.accessToken = await IntegrationService.getIntegrationAuthAccess({ const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString() integrationAuthId: integrationAuth._id.toString()
}); });
req.accessToken = access.accessToken;
} }
return next(); return next();
-23
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;
+4 -1
View File
@@ -16,6 +16,7 @@ import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace'; import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData'; import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
export { export {
BackupPrivateKey, BackupPrivateKey,
@@ -53,5 +54,7 @@ export {
ServiceTokenData, ServiceTokenData,
IServiceTokenData, IServiceTokenData,
APIKeyData, APIKeyData,
IAPIKeyData IAPIKeyData,
LoginSRPDetail,
ILoginSRPDetail
}; };
+28 -1
View File
@@ -1,5 +1,8 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -17,7 +20,18 @@ export interface IIntegration {
owner: string; owner: string;
targetEnvironment: string; targetEnvironment: string;
appId: 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; integrationAuth: Types.ObjectId;
} }
@@ -56,9 +70,22 @@ const integrationSchema = new Schema<IIntegration>(
type: String, type: String,
default: null default: null
}, },
path: {
// aws-parameter-store-specific path
type: String,
default: null
},
region: {
// aws-parameter-store-specific path
type: String,
default: null
},
integration: { integration: {
type: String, type: String,
enum: [ enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
+39 -5
View File
@@ -1,20 +1,37 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
} from '../variables'; } from '../variables';
export interface IIntegrationAuth { export interface IIntegrationAuth {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; integration:
teamId: string; | 'azure-key-vault'
accountId: string; | '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; refreshCiphertext?: string;
refreshIV?: string; refreshIV?: string;
refreshTag?: string; refreshTag?: string;
accessIdCiphertext?: string; // new
accessIdIV?: string; // new
accessIdTag?: string; // new
accessCiphertext?: string; accessCiphertext?: string;
accessIV?: string; accessIV?: string;
accessTag?: string; accessTag?: string;
@@ -31,10 +48,15 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
integration: { integration: {
type: String, type: String,
enum: [ enum: [
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB INTEGRATION_GITHUB,
INTEGRATION_RENDER,
INTEGRATION_FLYIO
], ],
required: true required: true
}, },
@@ -58,6 +80,18 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
type: String, type: String,
select: false select: false
}, },
accessIdCiphertext: {
type: String,
select: false
},
accessIdIV: {
type: String,
select: false
},
accessIdTag: {
type: String,
select: false
},
accessCiphertext: { accessCiphertext: {
type: String, type: String,
select: false select: false
+29
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;
+19 -1
View File
@@ -1,15 +1,21 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ADMIN, MEMBER } from '../variables'; import { ADMIN, MEMBER } from '../variables';
export interface IMembershipPermission {
environmentSlug: string,
ability: string
}
export interface IMembership { export interface IMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: 'admin' | 'member'; role: 'admin' | 'member';
deniedPermissions: IMembershipPermission[]
} }
const membershipSchema = new Schema( const membershipSchema = new Schema<IMembership>(
{ {
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
@@ -23,6 +29,18 @@ const membershipSchema = new Schema(
ref: 'Workspace', ref: 'Workspace',
required: true required: true
}, },
deniedPermissions: {
type: [
{
environmentSlug: String,
ability: {
type: String,
enum: ['read', 'write']
},
},
],
default: []
},
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER], enum: [ADMIN, MEMBER],
+9
View File
@@ -23,6 +23,7 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
tags?: string[];
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -47,6 +48,11 @@ const secretSchema = new Schema<ISecret>(
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User' ref: 'User'
}, },
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
environment: { environment: {
type: String, type: String,
required: true required: true
@@ -103,6 +109,9 @@ const secretSchema = new Schema<ISecret>(
} }
); );
secretSchema.index({ tags: 1 }, { background: true })
const Secret = model<ISecret>('Secret', secretSchema); const Secret = model<ISecret>('Secret', secretSchema);
export default Secret; export default Secret;
+49
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;
+3 -1
View File
@@ -17,6 +17,7 @@ export interface IUser {
verifier?: string; verifier?: string;
refreshVersion?: number; refreshVersion?: number;
isMfaEnabled: boolean; isMfaEnabled: boolean;
seenIps: [string];
} }
const userSchema = new Schema<IUser>( const userSchema = new Schema<IUser>(
@@ -80,7 +81,8 @@ const userSchema = new Schema<IUser>(
isMfaEnabled: { isMfaEnabled: {
type: Boolean, type: Boolean,
default: false default: false
} },
seenIps: [String]
}, },
{ {
timestamps: true timestamps: true
+5
View File
@@ -8,6 +8,7 @@ export interface IWorkspace {
name: string; name: string;
slug: string; slug: string;
}>; }>;
autoCapitalization: boolean;
} }
const workspaceSchema = new Schema<IWorkspace>({ const workspaceSchema = new Schema<IWorkspace>({
@@ -15,6 +16,10 @@ const workspaceSchema = new Schema<IWorkspace>({
type: String, type: String,
required: true required: true
}, },
autoCapitalization: {
type: Boolean,
default: true,
},
organization: { organization: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Organization', ref: 'Organization',
+10 -2
View File
@@ -10,7 +10,7 @@ import { ADMIN, MEMBER } from '../../variables';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1'; import { integrationController } from '../../controllers/v1';
router.post( // new: add new integration router.post( // new: add new integration for integration auth
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
@@ -19,7 +19,15 @@ router.post( // new: add new integration
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' 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, validateRequest,
integrationController.createIntegration integrationController.createIntegration
); );
+14
View File
@@ -18,6 +18,19 @@ router.get(
integrationAuthController.getIntegrationOptions integrationAuthController.getIntegrationOptions
); );
router.get(
'/:integrationAuthId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationAuthId'),
validateRequest,
integrationAuthController.getIntegrationAuth
);
router.post( router.post(
'/oauth-token', '/oauth-token',
requireAuth({ requireAuth({
@@ -44,6 +57,7 @@ router.post(
location: 'body' location: 'body'
}), }),
body('workspaceId').exists().trim().notEmpty(), body('workspaceId').exists().trim().notEmpty(),
body('accessId').trim(),
body('accessToken').exists().trim().notEmpty(), body('accessToken').exists().trim().notEmpty(),
body('integration').exists().trim().notEmpty(), body('integration').exists().trim().notEmpty(),
validateRequest, validateRequest,
+18 -6
View File
@@ -3,14 +3,15 @@ const router = express.Router();
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../../controllers/v1'; import { membershipController } from '../../controllers/v1';
import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1';
// note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId)
router.get( // used for old CLI (deprecate) router.get( // used for old CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
validateRequest, validateRequest,
membershipController.validateMembership membershipController.validateMembership
@@ -19,8 +20,8 @@ router.get( // used for old CLI (deprecate)
router.delete( router.delete(
'/:membershipId', '/:membershipId',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
validateRequest, validateRequest,
membershipController.deleteMembership membershipController.deleteMembership
@@ -29,11 +30,22 @@ router.delete(
router.post( router.post(
'/:membershipId/change-role', '/:membershipId/change-role',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
}), }),
body('role').exists().trim(), body('role').exists().trim(),
validateRequest, validateRequest,
membershipController.changeMembershipRole membershipController.changeMembershipRole
); );
router.post(
'/:membershipId/deny-permissions',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('membershipId').isMongoId().exists().trim(),
body('permissions').isArray().exists(),
validateRequest,
EEMembershipControllers.denyMembershipPermissions
);
export default router; export default router;
+13
View File
@@ -54,4 +54,17 @@ router.delete(
environmentController.deleteWorkspaceEnvironment environmentController.deleteWorkspaceEnvironment
); );
router.get(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [MEMBER, ADMIN],
}),
param('workspaceId').exists().trim(),
validateRequest,
environmentController.getAllAccessibleEnvironmentsOfWorkspace
);
export default router; export default router;
+3 -1
View File
@@ -8,6 +8,7 @@ import secrets from './secrets';
import serviceTokenData from './serviceTokenData'; import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData'; import apiKeyData from './apiKeyData';
import environment from "./environment" import environment from "./environment"
import tags from "./tags"
export { export {
auth, auth,
@@ -19,5 +20,6 @@ export {
secrets, secrets,
serviceTokenData, serviceTokenData,
apiKeyData, apiKeyData,
environment environment,
tags
} }
+1 -1
View File
@@ -30,7 +30,7 @@ router.patch(
'/:organizationId/memberships/:membershipId', '/:organizationId/memberships/:membershipId',
param('organizationId').exists().trim(), param('organizationId').exists().trim(),
param('membershipId').exists().trim(), param('membershipId').exists().trim(),
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]), body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
+50
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;
+15
View File
@@ -118,4 +118,19 @@ router.delete( // TODO - rewire dashboard to this route
workspaceController.deleteWorkspaceMembership 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; export default router;
+13 -13
View File
@@ -1,7 +1,3 @@
import * as Sentry from '@sentry/node';
import {
Integration
} from '../models';
import { import {
handleOAuthExchangeHelper, handleOAuthExchangeHelper,
syncIntegrationsHelper, syncIntegrationsHelper,
@@ -10,7 +6,6 @@ import {
setIntegrationAuthRefreshHelper, setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper, setIntegrationAuthAccessHelper,
} from '../helpers/integration'; } from '../helpers/integration';
import { exchangeCode } from '../integrations';
// should sync stuff be here too? Probably. // should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService. // 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 * - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
* - Add placeholder inactive integration * - Add placeholder inactive integration
* - Create bot sequence for integration * - Create bot sequence for integration
* @param {Object} obj * @param {Object} obj1
* @param {String} obj.workspaceId - id of workspace * @param {String} obj1.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment * @param {String} obj1.environment - workspace environment
* @param {String} obj.integration - name of integration * @param {String} obj1.integration - name of integration
* @param {String} obj.code - code * @param {String} obj1.code - code
* @returns {IntegrationAuth} integrationAuth - integration authorization after OAuth2 code-token exchange
*/ */
static async handleOAuthExchange({ static async handleOAuthExchange({
workspaceId, workspaceId,
@@ -43,7 +39,7 @@ class IntegrationService {
code: string; code: string;
environment: string; environment: string;
}) { }) {
await handleOAuthExchangeHelper({ return await handleOAuthExchangeHelper({
workspaceId, workspaceId,
integration, integration,
code, code,
@@ -116,26 +112,30 @@ class IntegrationService {
} }
/** /**
* Encrypt access token [accessToken] using the bot's copy * Encrypt access token [accessToken] and (optionally) access id using the
* of the workspace key for workspace belonging to integration auth * bot's copy of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId] * with id [integrationAuthId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessId - access id
* @param {String} obj.accessToken - access token * @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token * @param {Date} obj.accessExpiresAt - expiration date of access token
* @returns {IntegrationAuth} - updated integration auth * @returns {IntegrationAuth} - updated integration auth
*/ */
static async setIntegrationAuthAccess({ static async setIntegrationAuthAccess({
integrationAuthId, integrationAuthId,
accessId,
accessToken, accessToken,
accessExpiresAt accessExpiresAt
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null;
accessToken: string; accessToken: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) { }) {
return await setIntegrationAuthAccessHelper({ return await setIntegrationAuthAccessHelper({
integrationAuthId, integrationAuthId,
accessId,
accessToken, accessToken,
accessExpiresAt accessExpiresAt
}); });
@@ -8,11 +8,9 @@
</head> </head>
<body> <body>
<h2>Infisical</h2>
<h2>Confirm your email address</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>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
</p> <h1>{{code}}</h1>
<h2>{{code}}</h2>
<p>Questions about setting up Infisical? Email us at [email protected]</p> <p>Questions about setting up Infisical? Email us at [email protected]</p>
</body> </body>
@@ -7,12 +7,10 @@
<title>Organization Invitation</title> <title>Organization Invitation</title>
</head> </head>
<body> <body>
<h2>Infisical</h2> <h2>Join your organization on Infisical</h2>
<h2>Join your team on Infisical</h2> <p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<p>{{inviterFirstName}}({{inviterEmail}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a> <a href="{{callback_url}}?token={{token}}&to={{email}}">Join now</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is a simple end-to-end encrypted solution that enables teams to sync and manage their environment <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
variables.</p>
</body> </body>
</html> </html>
@@ -6,7 +6,6 @@
<title>Account Recovery</title> <title>Account Recovery</title>
</head> </head>
<body> <body>
<h2>Infisical</h2>
<h2>Reset your password</h2> <h2>Reset your password</h2>
<p>Someone requested a password reset.</p> <p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a> <a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
@@ -6,11 +6,10 @@
<title>Project Invitation</title> <title>Project Invitation</title>
</head> </head>
<body> <body>
<h2>Infisical</h2>
<h2>Join your team on 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> <a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3> <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> </body>
</html> </html>
+4
View File
@@ -1,9 +1,13 @@
const ACTION_LOGIN = 'login';
const ACTION_LOGOUT = 'logout';
const ACTION_ADD_SECRETS = 'addSecrets'; const ACTION_ADD_SECRETS = 'addSecrets';
const ACTION_DELETE_SECRETS = 'deleteSecrets'; const ACTION_DELETE_SECRETS = 'deleteSecrets';
const ACTION_UPDATE_SECRETS = 'updateSecrets'; const ACTION_UPDATE_SECRETS = 'updateSecrets';
const ACTION_READ_SECRETS = 'readSecrets'; const ACTION_READ_SECRETS = 'readSecrets';
export { export {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
+12
View File
@@ -6,6 +6,9 @@ import {
ENV_SET ENV_SET
} from './environment'; } from './environment';
import { import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -14,6 +17,7 @@ import {
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
@@ -35,6 +39,8 @@ import {
import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; import { SECRET_SHARED, SECRET_PERSONAL } from './secret';
import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event';
import { import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
@@ -65,6 +71,9 @@ export {
ENV_STAGING, ENV_STAGING,
ENV_PROD, ENV_PROD,
ENV_SET, ENV_SET,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -73,6 +82,7 @@ export {
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
@@ -84,6 +94,8 @@ export {
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS, EVENT_PULL_SECRETS,
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS, ACTION_ADD_SECRETS,
ACTION_UPDATE_SECRETS, ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
+42 -19
View File
@@ -1,3 +1,7 @@
import {
CLIENT_ID_AZURE,
TENANT_ID_AZURE
} from '../config';
import { import {
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
@@ -6,6 +10,9 @@ import {
} from '../config'; } from '../config';
// integrations // 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_HEROKU = 'heroku';
const INTEGRATION_VERCEL = 'vercel'; const INTEGRATION_VERCEL = 'vercel';
const INTEGRATION_NETLIFY = 'netlify'; const INTEGRATION_NETLIFY = 'netlify';
@@ -13,6 +20,7 @@ const INTEGRATION_GITHUB = 'github';
const INTEGRATION_RENDER = 'render'; const INTEGRATION_RENDER = 'render';
const INTEGRATION_FLYIO = 'flyio'; const INTEGRATION_FLYIO = 'flyio';
const INTEGRATION_SET = new Set([ const INTEGRATION_SET = new Set([
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -25,6 +33,7 @@ const INTEGRATION_SET = new Set([
const INTEGRATION_OAUTH2 = 'oauth2'; const INTEGRATION_OAUTH2 = 'oauth2';
// integration oauth endpoints // 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_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
const INTEGRATION_VERCEL_TOKEN_URL = const INTEGRATION_VERCEL_TOKEN_URL =
'https://api.vercel.com/v2/oauth/access_token'; 'https://api.vercel.com/v2/oauth/access_token';
@@ -90,11 +99,39 @@ const INTEGRATION_OPTIONS = [
name: 'Fly.io', name: 'Fly.io',
slug: 'flyio', slug: 'flyio',
image: 'Flyio.svg', image: 'Flyio.svg',
isAvailable: false, isAvailable: true,
type: 'pat', type: 'pat',
clientId: '', clientId: '',
docsLink: '' 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', name: 'Google Cloud Platform',
slug: 'gcp', slug: 'gcp',
@@ -104,24 +141,6 @@ const INTEGRATION_OPTIONS = [
clientId: '', clientId: '',
docsLink: '' 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', name: 'Travis CI',
slug: 'travisci', slug: 'travisci',
@@ -143,6 +162,9 @@ const INTEGRATION_OPTIONS = [
] ]
export { export {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -151,6 +173,7 @@ export {
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_SET, INTEGRATION_SET,
INTEGRATION_OAUTH2, INTEGRATION_OAUTH2,
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
+7 -1
View File
@@ -6,6 +6,10 @@ const MEMBER = 'member';
// membership statuses // membership statuses
const INVITED = 'invited'; const INVITED = 'invited';
// membership permissions ability
const ABILITY_READ = 'read';
const ABILITY_WRITE = 'write';
// -- organization // -- organization
const ACCEPTED = 'accepted'; const ACCEPTED = 'accepted';
@@ -14,5 +18,7 @@ export {
ADMIN, ADMIN,
MEMBER, MEMBER,
INVITED, INVITED,
ACCEPTED ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
} }
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package main package main
+43
View File
@@ -5,6 +5,7 @@ import (
"github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/config"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
) )
const USER_AGENT = "cli" const USER_AGENT = "cli"
@@ -144,3 +145,45 @@ func CallGetAllWorkSpacesUserBelongsTo(httpClient *resty.Client) (GetWorkSpacesR
return workSpacesResponse, nil return workSpacesResponse, nil
} }
func CallIsAuthenticated(httpClient *resty.Client) bool {
var workSpacesResponse GetWorkSpacesResponse
response, err := httpClient.
R().
SetResult(&workSpacesResponse).
SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
if err != nil {
return false
}
if response.IsError() {
return false
}
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
}
+36 -15
View File
@@ -201,21 +201,30 @@ type GetEncryptedSecretsV2Request struct {
type GetEncryptedSecretsV2Response struct { type GetEncryptedSecretsV2Response struct {
Secrets []struct { Secrets []struct {
ID string `json:"_id"` ID string `json:"_id"`
Version int `json:"version"` Version int `json:"version"`
Workspace string `json:"workspace"` Workspace string `json:"workspace"`
Type string `json:"type"` Type string `json:"type"`
Environment string `json:"environment"` Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"` SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"` SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"` SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"` SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"` SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"` SecretValueTag string `json:"secretValueTag"`
V int `json:"__v"` SecretCommentCiphertext string `json:"secretCommentCiphertext"`
CreatedAt time.Time `json:"createdAt"` SecretCommentIV string `json:"secretCommentIV"`
UpdatedAt time.Time `json:"updatedAt"` SecretCommentTag string `json:"secretCommentTag"`
User string `json:"user,omitempty"` 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"` } `json:"secrets"`
} }
@@ -241,3 +250,15 @@ type GetServiceTokenDetailsResponse struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"` 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"`
}
+8 -2
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
@@ -56,7 +56,12 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") 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 { if err != nil {
util.HandleError(err, "Unable to fetch secrets") 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().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().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().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 // Format according to the format flag
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
+6 -3
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
@@ -33,13 +33,13 @@ var loginCmd = &cobra.Command{
PreRun: toggleDebug, PreRun: toggleDebug,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil && strings.Contains(err.Error(), "The specified item could not be found in the keyring") { // if the key can't be found allow them to override if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring")) { // if the key can't be found allow them to override
log.Debug(err) log.Debug(err)
} else if err != nil { } else if err != nil {
util.HandleError(err) util.HandleError(err)
} }
if currentLoggedInUserDetails.IsUserLoggedIn { if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired { // if you are logged in but not expired
shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email) shouldOverride, err := shouldOverrideLoginPrompt(currentLoggedInUserDetails.UserCredentials.Email)
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
@@ -101,6 +101,9 @@ var loginCmd = &cobra.Command{
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again") 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) color.Green("Nice! You are logged in as: %v", email)
}, },
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
+45
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)
}
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
+8 -4
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
@@ -12,6 +12,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color" "github.com/fatih/color"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -58,8 +59,9 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
if !util.IsSecretEnvironmentValid(envName) { infisicalToken, err := cmd.Flags().GetString("token")
util.PrintMessageAndExit("Invalid environment name passed. Environment names can only be prod, dev, test or staging") if err != nil {
util.HandleError(err, "Unable to parse flag")
} }
secretOverriding, err := cmd.Flags().GetBool("secret-overriding") secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
@@ -72,7 +74,8 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") 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 { 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") 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() { func init() {
rootCmd.AddCommand(runCmd) 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().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("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") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
+219 -30
View File
@@ -1,11 +1,13 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"regexp"
"sort"
"strings" "strings"
"unicode" "unicode"
@@ -22,7 +24,7 @@ import (
) )
var secretsCmd = &cobra.Command{ var secretsCmd = &cobra.Command{
Example: `infisical secrets"`, Example: `infisical secrets`,
Short: "Used to create, read update and delete secrets", Short: "Used to create, read update and delete secrets",
Use: "secrets", Use: "secrets",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
@@ -34,12 +36,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err) 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") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
secrets, err := util.GetAllEnvironmentVariables(environmentName) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken})
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -62,6 +69,16 @@ var secretsGetCmd = &cobra.Command{
Run: getSecretsByNames, 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{ var secretsSetCmd = &cobra.Command{
Example: `secrets set <secretName=secretValue> <secretName=secretValue>..."`, Example: `secrets set <secretName=secretValue> <secretName=secretValue>..."`,
Short: "Used set secrets", Short: "Used set secrets",
@@ -111,7 +128,7 @@ var secretsSetCmd = &cobra.Command{
plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
// pull current secrets // pull current secrets
secrets, err := util.GetAllEnvironmentVariables(environmentName) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName})
if err != nil { if err != nil {
util.HandleError(err, "unable to retrieve secrets") util.HandleError(err, "unable to retrieve secrets")
} }
@@ -267,7 +284,7 @@ var secretsDeleteCmd = &cobra.Command{
util.HandleError(err, "Unable to get local project details") 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 { if err != nil {
util.HandleError(err, "Unable to fetch secrets") 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) { func getSecretsByNames(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, err := cmd.Flags().GetString("env")
if err != nil { if err != nil {
@@ -344,7 +337,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
util.HandleError(err, "Unable to parse flag") 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 { if err != nil {
util.HandleError(err, "To fetch all secrets") util.HandleError(err, "To fetch all secrets")
} }
@@ -371,6 +369,171 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
visualize.PrintAllSecretDetails(requestedSecrets) 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 { func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable {
secretMapByName := make(map[string]models.SingleEnvironmentVariable) secretMapByName := make(map[string]models.SingleEnvironmentVariable)
@@ -380,3 +543,29 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod
return secretMapByName 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)
}
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 NAME HERE <EMAIL ADDRESS> Copyright (c) 2023 Infisical Inc.
*/ */
package cmd package cmd
+5
View File
@@ -12,6 +12,11 @@ import (
// will decrypt cipher text to plain text using iv and tag // will decrypt cipher text to plain text using iv and tag
func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { 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) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil, err return nil, err
+18 -4
View File
@@ -1,6 +1,8 @@
package models package models
import "github.com/99designs/keyring" import (
"github.com/99designs/keyring"
)
type UserCredentials struct { type UserCredentials struct {
Email string `json:"email"` Email string `json:"email"`
@@ -19,6 +21,13 @@ type SingleEnvironmentVariable struct {
Value string `json:"value"` Value string `json:"value"`
Type string `json:"type"` Type string `json:"type"`
ID string `json:"_id"` 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 { type Workspace struct {
@@ -34,7 +43,12 @@ type WorkspaceConfigFile struct {
} }
type SymmetricEncryptionResult struct { type SymmetricEncryptionResult struct {
CipherText []byte CipherText []byte `json:"CipherText"`
Nonce []byte Nonce []byte `json:"Nonce"`
AuthTag []byte AuthTag []byte `json:"AuthTag"`
}
type GetAllSecretsParameters struct {
Environment string
InfisicalToken string
} }
+2 -2
View File
@@ -2,10 +2,10 @@ package util
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"errors"
) )
func CheckForUpdate() { func CheckForUpdate() {
@@ -42,5 +42,5 @@ func getLatestTag(repoOwner string, repoName string) (string, error) {
json.Unmarshal(body, &tags) json.Unmarshal(body, &tags)
return tags[0].Name, nil return tags[0].Name[1:], nil
} }
+9
View File
@@ -2,6 +2,7 @@ package util
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
) )
@@ -19,3 +20,11 @@ func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) erro
return nil return nil
} }
func CheckIsConnectedToInternet() (ok bool) {
_, err := http.Get("http://clients3.google.com/generate_204")
if err != nil {
return false
}
return true
}
+4 -1
View File
@@ -11,5 +11,8 @@ const (
KEYRING_SERVICE_NAME = "infisical" KEYRING_SERVICE_NAME = "infisical"
PERSONAL_SECRET_TYPE_NAME = "personal" PERSONAL_SECRET_TYPE_NAME = "personal"
SHARED_SECRET_TYPE_NAME = "shared" SHARED_SECRET_TYPE_NAME = "shared"
CLI_VERSION = "v0.2.7" )
var (
CLI_VERSION = "devel"
) )
+4 -11
View File
@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"github.com/99designs/keyring" "github.com/99designs/keyring"
"github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/models"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
@@ -87,17 +87,10 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
SetAuthToken(userCreds.JTWToken). SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
response, err := httpClient. isAuthenticated := api.CallIsAuthenticated(httpClient)
R(). if !isAuthenticated {
Post(fmt.Sprintf("%v/v1/auth/checkAuth", config.INFISICAL_URL))
if err != nil {
return LoggedInUserDetails{}, err
}
if response.StatusCode() > 299 {
return LoggedInUserDetails{ return LoggedInUserDetails{
IsUserLoggedIn: true, IsUserLoggedIn: true, // was logged in
LoginExpired: true, LoginExpired: true,
UserCredentials: userCreds, UserCredentials: userCreds,
}, nil }, nil
+12
View File
@@ -1,6 +1,7 @@
package util package util
import ( import (
"crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os" "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]") 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)
}
+5 -1
View File
@@ -24,7 +24,11 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) {
} }
func PrintWarning(message 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) { func PrintMessageAndExit(messages ...string) {
+197 -12
View File
@@ -2,6 +2,8 @@ package util
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@@ -97,13 +99,26 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
return plainTextSecrets, nil return plainTextSecrets, nil
} }
func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVariable, error) { func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
infisicalToken := os.Getenv(INFISICAL_TOKEN_NAME) 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 == "" { if infisicalToken == "" {
RequireLocalWorkspaceFile() if isConnected {
RequireLogin() log.Debug("GetAllEnvironmentVariables: Connected to internet, checking logged in creds")
log.Debug("Trying to fetch secrets using logged in details") RequireLocalWorkspaceFile()
RequireLogin()
}
log.Debug("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails() loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
if err != nil { if err != nil {
@@ -115,13 +130,63 @@ func GetAllEnvironmentVariables(envName string) ([]models.SingleEnvironmentVaria
return nil, err return nil, err
} }
secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, envName) // Verify environment
return secrets, err 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 { } else {
log.Debug("Trying to fetch secrets using service token") 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 { 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") 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{ plainTextSecret := models.SingleEnvironmentVariable{
Key: string(plainTextKey), Key: string(plainTextKey),
Value: string(plainTextValue), Value: string(plainTextValue),
Type: string(secret.Type), Type: string(secret.Type),
ID: secret.ID, ID: secret.ID,
Tags: secret.Tags,
Comment: string(plainTextComment),
} }
plainTextSecrets = append(plainTextSecrets, plainTextSecret) plainTextSecrets = append(plainTextSecrets, plainTextSecret)
@@ -295,3 +383,100 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R
return plainTextSecrets, nil 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)
}
+2 -1
View File
@@ -57,7 +57,7 @@ func fileKeyringPassphrasePrompt(prompt string) (string, error) {
if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok { if password, ok := os.LookupEnv("INFISICAL_VAULT_FILE_PASSPHRASE"); ok {
return password, nil return password, nil
} else { } else {
fmt.Println("You may set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable to avoid typing password") fmt.Println("You may set the environment variable `INFISICAL_VAULT_FILE_PASSPHRASE` with your password to avoid typing it")
} }
fmt.Fprintf(os.Stderr, "%s:", prompt) fmt.Fprintf(os.Stderr, "%s:", prompt)
@@ -65,6 +65,7 @@ func fileKeyringPassphrasePrompt(prompt string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
fmt.Println("") fmt.Println("")
return string(b), nil return string(b), nil
} }
+1 -5
View File
@@ -46,11 +46,7 @@ services:
context: ./frontend context: ./frontend
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
volumes: volumes:
- ./frontend/src/pages:/app/src/pages - ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files
- ./frontend/src/components:/app/src/components
- ./frontend/src/ee:/app/src/ee
- ./frontend/src/locales:/app/src/locales
- ./frontend/src/styles:/app/src/styles
- ./frontend/public:/app/public - ./frontend/public:/app/public
- ./frontend/next-i18next.config.js:/app/next-i18next.config.js - ./frontend/next-i18next.config.js:/app/next-i18next.config.js
env_file: .env env_file: .env
+4 -2
View File
@@ -9,6 +9,8 @@ infisical init
## Description ## 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>
+11
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.
+53 -8
View File
@@ -25,13 +25,58 @@ description: "The command that injects your secrets into local environment"
## Description ## Description
Inject environment variables from the platform into an application process. Inject secrets from Infisical into your application process.
## Options
| Option | Description | Default value | ## Subcommands & flags
| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | <Accordion title="infisical run" defaultOpen="true">
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | Use this command to inject secrets into your applications process
| `--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` | ```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>
+26 -27
View File
@@ -14,17 +14,8 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
<Accordion title="infisical secrets" defaultOpen="true"> <Accordion title="infisical secrets" defaultOpen="true">
Use this command to print out all of the secrets in your project Use this command to print out all of the secrets in your project
``` ```bash
$ infisical secrets $ infisical secrets
## Example
$ infisical secrets
┌─────────────┬──────────────┬─────────────┐
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
├─────────────┼──────────────┼─────────────┤
│ DOMAIN │ example.com │ shared │
│ HASH │ jebhfbwe │ shared │
└─────────────┴──────────────┴─────────────┘
``` ```
### flags ### flags
@@ -45,16 +36,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio
<Accordion title="infisical secrets get"> <Accordion title="infisical secrets get">
This command allows you selectively print the requested secrets by name This command allows you selectively print the requested secrets by name
``` ```bash
$ infisical secrets get <secret-name-a> <secret-name-b> ... $ infisical secrets get <secret-name-a> <secret-name-b> ...
# Example # Example
$ infisical secrets get DOMAIN $ 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. 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. 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>... $ infisical secrets set <key1=value1> <key2=value2>...
## Example ## Example
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe $ 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 ### Flags
@@ -95,12 +74,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
<Accordion title="infisical secrets delete"> <Accordion title="infisical secrets delete">
This command allows you to delete secrets by their name(s). This command allows you to delete secrets by their name(s).
``` ```bash
$ infisical secrets delete <keyName1> <keyName2>... $ infisical secrets delete <keyName1> <keyName2>...
## Example ## Example
$ infisical secrets delete STRIPE_API_KEY DOMAIN HASH $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH
secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project
``` ```
### Flags ### Flags
@@ -109,4 +87,25 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
Default value: `dev` Default value: `dev`
</Accordion> </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>
+5
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. If you are still experiencing trouble, please seek support.
[Learn more about vault command](./commands/vault) [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> </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

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