Update package.json
@@ -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
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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');
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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']
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
Copyright (c) 2023 Infisical Inc.
|
||||||
*/
|
*/
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
Copyright (c) 2023 Infisical Inc.
|
||||||
*/
|
*/
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
Copyright (c) 2023 Infisical Inc.
|
||||||
*/
|
*/
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
Copyright (c) 2023 Infisical Inc.
|
||||||
*/
|
*/
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
|
Copyright (c) 2023 Infisical Inc.
|
||||||
*/
|
*/
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 219 KiB |