mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Fixing merge conflicts for pages/users
This commit is contained in:
1
.github/workflows/release_build.yml
vendored
1
.github/workflows/release_build.yml
vendored
@@ -41,6 +41,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
|
||||
@@ -30,13 +30,13 @@ builds:
|
||||
- openbsd
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- "386"
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
- "6"
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
@@ -85,7 +85,7 @@ nfpms:
|
||||
homepage: https://infisical.com/
|
||||
maintainer: Infisical, Inc
|
||||
description: The offical Infisical CLI
|
||||
license: Apache 2.0
|
||||
license: MIT
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
@@ -101,7 +101,23 @@ scoop:
|
||||
email: ai@infisical.com
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
license: Apache-2.0
|
||||
license: MIT
|
||||
aurs:
|
||||
-
|
||||
name: infisical-bin
|
||||
homepage: "https://infisical.com"
|
||||
description: "The official Infisical CLI"
|
||||
maintainers:
|
||||
- Infisical, Inc <support@infisical.com>
|
||||
license: MIT
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
|
||||
package: |-
|
||||
# bin
|
||||
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
||||
# license
|
||||
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
|
||||
|
||||
# dockers:
|
||||
# - dockerfile: goreleaser.dockerfile
|
||||
# goos: linux
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
||||
- **Navigate Multiple Environments** per project (e.g. development, staging, production, etc.)
|
||||
- **Personal/Shared** scoping for environment variables
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure (Heroku available, more coming soon)
|
||||
- **[Integrations](https://infisical.com/docs/integrations/overview)** with CI/CD and production infrastructure
|
||||
- 🔜 **1-Click Deploy** to Digital Ocean and Heroku
|
||||
- 🔜 **Authentication/Authorization** for projects (read/write controls soon)
|
||||
- 🔜 **Automatic Secret Rotation**
|
||||
@@ -321,4 +321,4 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jon4hz"><img src="https://avatars.githubusercontent.com/u/26183582?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||
|
||||
import { patchRouterParam } from './utils/patchAsyncRoutes';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
@@ -11,30 +12,39 @@ import { PORT, NODE_ENV, SITE_URL } from './config';
|
||||
import { apiLimiter } from './helpers/rateLimiter';
|
||||
|
||||
import {
|
||||
signup as signupRouter,
|
||||
auth as authRouter,
|
||||
bot as botRouter,
|
||||
organization as organizationRouter,
|
||||
workspace as workspaceRouter,
|
||||
membershipOrg as membershipOrgRouter,
|
||||
membership as membershipRouter,
|
||||
key as keyRouter,
|
||||
inviteOrg as inviteOrgRouter,
|
||||
user as userRouter,
|
||||
userAction as userActionRouter,
|
||||
secret as secretRouter,
|
||||
serviceToken as serviceTokenRouter,
|
||||
password as passwordRouter,
|
||||
stripe as stripeRouter,
|
||||
integration as integrationRouter,
|
||||
integrationAuth as integrationAuthRouter
|
||||
} from './routes';
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter
|
||||
} from './ee/routes/v1';
|
||||
import {
|
||||
signup as v1SignupRouter,
|
||||
auth as v1AuthRouter,
|
||||
bot as v1BotRouter,
|
||||
organization as v1OrganizationRouter,
|
||||
workspace as v1WorkspaceRouter,
|
||||
membershipOrg as v1MembershipOrgRouter,
|
||||
membership as v1MembershipRouter,
|
||||
key as v1KeyRouter,
|
||||
inviteOrg as v1InviteOrgRouter,
|
||||
user as v1UserRouter,
|
||||
userAction as v1UserActionRouter,
|
||||
secret as v1SecretRouter,
|
||||
serviceToken as v1ServiceTokenRouter,
|
||||
password as v1PasswordRouter,
|
||||
stripe as v1StripeRouter,
|
||||
integration as v1IntegrationRouter,
|
||||
integrationAuth as v1IntegrationAuthRouter
|
||||
} from './routes/v1';
|
||||
import {
|
||||
secret as v2SecretRouter,
|
||||
workspace as v2WorkspaceRouter
|
||||
} from './routes/v2';
|
||||
|
||||
import { getLogger } from './utils/logger';
|
||||
import { RouteNotFoundError } from './utils/errors';
|
||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||
|
||||
//* Patch Async route params to handle Promise Rejections
|
||||
patchRouterParam()
|
||||
// patch async route params to handle Promise Rejections
|
||||
patchRouterParam();
|
||||
|
||||
export const app = express();
|
||||
|
||||
@@ -56,24 +66,32 @@ if (NODE_ENV === 'production') {
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
// routers
|
||||
app.use('/api/v1/signup', signupRouter);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
app.use('/api/v1/bot', botRouter);
|
||||
app.use('/api/v1/user', userRouter);
|
||||
app.use('/api/v1/user-action', userActionRouter);
|
||||
app.use('/api/v1/organization', organizationRouter);
|
||||
app.use('/api/v1/workspace', workspaceRouter);
|
||||
app.use('/api/v1/membership-org', membershipOrgRouter);
|
||||
app.use('/api/v1/membership', membershipRouter);
|
||||
app.use('/api/v1/key', keyRouter);
|
||||
app.use('/api/v1/invite-org', inviteOrgRouter);
|
||||
app.use('/api/v1/secret', secretRouter);
|
||||
app.use('/api/v1/service-token', serviceTokenRouter);
|
||||
app.use('/api/v1/password', passwordRouter);
|
||||
app.use('/api/v1/stripe', stripeRouter);
|
||||
app.use('/api/v1/integration', integrationRouter);
|
||||
app.use('/api/v1/integration-auth', integrationAuthRouter);
|
||||
// (EE) routes
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
|
||||
// v1 routes
|
||||
app.use('/api/v1/signup', v1SignupRouter);
|
||||
app.use('/api/v1/auth', v1AuthRouter);
|
||||
app.use('/api/v1/bot', v1BotRouter);
|
||||
app.use('/api/v1/user', v1UserRouter);
|
||||
app.use('/api/v1/user-action', v1UserActionRouter);
|
||||
app.use('/api/v1/organization', v1OrganizationRouter);
|
||||
app.use('/api/v1/workspace', v1WorkspaceRouter);
|
||||
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
|
||||
app.use('/api/v1/membership', v1MembershipRouter);
|
||||
app.use('/api/v1/key', v1KeyRouter);
|
||||
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||
app.use('/api/v1/secret', v1SecretRouter);
|
||||
app.use('/api/v1/service-token', v1ServiceTokenRouter);
|
||||
app.use('/api/v1/password', v1PasswordRouter);
|
||||
app.use('/api/v1/stripe', v1StripeRouter);
|
||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
|
||||
// v2 routes
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||
app.use('/api/v2/secret', v2SecretRouter);
|
||||
|
||||
|
||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||
|
||||
@@ -41,6 +41,7 @@ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
||||
const LICENSE_KEY = process.env.LICENSE_KEY!;
|
||||
|
||||
export {
|
||||
PORT,
|
||||
@@ -83,5 +84,6 @@ export {
|
||||
STRIPE_PUBLISHABLE_KEY,
|
||||
STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
TELEMETRY_ENABLED
|
||||
TELEMETRY_ENABLED,
|
||||
LICENSE_KEY
|
||||
};
|
||||
|
||||
@@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
const jsrp = require('jsrp');
|
||||
import { User } from '../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
|
||||
import { User } from '../../models';
|
||||
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
|
||||
import {
|
||||
NODE_ENV,
|
||||
JWT_AUTH_LIFETIME,
|
||||
JWT_AUTH_SECRET,
|
||||
JWT_REFRESH_SECRET
|
||||
} from '../config';
|
||||
} from '../../config';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Bot, BotKey } from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
import { Bot, BotKey } from '../../models';
|
||||
import { createBot } from '../../helpers/bot';
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
@@ -2,10 +2,10 @@ import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
import { getApps, revokeAccess } from '../integrations';
|
||||
import { IntegrationAuth, Integration } from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
|
||||
export const getIntegrationOptions = async (
|
||||
req: Request,
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Integration, Bot, BotKey } from '../models';
|
||||
import { EventService } from '../services';
|
||||
import { eventPushSecrets } from '../events';
|
||||
import { Integration, Bot, BotKey } from '../../models';
|
||||
import { EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
|
||||
interface Key {
|
||||
encryptedKey: string;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key } from '../models';
|
||||
import { findMembership } from '../helpers/membership';
|
||||
import { GRANTED } from '../variables';
|
||||
import { Key } from '../../models';
|
||||
import { findMembership } from '../../helpers/membership';
|
||||
import { GRANTED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Membership, MembershipOrg, User, Key } from '../models';
|
||||
import { Membership, MembershipOrg, User, Key } from '../../models';
|
||||
import {
|
||||
findMembership,
|
||||
deleteMembership as deleteMember
|
||||
} from '../helpers/membership';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { SITE_URL } from '../config';
|
||||
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
|
||||
} from '../../helpers/membership';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { SITE_URL } from '../../config';
|
||||
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Check that user is a member of workspace with id [workspaceId]
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
||||
import { MembershipOrg, Organization, User, Token } from '../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { updateSubscriptionOrgQuantity } from '../helpers/organization';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables';
|
||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||
import { MembershipOrg, Organization, User, Token } from '../../models';
|
||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipOrgId] from organization
|
||||
@@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
||||
// TODO
|
||||
|
||||
let membershipToChangeRole;
|
||||
try {
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to change organization membership role'
|
||||
});
|
||||
}
|
||||
// try {
|
||||
// } catch (err) {
|
||||
// Sentry.setUser({ email: req.user.email });
|
||||
// Sentry.captureException(err);
|
||||
// return res.status(400).send({
|
||||
// message: 'Failed to change organization membership role'
|
||||
// });
|
||||
// }
|
||||
|
||||
return res.status(200).send({
|
||||
membershipOrg: membershipToChangeRole
|
||||
@@ -218,12 +218,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
const { email, code } = req.body;
|
||||
|
||||
user = await User.findOne({ email }).select('+publicKey');
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
return res.status(403).send({
|
||||
error: 'Failed email magic link verification for complete account'
|
||||
});
|
||||
}
|
||||
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
inviteEmail: email,
|
||||
@@ -238,6 +232,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||
code
|
||||
});
|
||||
|
||||
if (user && user?.publicKey) {
|
||||
// case: user has already completed account
|
||||
// membership can be approved and redirected to login/dashboard
|
||||
membershipOrg.status = ACCEPTED;
|
||||
await membershipOrg.save();
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully verified email',
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// initialize user account
|
||||
user = await new User({
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
STRIPE_PRODUCT_STARTER,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_CARD_AUTH
|
||||
} from '../config';
|
||||
} from '../../config';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
Organization,
|
||||
Workspace,
|
||||
IncidentContactOrg
|
||||
} from '../models';
|
||||
import { createOrganization as create } from '../helpers/organization';
|
||||
import { addMembershipsOrg } from '../helpers/membershipOrg';
|
||||
import { OWNER, ACCEPTED } from '../variables';
|
||||
} from '../../models';
|
||||
import { createOrganization as create } from '../../helpers/organization';
|
||||
import { addMembershipsOrg } from '../../helpers/membershipOrg';
|
||||
import { OWNER, ACCEPTED } from '../../variables';
|
||||
|
||||
const productToPriceMap = {
|
||||
starter: STRIPE_PRODUCT_STARTER,
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import crypto from 'crypto';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
import { User, Token, BackupPrivateKey } from '../models';
|
||||
import { checkEmailVerification } from '../helpers/signup';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { sendMail } from '../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
|
||||
import { User, Token, BackupPrivateKey } from '../../models';
|
||||
import { checkEmailVerification } from '../../helpers/signup';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { sendMail } from '../../helpers/nodemailer';
|
||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||
|
||||
const clientPublicKeys: any = {};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Key } from '../models';
|
||||
import { Key, Secret } from '../../models';
|
||||
import {
|
||||
pushSecrets as push,
|
||||
v1PushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
reformatPullSecrets
|
||||
} from '../helpers/secret';
|
||||
import { pushKeys } from '../helpers/key';
|
||||
import { eventPushSecrets } from '../events';
|
||||
import { EventService } from '../services';
|
||||
import { ENV_SET } from '../variables';
|
||||
import { postHogClient } from '../services';
|
||||
} from '../../helpers/secret';
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EventService } from '../../services';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { postHogClient } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
@@ -21,6 +21,10 @@ interface PushSecret {
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
ciphertextComment: string;
|
||||
ivComment: string;
|
||||
tagComment: string;
|
||||
hashComment: string;
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
@@ -169,9 +173,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
// get (encrypted) secrets from workspace with id [workspaceId]
|
||||
// service token route
|
||||
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
@@ -225,4 +226,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ServiceToken } from '../models';
|
||||
import { createToken } from '../helpers/auth';
|
||||
import { ENV_SET } from '../variables';
|
||||
import { JWT_SERVICE_SECRET } from '../config';
|
||||
import { ServiceToken } from '../../models';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { JWT_SERVICE_SECRET } from '../../config';
|
||||
|
||||
/**
|
||||
* Return service token on request
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
||||
import { User, MembershipOrg } from '../models';
|
||||
import { completeAccount } from '../helpers/user';
|
||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||
import { User, MembershipOrg } from '../../models';
|
||||
import { completeAccount } from '../../helpers/user';
|
||||
import {
|
||||
sendEmailVerification,
|
||||
checkEmailVerification,
|
||||
initializeDefaultOrg
|
||||
} from '../helpers/signup';
|
||||
import { issueTokens, createToken } from '../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../variables';
|
||||
} from '../../helpers/signup';
|
||||
import { issueTokens, createToken } from '../../helpers/auth';
|
||||
import { INVITED, ACCEPTED } from '../../variables';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||
@@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||
|
||||
token = tokens.token;
|
||||
refreshToken = tokens.refreshToken;
|
||||
|
||||
// sending a welcome email to new users
|
||||
if (process.env.LOOPS_API_KEY) {
|
||||
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||
"email": email,
|
||||
"eventName": "Sign Up",
|
||||
"firstName": firstName,
|
||||
"lastName": lastName
|
||||
}, {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { UserAction } from '../models';
|
||||
import { UserAction } from '../../models';
|
||||
|
||||
/**
|
||||
* Add user action [action]
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
IUser,
|
||||
ServiceToken
|
||||
} from '../models';
|
||||
ServiceToken,
|
||||
} from '../../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork
|
||||
} from '../helpers/workspace';
|
||||
import { addMemberships } from '../helpers/membership';
|
||||
import { ADMIN, COMPLETED, GRANTED } from '../variables';
|
||||
} from '../../helpers/workspace';
|
||||
import { addMemberships } from '../../helpers/membership';
|
||||
import { ADMIN, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
/**
|
||||
* Return public keys of members of workspace with id [workspaceId]
|
||||
5
backend/src/controllers/v2/index.ts
Normal file
5
backend/src/controllers/v2/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as workspaceController from './workspaceController';
|
||||
|
||||
export {
|
||||
workspaceController
|
||||
}
|
||||
0
backend/src/controllers/v2/secretController.ts
Normal file
0
backend/src/controllers/v2/secretController.ts
Normal file
565
backend/src/controllers/v2/workspaceController.ts
Normal file
565
backend/src/controllers/v2/workspaceController.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Workspace,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Integration,
|
||||
IntegrationAuth,
|
||||
Key,
|
||||
IUser,
|
||||
ServiceToken,
|
||||
} from '../../models';
|
||||
import {
|
||||
createWorkspace as create,
|
||||
deleteWorkspace as deleteWork
|
||||
} from '../../helpers/workspace';
|
||||
import {
|
||||
v2PushSecrets as push,
|
||||
pullSecrets as pull,
|
||||
reformatPullSecrets
|
||||
} from '../../helpers/secret';
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { addMemberships } from '../../helpers/membership';
|
||||
import { postHogClient, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables';
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return public keys of members of workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||
let publicKeys;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
publicKeys = (
|
||||
await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate<{ user: IUser }>('user', 'publicKey')
|
||||
)
|
||||
.filter((m) => m.status === COMPLETED || m.status === GRANTED)
|
||||
.map((member) => {
|
||||
return {
|
||||
publicKey: member.user.publicKey,
|
||||
userId: member.user._id
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace member public keys'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
publicKeys
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||
let users;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
users = await Membership.find({
|
||||
workspace: workspaceId
|
||||
}).populate('user', '+publicKey');
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace members'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
users
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces that user is part of
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||
let workspaces;
|
||||
try {
|
||||
workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate('workspace')
|
||||
).map((m) => m.workspace);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspaces'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspace = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
workspace = await Workspace.findOne({
|
||||
_id: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new workspace named [workspaceName] under organization with id
|
||||
* [organizationId] and add user as admin
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const createWorkspace = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceName, organizationId } = req.body;
|
||||
|
||||
// validate organization membership
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: req.user._id,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw new Error('Failed to validate organization membership');
|
||||
}
|
||||
|
||||
if (workspaceName.length < 1) {
|
||||
throw new Error('Workspace names must be at least 1-character long');
|
||||
}
|
||||
|
||||
// create workspace and add user as member
|
||||
workspace = await create({
|
||||
name: workspaceName,
|
||||
organizationId
|
||||
});
|
||||
|
||||
await addMemberships({
|
||||
userIds: [req.user._id],
|
||||
workspaceId: workspace._id.toString(),
|
||||
roles: [ADMIN],
|
||||
statuses: [GRANTED]
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to create workspace'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// delete workspace
|
||||
await deleteWork({
|
||||
id: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete workspace'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted workspace'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change name of workspace with id [workspaceId] to [name]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||
let workspace;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
workspace = await Workspace.findOneAndUpdate(
|
||||
{
|
||||
_id: workspaceId
|
||||
},
|
||||
{
|
||||
name
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to change workspace name'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully changed workspace name',
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return integrations for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||
let integrations;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
integrations = await Integration.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace integrations'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integrations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (integration) authorizations for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrationAuthorizations = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let authorizations;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
authorizations = await IntegrationAuth.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace integration authorizations'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
authorizations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceServiceTokens = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
let serviceTokens;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
serviceTokens = await ServiceToken.find({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get workspace service tokens'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokens
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
* for environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||
|
||||
try {
|
||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||
const { keys, environment, channel } = req.body;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
// sanitize secrets
|
||||
secrets = secrets.filter(
|
||||
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||
);
|
||||
|
||||
await push({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
});
|
||||
|
||||
await pushKeys({
|
||||
userId: req.user._id,
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
distinctId: req.user.email,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to upload workspace secrets'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully uploaded workspace secrets'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment] and (encrypted) workspace key
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecrets = async (req: Request, res: Response) => {
|
||||
// TODO: only return secrets, do not return workspace key
|
||||
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
key = await Key.findOne({
|
||||
workspace: workspaceId,
|
||||
receiver: req.user._id
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.populate('sender', '+publicKey');
|
||||
|
||||
if (channel !== 'cli') {
|
||||
secrets = reformatPullSecrets({ secrets });
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to pull workspace secrets'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets,
|
||||
key
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: modify based on upcoming serviceTokenData changes
|
||||
|
||||
/**
|
||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||
* for environment [environment] and (encrypted) workspace key
|
||||
* via service token
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
let secrets;
|
||||
let key;
|
||||
try {
|
||||
const environment: string = req.query.environment as string;
|
||||
const channel: string = req.query.channel as string;
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// validate environment
|
||||
if (!ENV_SET.has(environment)) {
|
||||
throw new Error('Failed to validate environment');
|
||||
}
|
||||
|
||||
secrets = await pull({
|
||||
userId: req.serviceToken.user._id.toString(),
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
key = {
|
||||
encryptedKey: req.serviceToken.encryptedKey,
|
||||
nonce: req.serviceToken.nonce,
|
||||
sender: {
|
||||
publicKey: req.serviceToken.publicKey
|
||||
},
|
||||
receiver: req.serviceToken.user,
|
||||
workspace: req.serviceToken.workspace
|
||||
};
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pulled event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.serviceToken.user.email,
|
||||
event: 'secrets pulled',
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel: channel ? channel : 'cli'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.serviceToken.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to pull workspace secrets'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: reformatPullSecrets({ secrets }),
|
||||
key
|
||||
});
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as stripeController from './stripeController';
|
||||
|
||||
export {
|
||||
stripeController
|
||||
}
|
||||
9
backend/src/ee/controllers/v1/index.ts
Normal file
9
backend/src/ee/controllers/v1/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as stripeController from './stripeController';
|
||||
import * as secretController from './secretController';
|
||||
import * as workspaceController from './workspaceController';
|
||||
|
||||
export {
|
||||
stripeController,
|
||||
secretController,
|
||||
workspaceController
|
||||
}
|
||||
35
backend/src/ee/controllers/v1/secretController.ts
Normal file
35
backend/src/ee/controllers/v1/secretController.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretVersion } from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret versions for secret with id [secretId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||
let secretVersions;
|
||||
try {
|
||||
const { secretId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
secretVersions = await SecretVersion.find({
|
||||
secret: secretId
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret versions'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config';
|
||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01'
|
||||
});
|
||||
35
backend/src/ee/controllers/v1/workspaceController.ts
Normal file
35
backend/src/ee/controllers/v1/workspaceController.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { SecretSnapshot } from '../../models';
|
||||
|
||||
/**
|
||||
* Return secret snapshots for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
||||
let secretSnapshots;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
const offset: number = parseInt(req.query.offset as string);
|
||||
const limit: number = parseInt(req.query.limit as string);
|
||||
|
||||
secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId
|
||||
})
|
||||
.skip(offset)
|
||||
.limit(limit);
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get secret snapshots'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
secretSnapshots
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.licenseKey - Infisical license key
|
||||
*/
|
||||
const checkLicenseKey = ({
|
||||
licenseKey
|
||||
}: {
|
||||
licenseKey: string
|
||||
}) => {
|
||||
try {
|
||||
// TODO
|
||||
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
checkLicenseKey
|
||||
}
|
||||
74
backend/src/ee/helpers/secret.ts
Normal file
74
backend/src/ee/helpers/secret.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret
|
||||
} from '../../models';
|
||||
import {
|
||||
SecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* secretsnapshots collection.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
*/
|
||||
const takeSecretSnapshotHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
try {
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId
|
||||
}).sort({ version: -1 });
|
||||
|
||||
if (!latestSecretSnapshot) {
|
||||
// case: no snapshots exist for workspace -> create first snapshot
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// case: snapshots exist for workspace
|
||||
await new SecretSnapshot({
|
||||
workspace: workspaceId,
|
||||
version: latestSecretSnapshot.version + 1,
|
||||
secrets
|
||||
}).save();
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to take a secret snapshot');
|
||||
}
|
||||
}
|
||||
|
||||
const addSecretVersionsHelper = async ({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[]
|
||||
}) => {
|
||||
try {
|
||||
await SecretVersion.insertMany(secretVersions);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to add secret versions');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
}
|
||||
9
backend/src/ee/models/index.ts
Normal file
9
backend/src/ee/models/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||
|
||||
export {
|
||||
SecretSnapshot,
|
||||
ISecretSnapshot,
|
||||
SecretVersion,
|
||||
ISecretVersion
|
||||
}
|
||||
109
backend/src/ee/models/secretSnapshot.ts
Normal file
109
backend/src/ee/models/secretSnapshot.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretSnapshot {
|
||||
workspace: Types.ObjectId;
|
||||
version: number;
|
||||
secrets: {
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
environment: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
secrets: [{
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
|
||||
|
||||
export default SecretSnapshot;
|
||||
75
backend/src/ee/models/secretVersion.ts
Normal file
75
backend/src/ee/models/secretVersion.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface ISecretVersion {
|
||||
_id?: Types.ObjectId;
|
||||
secret: Types.ObjectId;
|
||||
version: number;
|
||||
isDeleted: boolean;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
}
|
||||
|
||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
{
|
||||
secret: { // could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Secret',
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
||||
|
||||
export default SecretVersion;
|
||||
7
backend/src/ee/routes/v1/index.ts
Normal file
7
backend/src/ee/routes/v1/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import secret from './secret';
|
||||
import workspace from './workspace';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
}
|
||||
26
backend/src/ee/routes/v1/secret.ts
Normal file
26
backend/src/ee/routes/v1/secret.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables';
|
||||
|
||||
router.get(
|
||||
'/:secretId/secret-versions',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('secretId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
validateRequest,
|
||||
secretController.getSecretVersions
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { stripeController } from '../controllers';
|
||||
import { stripeController } from '../../controllers/v1';
|
||||
|
||||
router.post('/webhook', stripeController.handleWebhook);
|
||||
|
||||
27
backend/src/ee/routes/v1/workspace.ts
Normal file
27
backend/src/ee/routes/v1/workspace.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../../../middleware';
|
||||
import { param, query } from 'express-validator';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../../variables';
|
||||
import { workspaceController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secret-snapshots',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
query('offset').exists().isInt(),
|
||||
query('limit').exists().isInt(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceSecretSnapshots
|
||||
);
|
||||
|
||||
|
||||
export default router;
|
||||
19
backend/src/ee/services/EELicenseService.ts
Normal file
19
backend/src/ee/services/EELicenseService.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { LICENSE_KEY } from '../../config';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition license actions
|
||||
*/
|
||||
class EELicenseService {
|
||||
|
||||
private readonly _isLicenseValid: boolean;
|
||||
|
||||
constructor(licenseKey: string) {
|
||||
this._isLicenseValid = true;
|
||||
}
|
||||
|
||||
public get isLicenseValid(): boolean {
|
||||
return this._isLicenseValid;
|
||||
}
|
||||
}
|
||||
|
||||
export default new EELicenseService(LICENSE_KEY);
|
||||
47
backend/src/ee/services/EESecretService.ts
Normal file
47
backend/src/ee/services/EESecretService.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ISecretVersion } from '../models';
|
||||
import {
|
||||
takeSecretSnapshotHelper,
|
||||
addSecretVersionsHelper
|
||||
} from '../helpers/secret';
|
||||
import EELicenseService from './EELicenseService';
|
||||
|
||||
/**
|
||||
* Class to handle Enterprise Edition secret actions
|
||||
*/
|
||||
class EESecretService {
|
||||
|
||||
/**
|
||||
* Save a copy of the current state of secrets in workspace with id
|
||||
* [workspaceId] under a new snapshot with incremented version under the
|
||||
* SecretSnapshot collection.
|
||||
* Requires a valid license key [licenseKey]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId
|
||||
*/
|
||||
static async takeSecretSnapshot({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await takeSecretSnapshotHelper({ workspaceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds secret versions [secretVersions] to the SecretVersion collection.
|
||||
* @param {Object} obj
|
||||
* @param {SecretVersion} obj.secretVersions
|
||||
*/
|
||||
static async addSecretVersions({
|
||||
secretVersions
|
||||
}: {
|
||||
secretVersions: ISecretVersion[];
|
||||
}) {
|
||||
if (!EELicenseService.isLicenseValid) return;
|
||||
await addSecretVersionsHelper({
|
||||
secretVersions
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EESecretService;
|
||||
7
backend/src/ee/services/index.ts
Normal file
7
backend/src/ee/services/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import EELicenseService from "./EELicenseService";
|
||||
import EESecretService from "./EESecretService";
|
||||
|
||||
export {
|
||||
EELicenseService,
|
||||
EESecretService
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
decryptSymmetric,
|
||||
decryptAsymmetric
|
||||
} from '../utils/crypto';
|
||||
import { decryptSecrets } from '../helpers/secret';
|
||||
import { ENCRYPTION_KEY } from '../config';
|
||||
import { SECRET_SHARED } from '../variables';
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const validateMembership = async ({
|
||||
membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
}).populate("workspace");
|
||||
|
||||
if (!membership) throw new Error('Failed to find membership');
|
||||
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Secret,
|
||||
ISecret
|
||||
ISecret,
|
||||
} from '../models';
|
||||
import {
|
||||
EESecretService
|
||||
} from '../ee/services';
|
||||
import {
|
||||
SecretVersion
|
||||
} from '../ee/models';
|
||||
import {
|
||||
takeSecretSnapshotHelper
|
||||
} from '../ee/helpers/secret';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
||||
|
||||
interface PushSecret {
|
||||
interface V1PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
@@ -15,11 +24,31 @@ interface PushSecret {
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
ciphertextComment: string;
|
||||
ivComment: string;
|
||||
tagComment: string;
|
||||
hashComment: string;
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretKeyHash: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
interface Update {
|
||||
[index: string]: string;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
@@ -35,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @param {Object[]} obj.secrets - secrets to push
|
||||
*/
|
||||
const pushSecrets = async ({
|
||||
const v1PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
@@ -44,8 +73,9 @@ const pushSecrets = async ({
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secrets: PushSecret[];
|
||||
secrets: V1PushSecret[];
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
@@ -53,74 +83,133 @@ const pushSecrets = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => {
|
||||
return { ...accumulator, [s.secretKeyHash]: s };
|
||||
}, {});
|
||||
const newSecretsObj = secrets.reduce((accumulator, s) => {
|
||||
return { ...accumulator, [s.hashKey]: s };
|
||||
}, {});
|
||||
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||
({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
|
||||
, {});
|
||||
|
||||
// handle deleting secrets
|
||||
const toDelete = oldSecrets.filter(
|
||||
(s: ISecret) => !(s.secretKeyHash in newSecretsObj)
|
||||
);
|
||||
const toDelete = oldSecrets
|
||||
.filter(
|
||||
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||
)
|
||||
.map((s) => s._id);
|
||||
if (toDelete.length > 0) {
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete.map((s) => s._id) }
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
});
|
||||
}
|
||||
|
||||
// handle modifying secrets where type or value changed
|
||||
const operations = secrets
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
.filter((s) => {
|
||||
if (s.hashKey in oldSecretsObj) {
|
||||
if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) {
|
||||
// case: filter secrets where value changed
|
||||
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|
||||
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
|
||||
// case: filter secrets where value or comment changed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s.type !== oldSecretsObj[s.hashKey].type) {
|
||||
// case: filter secrets where type changed
|
||||
if (!s.version) {
|
||||
// case: filter (legacy) secrets that were not versioned
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
})
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue,
|
||||
ciphertextComment,
|
||||
ivComment,
|
||||
tagComment,
|
||||
hashComment
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
type: s.type,
|
||||
secretValueCiphertext: s.ciphertextValue,
|
||||
secretValueIV: s.ivValue,
|
||||
secretValueTag: s.tagValue,
|
||||
secretValueHash: s.hashValue
|
||||
};
|
||||
secretValueCiphertext: ciphertextValue,
|
||||
secretValueIV: ivValue,
|
||||
secretValueTag: tagValue,
|
||||
secretValueHash: hashValue,
|
||||
secretCommentCiphertext: ciphertextComment,
|
||||
secretCommentIV: ivComment,
|
||||
secretCommentTag: tagComment,
|
||||
secretCommentHash: hashComment,
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user assocaited with the personal secret
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: workspaceId,
|
||||
_id: oldSecretsObj[s.hashKey]._id
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
const a = await Secret.bulkWrite(operations as any);
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map(({
|
||||
_id,
|
||||
version,
|
||||
type,
|
||||
secretKeyHash,
|
||||
}) => {
|
||||
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||
return ({
|
||||
secret: _id,
|
||||
version: version ? version + 1 : 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext: newSecret.ciphertextKey,
|
||||
secretKeyIV: newSecret.ivKey,
|
||||
secretKeyTag: newSecret.tagKey,
|
||||
secretKeyHash: newSecret.hashKey,
|
||||
secretValueCiphertext: newSecret.ciphertextValue,
|
||||
secretValueIV: newSecret.ivValue,
|
||||
secretValueTag: newSecret.tagValue,
|
||||
secretValueHash: newSecret.hashValue
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj));
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
await Secret.insertMany(
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map((s, idx) => {
|
||||
let obj: any = {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
@@ -131,7 +220,11 @@ const pushSecrets = async ({
|
||||
secretValueCiphertext: s.ciphertextValue,
|
||||
secretValueIV: s.ivValue,
|
||||
secretValueTag: s.tagValue,
|
||||
secretValueHash: s.hashValue
|
||||
secretValueHash: s.hashValue,
|
||||
secretCommentCiphertext: s.ciphertextComment,
|
||||
secretCommentIV: s.ivComment,
|
||||
secretCommentTag: s.tagComment,
|
||||
secretCommentHash: s.hashComment
|
||||
};
|
||||
|
||||
if (toAdd[idx].type === 'personal') {
|
||||
@@ -141,7 +234,282 @@ const pushSecrets = async ({
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to push shared and personal secrets');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Push secrets for user with id [userId] to workspace
|
||||
* with id [workspaceId] with environment [environment]. Follow steps:
|
||||
* 1. Handle shared secrets (insert, delete)
|
||||
* 2. handle personal secrets (insert, delete)
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user to push secrets for
|
||||
* @param {String} obj.workspaceId - id of workspace to push to
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @param {Object[]} obj.secrets - secrets to push
|
||||
*/
|
||||
const v2PushSecrets = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secrets: V2PushSecret[];
|
||||
}): Promise<void> => {
|
||||
// TODO: clean up function and fix up types
|
||||
try {
|
||||
// construct useful data structures
|
||||
const oldSecrets = await pullSecrets({
|
||||
userId,
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||
, {});
|
||||
|
||||
// handle deleting secrets
|
||||
const toDelete = oldSecrets
|
||||
.filter(
|
||||
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||
)
|
||||
.map((s) => s._id);
|
||||
if (toDelete.length > 0) {
|
||||
await Secret.deleteMany({
|
||||
_id: { $in: toDelete }
|
||||
});
|
||||
|
||||
await SecretVersion.updateMany({
|
||||
secret: { $in: toDelete }
|
||||
}, {
|
||||
isDeleted: true
|
||||
});
|
||||
}
|
||||
|
||||
const toUpdate = oldSecrets
|
||||
.filter((s) => {
|
||||
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|
||||
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
|
||||
// case: filter secrets where value or comment changed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: filter (legacy) secrets that were not versioned
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const operations = toUpdate
|
||||
.map((s) => {
|
||||
const {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
const update: Update = {
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}
|
||||
|
||||
if (!s.version) {
|
||||
// case: (legacy) secret was not versioned
|
||||
update.version = 1;
|
||||
} else {
|
||||
update['$inc'] = {
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === SECRET_PERSONAL) {
|
||||
// attach user associated with the personal secret
|
||||
update['user'] = userId;
|
||||
}
|
||||
|
||||
return {
|
||||
updateOne: {
|
||||
filter: {
|
||||
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||
},
|
||||
update
|
||||
}
|
||||
};
|
||||
});
|
||||
await Secret.bulkWrite(operations as any);
|
||||
|
||||
// (EE) add secret versions for updated secrets
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: toUpdate.map((s) => {
|
||||
const {
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||
|
||||
return ({
|
||||
secret: s._id,
|
||||
version: s.version ? s.version + 1 : 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// handle adding new secrets
|
||||
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
// add secrets
|
||||
const newSecrets = await Secret.insertMany(
|
||||
toAdd.map(({
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash,
|
||||
}, idx) => {
|
||||
const obj: any = {
|
||||
version: 1,
|
||||
workspace: workspaceId,
|
||||
type: toAdd[idx].type,
|
||||
environment,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash
|
||||
};
|
||||
|
||||
if (toAdd[idx].type === 'personal') {
|
||||
obj['user' as keyof typeof obj] = userId;
|
||||
}
|
||||
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
|
||||
// (EE) add secret versions for new secrets
|
||||
EESecretService.addSecretVersions({
|
||||
secretVersions: newSecrets.map(({
|
||||
_id,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}) => ({
|
||||
secret: _id,
|
||||
version: 1,
|
||||
isDeleted: false,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId
|
||||
})
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -222,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
iv: s.secretValueIV,
|
||||
tag: s.secretValueTag,
|
||||
hash: s.secretValueHash
|
||||
},
|
||||
secretComment: {
|
||||
workspace: s.workspace,
|
||||
ciphertext: s.secretCommentCiphertext,
|
||||
iv: s.secretCommentIV,
|
||||
tag: s.secretCommentTag,
|
||||
hash: s.secretCommentHash
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
@@ -233,71 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
||||
return reformatedSecrets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return decrypted secrets in format [format]
|
||||
* @param {Object} obj
|
||||
* @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects
|
||||
* @param {String} obj.key - symmetric key to decrypt secret key-value pairs
|
||||
* @param {String} obj.format - desired return format that is either "text," "object," or "expanded"
|
||||
* @return {String|Object} (decrypted) secrets also called the content
|
||||
*/
|
||||
const decryptSecrets = ({
|
||||
secrets,
|
||||
key,
|
||||
format
|
||||
}: {
|
||||
secrets: PushSecret[];
|
||||
key: string;
|
||||
format: DecryptSecretType;
|
||||
}) => {
|
||||
// init content
|
||||
let content: any = format === 'text' ? '' : {};
|
||||
|
||||
// decrypt secrets
|
||||
secrets.forEach((s, idx) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: s.ciphertextKey,
|
||||
iv: s.ivKey,
|
||||
tag: s.tagKey,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: s.ciphertextValue,
|
||||
iv: s.ivValue,
|
||||
tag: s.tagValue,
|
||||
key
|
||||
});
|
||||
|
||||
switch (format) {
|
||||
case 'text':
|
||||
content += secretKey;
|
||||
content += '=';
|
||||
content += secretValue;
|
||||
|
||||
if (idx < secrets.length) {
|
||||
content += '\n';
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
content[secretKey] = secretValue;
|
||||
break;
|
||||
case 'expanded':
|
||||
content[secretKey] = {
|
||||
...s,
|
||||
plaintextKey: secretKey,
|
||||
plaintextValue: secretValue
|
||||
};
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export {
|
||||
pushSecrets,
|
||||
v1PushSecrets,
|
||||
v2PushSecrets,
|
||||
pullSecrets,
|
||||
reformatPullSecrets,
|
||||
decryptSecrets
|
||||
reformatPullSecrets
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SECRET_GITHUB
|
||||
} from '../config';
|
||||
import { user } from '../routes';
|
||||
|
||||
interface ExchangeCodeHerokuResponse {
|
||||
token_type: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
export interface ISecret {
|
||||
_id: Types.ObjectId;
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
@@ -22,10 +23,18 @@ export interface ISecret {
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretValueHash: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
}
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
{
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
@@ -77,6 +86,22 @@ const secretSchema = new Schema<ISecret>(
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretCommentCiphertext: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
secretCommentIV: {
|
||||
type: String, // symmetric
|
||||
required: false
|
||||
},
|
||||
secretCommentTag: {
|
||||
type: String, // symmetric
|
||||
required: false
|
||||
},
|
||||
secretCommentHash: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { authController } from '../controllers';
|
||||
import { loginLimiter } from '../helpers/rateLimiter';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { authController } from '../../controllers/v1';
|
||||
import { loginLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post('/token', validateRequest, authController.getNewToken);
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
requireBotAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { botController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
} from '../../middleware';
|
||||
import { botController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
requireAuth,
|
||||
requireIntegrationAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../controllers';
|
||||
import { integrationController } from '../../controllers/v1';
|
||||
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
requireIntegrationAuthorizationAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { integrationAuthController } from '../controllers';
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { integrationAuthController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/integration-options',
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { membershipOrgController } from '../controllers';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipOrgController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/signup',
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
} from '../../middleware';
|
||||
import { body, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { keyController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { keyController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { membershipController } from '../controllers';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipController } from '../../controllers/v1';
|
||||
|
||||
router.get( // used for CLI (deprecate)
|
||||
'/:workspaceId/connect',
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { param } from 'express-validator';
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { membershipOrgController } from '../controllers';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { membershipOrgController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
// TODO
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables';
|
||||
import { organizationController } from '../controllers';
|
||||
} from '../../middleware';
|
||||
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
|
||||
import { organizationController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
@@ -1,9 +1,9 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware';
|
||||
import { passwordController } from '../controllers';
|
||||
import { passwordLimiter } from '../helpers/rateLimiter';
|
||||
import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { passwordController } from '../../controllers/v1';
|
||||
import { passwordLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/srp1',
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
} from '../../middleware';
|
||||
import { body, query, param } from 'express-validator';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { secretController } from '../controllers';
|
||||
import { secretController } from '../../controllers/v1';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
|
||||
router.post(
|
||||
'/:workspaceId',
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
} from '../../middleware';
|
||||
import { body } from 'express-validator';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { serviceTokenController } from '../controllers';
|
||||
import { ADMIN, MEMBER, GRANTED } from '../../variables';
|
||||
import { serviceTokenController } from '../../controllers/v1';
|
||||
|
||||
// TODO: revoke service token
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body } from 'express-validator';
|
||||
import { requireSignupAuth, validateRequest } from '../middleware';
|
||||
import { signupController } from '../controllers';
|
||||
import { signupLimiter } from '../helpers/rateLimiter';
|
||||
import { requireSignupAuth, validateRequest } from '../../middleware';
|
||||
import { signupController } from '../../controllers/v1';
|
||||
import { signupLimiter } from '../../helpers/rateLimiter';
|
||||
|
||||
router.post(
|
||||
'/email/signup',
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { stripeController } from '../controllers';
|
||||
import { stripeController } from '../../controllers/v1';
|
||||
|
||||
router.post('/webhook', stripeController.handleWebhook);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { requireAuth } from '../middleware';
|
||||
import { userController } from '../controllers';
|
||||
import { requireAuth } from '../../middleware';
|
||||
import { userController } from '../../controllers/v1';
|
||||
|
||||
router.get('/', requireAuth, userController.getUser);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { requireAuth, validateRequest } from '../middleware';
|
||||
import { requireAuth, validateRequest } from '../../middleware';
|
||||
import { body, query } from 'express-validator';
|
||||
import { userActionController } from '../controllers';
|
||||
import { userActionController } from '../../controllers/v1';
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
@@ -1,13 +1,13 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
import { workspaceController, membershipController } from '../controllers';
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { workspaceController, membershipController } from '../../controllers/v1';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/keys',
|
||||
7
backend/src/routes/v2/index.ts
Normal file
7
backend/src/routes/v2/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import secret from './secret';
|
||||
import workspace from './workspace';
|
||||
|
||||
export {
|
||||
secret,
|
||||
workspace
|
||||
}
|
||||
4
backend/src/routes/v2/secret.ts
Normal file
4
backend/src/routes/v2/secret.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
|
||||
export default router;
|
||||
176
backend/src/routes/v2/workspace.ts
Normal file
176
backend/src/routes/v2/workspace.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param, query } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireServiceTokenAuth,
|
||||
validateRequest
|
||||
} from '../../middleware';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
||||
import { membershipController } from '../../controllers/v1';
|
||||
import { workspaceController } from '../../controllers/v2';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/keys',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspacePublicKeys
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/users',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceMemberships
|
||||
);
|
||||
|
||||
router.get('/', requireAuth, workspaceController.getWorkspaces);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspace
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireAuth,
|
||||
body('workspaceName').exists().trim().notEmpty(),
|
||||
body('organizationId').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
workspaceController.createWorkspace
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.deleteWorkspace
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/name',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('name').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
workspaceController.changeWorkspaceName
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/invite-signup',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
body('email').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
membershipController.inviteUserToWorkspace
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/integrations',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceIntegrations
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/authorizations',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceIntegrationAuthorizations
|
||||
);
|
||||
|
||||
router.get( // TODO: modify
|
||||
'/:workspaceId/service-tokens',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.getWorkspaceServiceTokens
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:workspaceId/secrets',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
body('secrets').exists(),
|
||||
body('keys').exists(),
|
||||
body('environment').exists().trim().notEmpty(),
|
||||
body('channel'),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.pushWorkspaceSecrets
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:workspaceId/secrets',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
query('environment').exists().trim(),
|
||||
query('channel'),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.pullSecrets
|
||||
);
|
||||
|
||||
router.get( // TODO: modify based on upcoming serviceTokenData changes
|
||||
'/:workspaceId/secrets-service-token',
|
||||
requireServiceTokenAuth,
|
||||
query('environment').exists().trim(),
|
||||
query('channel'),
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
workspaceController.pullSecretsServiceToken
|
||||
);
|
||||
|
||||
|
||||
export default router;
|
||||
@@ -45,7 +45,7 @@ function wrap(fn) {
|
||||
return copyFnProps(fn, newFn);
|
||||
}
|
||||
|
||||
export function patchRouterParam() {
|
||||
function patchRouterParam() {
|
||||
const originalParam = Router.prototype.constructor.param;
|
||||
Router.prototype.constructor.param = function param(name, fn) {
|
||||
fn = wrap(fn);
|
||||
@@ -62,4 +62,8 @@ Object.defineProperty(Layer.prototype, 'handle', {
|
||||
fn = wrap(fn);
|
||||
this.__handle = fn;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
patchRouterParam
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
|
||||
Short: "Infisical CLI is used to inject environment variables into any process",
|
||||
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
|
||||
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
|
||||
Version: "0.1.14",
|
||||
Version: "0.1.16",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
||||
@@ -60,6 +60,13 @@ var runCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the secret-overriding flag")
|
||||
log.Debugln(err)
|
||||
return
|
||||
}
|
||||
|
||||
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
|
||||
if err != nil {
|
||||
log.Errorln("Unable to parse the substitute flag")
|
||||
@@ -84,6 +91,10 @@ var runCmd = &cobra.Command{
|
||||
secrets = util.SubstituteSecrets(secrets)
|
||||
}
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideWithPersonalSecrets(secrets)
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("command") {
|
||||
command := cmd.Flag("command").Value.String()
|
||||
err = executeMultipleCommandWithEnvs(command, secrets)
|
||||
@@ -108,6 +119,7 @@ func init() {
|
||||
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
|
||||
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
|
||||
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
|
||||
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets with the same name over shared secrets")
|
||||
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ type ConfigFile struct {
|
||||
type SingleEnvironmentVariable struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type WorkspaceConfigFile struct {
|
||||
|
||||
@@ -14,6 +14,9 @@ import (
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
const PERSONAL_SECRET_TYPE_NAME = "personal"
|
||||
const SHARED_SECRET_TYPE_NAME = "shared"
|
||||
|
||||
func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) {
|
||||
var pullSecretsRequestResponse models.PullSecretsResponse
|
||||
response, err := httpClient.
|
||||
@@ -78,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string,
|
||||
env := models.SingleEnvironmentVariable{
|
||||
Key: string(plainTextKey),
|
||||
Value: string(plainTextValue),
|
||||
Type: string(secret.Type),
|
||||
}
|
||||
|
||||
listOfEnv = append(listOfEnv, env)
|
||||
@@ -187,6 +191,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
|
||||
env := models.SingleEnvironmentVariable{
|
||||
Key: string(plainTextKey),
|
||||
Value: string(plainTextValue),
|
||||
Type: string(secret.Type),
|
||||
}
|
||||
|
||||
listOfEnv = append(listOfEnv, env)
|
||||
@@ -335,9 +340,48 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing
|
||||
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: expandedVariable,
|
||||
Type: secret.Type,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return expandedSecrets
|
||||
}
|
||||
|
||||
// if two secrets with the same name are found, the one that has type `personal` will be in the returned list
|
||||
func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
|
||||
personalSecret := make(map[string]models.SingleEnvironmentVariable)
|
||||
sharedSecret := make(map[string]models.SingleEnvironmentVariable)
|
||||
secretsToReturn := []models.SingleEnvironmentVariable{}
|
||||
|
||||
for _, secret := range secrets {
|
||||
if secret.Type == PERSONAL_SECRET_TYPE_NAME {
|
||||
personalSecret[secret.Key] = models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: secret.Value,
|
||||
Type: secret.Type,
|
||||
}
|
||||
}
|
||||
|
||||
if secret.Type == SHARED_SECRET_TYPE_NAME {
|
||||
sharedSecret[secret.Key] = models.SingleEnvironmentVariable{
|
||||
Key: secret.Key,
|
||||
Value: secret.Value,
|
||||
Type: secret.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
personalValue, personalExists := personalSecret[secret.Key]
|
||||
sharedValue, sharedExists := sharedSecret[secret.Key]
|
||||
|
||||
if personalExists && sharedExists || personalExists && !sharedExists {
|
||||
secretsToReturn = append(secretsToReturn, personalValue)
|
||||
} else {
|
||||
secretsToReturn = append(secretsToReturn, sharedValue)
|
||||
}
|
||||
}
|
||||
|
||||
return secretsToReturn
|
||||
}
|
||||
|
||||
@@ -34,3 +34,4 @@ Inject environment variables from the platform into an application process.
|
||||
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None |
|
||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||
| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |
|
||||
| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` |
|
||||
|
||||
@@ -88,6 +88,14 @@ The Infisical CLI provides a way to inject environment variables from the platfo
|
||||
sudo apt-get update && sudo apt-get install -y infisical
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Arch Linux">
|
||||
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin)
|
||||
|
||||
```bash
|
||||
yay -S infisical-bin
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -38,13 +38,7 @@ infisical init
|
||||
infisical run -- [your application start command]
|
||||
```
|
||||
|
||||
Options you can specify:
|
||||
|
||||
| Option | Description | Default value |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
|
||||
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
|
||||
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
|
||||
View all available options for `run` command [here](./commands/run)
|
||||
|
||||
## Examples:
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Configuring Infisical requires setting some environment variables. There is a fi
|
||||
| `SMTP_FROM_ADDRESS` | ❗️ Email address to be used for sending emails (e.g. `team@infisical.com`) | `None` |
|
||||
| `SMTP_FROM_NAME` | Name label to be used in From field (e.g. `Team`) | `Infisical` |
|
||||
| `TELEMETRY_ENABLED` | `true` or `false`. [More](../overview). | `true` |
|
||||
| `LICENSE_KEY` | License key if using Infisical Enterprise Edition | `true` |
|
||||
| `CLIENT_ID_HEROKU` | OAuth2 client ID for Heroku integration | `None` |
|
||||
| `CLIENT_ID_VERCEL` | OAuth2 client ID for Vercel integration | `None` |
|
||||
| `CLIENT_ID_NETLIFY` | OAuth2 client ID for Netlify integration | `None` |
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function RouteGuard({ children }) {
|
||||
// Check if the user is authenticated
|
||||
const response = await checkAuth();
|
||||
// #TODO: figure our why sometimes it doesn't output a response
|
||||
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
||||
if (!publicPaths.includes(path)) {
|
||||
try {
|
||||
if (response.status !== 200) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { memo,useState } from 'react';
|
||||
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -140,4 +140,4 @@ const InputField = (
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(InputField);
|
||||
export default memo(InputField);
|
||||
|
||||
83
frontend/components/basic/Toggle.tsx
Normal file
83
frontend/components/basic/Toggle.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
id: string;
|
||||
comment: string;
|
||||
deleteOverride: (id: string) => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
|
||||
* @param obj
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {string} obj.keyName - key of a certain secret
|
||||
* @param {string} obj.value - value of a certain secret
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
#TODO: make the secret id persistent?
|
||||
* @param {string} obj.id - id of a certain secret
|
||||
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns
|
||||
*/
|
||||
export default function Toggle ({
|
||||
enabled,
|
||||
setEnabled,
|
||||
addOverride,
|
||||
keyName,
|
||||
value,
|
||||
pos,
|
||||
id,
|
||||
comment,
|
||||
deleteOverride,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
}: ToggleProps): JSX.Element {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
addOverride({ id, keyName, value, pos, comment });
|
||||
setSharedToHide([
|
||||
...sharedToHide!,
|
||||
id
|
||||
])
|
||||
} else {
|
||||
deleteOverride(id);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
className={`${
|
||||
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||
>
|
||||
<span className="sr-only">Enable notifications</span>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
|
||||
/>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type ButtonProps = {
|
||||
size: string;
|
||||
icon?: IconProp;
|
||||
active?: boolean;
|
||||
iconDisabled?: string;
|
||||
iconDisabled?: IconProp;
|
||||
textDisabled?: string;
|
||||
type?: ButtonHTMLAttributes<any>['type'];
|
||||
};
|
||||
@@ -73,15 +73,16 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
|
||||
// Setting the text color for the text and icon
|
||||
props.color == "mineshaft" && "text-gray-400",
|
||||
props.color != "mineshaft" && props.color != "red" && "text-black",
|
||||
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
|
||||
props.color == "red" && "text-gray-200",
|
||||
activityStatus && props.color != "red" ? "group-hover:text-black" : "",
|
||||
props.color == "none" && "text-gray-200 text-xl",
|
||||
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
|
||||
|
||||
props.size == "icon" && "flex items-center justify-center"
|
||||
);
|
||||
|
||||
const textStyle = classNames(
|
||||
"relative duration-200",
|
||||
"relative duration-200 text-center w-full",
|
||||
|
||||
// Show the loading sign if the loading indicator is on
|
||||
props.loading ? "opacity-0" : "opacity-100",
|
||||
|
||||
@@ -3,6 +3,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
|
||||
interface PopupProps {
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
titleText: string;
|
||||
emoji: string;
|
||||
textLine1: string;
|
||||
textLine2: string;
|
||||
setCheckDocsPopUpVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the notification that pops up at the bottom right when a user performs a certain action
|
||||
* @param {object} org
|
||||
@@ -23,16 +33,16 @@ export default function BottonRightPopup({
|
||||
textLine1,
|
||||
textLine2,
|
||||
setCheckDocsPopUpVisible,
|
||||
}) {
|
||||
}: PopupProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
||||
<div>{titleText}</div>
|
||||
<div class="ml-2.5">{emoji}</div>
|
||||
<div className="ml-2.5">{emoji}</div>
|
||||
</div>
|
||||
<button
|
||||
className="mt-1"
|
||||
@@ -44,14 +54,14 @@ export default function BottonRightPopup({
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
|
||||
{textLine1}
|
||||
</div>
|
||||
<div class="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="flex flex-row px-6 w-full">
|
||||
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
href={buttonLink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
16
frontend/components/dashboard/CommentField.tsx
Normal file
16
frontend/components/dashboard/CommentField.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* This is the text field where people can add comments to particular secrets.
|
||||
*/
|
||||
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
||||
return <div className={`relative mt-4 px-4 pt-4`}>
|
||||
<p className='text-sm text-bunker-300'>Comments & notes</p>
|
||||
<textarea
|
||||
className="bg-bunker-800 h-32 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
placeholder="Leave any comments here..."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default CommentField;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { SyntheticEvent, useRef } from 'react';
|
||||
import { memo, SyntheticEvent, useRef } from 'react';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -11,19 +11,21 @@ interface DashboardInputFieldProps {
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
value: string;
|
||||
type: 'varName' | 'value';
|
||||
blurred: boolean;
|
||||
duplicates: string[];
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
override?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the input fields on the dashboard
|
||||
* @param {object} obj - the order number of a keyPair
|
||||
* @param {number} obj.pos - the order number of a keyPair
|
||||
* @param {number} obj.position - the order number of a keyPair
|
||||
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
||||
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
||||
* @param {string} obj.value - value of the InputField
|
||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||
* @param {string[]} obj.duplicates - list of all the duplicated key names on the dashboard
|
||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@@ -33,7 +35,8 @@ const DashboardInputField = ({
|
||||
type,
|
||||
value,
|
||||
blurred,
|
||||
duplicates
|
||||
isDuplicate,
|
||||
override
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
@@ -45,8 +48,7 @@ const DashboardInputField = ({
|
||||
|
||||
if (type === 'varName') {
|
||||
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
|
||||
const hasDuplicates = duplicates?.includes(value);
|
||||
const error = startsWithNumber || hasDuplicates;
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
@@ -72,7 +74,7 @@ const DashboardInputField = ({
|
||||
Should not start with a number
|
||||
</p>
|
||||
)}
|
||||
{hasDuplicates && !startsWithNumber && (
|
||||
{isDuplicate && !startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Secret names should be unique
|
||||
</p>
|
||||
@@ -85,6 +87,7 @@ const DashboardInputField = ({
|
||||
<div
|
||||
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
|
||||
>
|
||||
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
@@ -99,10 +102,13 @@ const DashboardInputField = ({
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
blurred
|
||||
blurred && !override
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||
: ''
|
||||
} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
} ${
|
||||
override ? 'text-primary-300' : 'text-gray-400'
|
||||
}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value.split(REGEX).map((word, id) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
@@ -153,4 +159,8 @@ const DashboardInputField = ({
|
||||
return <>Something Wrong</>;
|
||||
};
|
||||
|
||||
export default React.memo(DashboardInputField);
|
||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
||||
}
|
||||
|
||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||
|
||||
@@ -152,7 +152,13 @@ const DropZone = ({
|
||||
icon={faUpload}
|
||||
className="text-bunker-300 text-3xl mr-6"
|
||||
/>
|
||||
<<<<<<< HEAD
|
||||
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
|
||||
=======
|
||||
<p className="text-bunker-300 mt-1">
|
||||
Drag and drop your .env file here to add more keys.
|
||||
</p>
|
||||
>>>>>>> 876c5f51c2caabf92b47d5a18a9b98e601806bf4
|
||||
</div>
|
||||
{errorDragAndDrop ? (
|
||||
<div className="mt-8 max-w-xl opacity-80">
|
||||
|
||||
93
frontend/components/dashboard/GenerateSecretMenu.tsx
Normal file
93
frontend/components/dashboard/GenerateSecretMenu.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Fragment,useState } from 'react';
|
||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
|
||||
/**
|
||||
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||
* @returns the popup-menu for randomly generating secrets
|
||||
*/
|
||||
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
|
||||
return <Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
|
||||
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
position
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>Generate Random Hex</p>
|
||||
<p>digits</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
|
||||
export default GenerateSecretMenu;
|
||||
102
frontend/components/dashboard/KeyPair.tsx
Normal file
102
frontend/components/dashboard/KeyPair.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
|
||||
interface SecretDataProps {
|
||||
type: 'personal' | 'shared';
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
deleteRow: (id: string) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
deleteRow,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
toggleSidebar,
|
||||
sidebarSecretId
|
||||
}: KeyPairProps) => {
|
||||
return (
|
||||
<div className={`mx-1 flex flex-col items-center ml-1 ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||
This secret is overriden
|
||||
</span>
|
||||
</div>}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-5xl">
|
||||
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.value}
|
||||
blurred={isBlurred}
|
||||
override={keyPair.type == "personal"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2"></div>
|
||||
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteRow(keyPair.id)}
|
||||
color="none"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(KeyPair);
|
||||
168
frontend/components/dashboard/SideBar.tsx
Normal file
168
frontend/components/dashboard/SideBar.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useState } from 'react';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import SecretVersionList from 'ee/components/SecretVersionList';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import CommentField from './CommentField';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
type: string;
|
||||
id: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
keyName: string;
|
||||
value: string;
|
||||
pos: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
data: SecretProps[];
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
addOverride: (value: OverrideProps) => void;
|
||||
deleteOverride: (id: string) => void;
|
||||
buttonReady: boolean;
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
|
||||
* @param {function} obj.modifyKey - function that modifies the secret key
|
||||
* @param {function} obj.modifyValue - function that modifies the secret value
|
||||
* @param {function} obj.addOverride - override a certain secret
|
||||
* @param {function} obj.deleteOverride - delete the personal override for a certain secret
|
||||
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
|
||||
* @param {function} obj.savePush - save changes andp ush secrets
|
||||
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
|
||||
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
|
||||
* @returns the sidebar with 'secret's settings'
|
||||
*/
|
||||
const SideBar = ({
|
||||
toggleSidebar,
|
||||
data,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyComment,
|
||||
addOverride,
|
||||
deleteOverride,
|
||||
buttonReady,
|
||||
savePush,
|
||||
sharedToHide,
|
||||
setSharedToHide
|
||||
}: SideBarProps) => {
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
|
||||
|
||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">Secret</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>Key</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>Value</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data.filter(secret => secret.type == "shared")[0]?.pos}
|
||||
value={data.filter(secret => secret.type == "shared")[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>Note:</span>
|
||||
This secret is personal. It is not shared with any of your teammates.
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data.filter(secret => secret.type == "shared")[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>Override value with a personal value</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={addOverride}
|
||||
keyName={data[0]?.key}
|
||||
value={data[0]?.value}
|
||||
pos={data[0]?.pos}
|
||||
id={data[0]?.id}
|
||||
comment={data[0]?.comment}
|
||||
deleteOverride={deleteOverride}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
|
||||
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}>
|
||||
<p className='text-sm text-bunker-200'>Group</p>
|
||||
<ListBox
|
||||
selected={"Database Secrets"}
|
||||
onChange={() => {}}
|
||||
data={["Group1"]}
|
||||
isFull={true}
|
||||
/>
|
||||
</div> */}
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
</div>
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
<Button
|
||||
text="Save Changes"
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
/* eslint-disable react/jsx-key */
|
||||
<<<<<<< HEAD
|
||||
import React, { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { TFunction, useTranslation } from "next-i18next";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
=======
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
|
||||
>>>>>>> 876c5f51c2caabf92b47d5a18a9b98e601806bf4
|
||||
import {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faAngleRight,
|
||||
|
||||
@@ -16,12 +16,19 @@ export default class SecurityClient {
|
||||
const req = new Request(resource, options);
|
||||
|
||||
if (this.#token == '') {
|
||||
this.setToken(await token());
|
||||
try {
|
||||
// TODO: This should be moved to a context to do it only once when app loads
|
||||
// this try catch saves route guard from a stuck state
|
||||
this.setToken(await token());
|
||||
} catch (error) {
|
||||
console.error("Unauthorized access");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#token) {
|
||||
req.headers.set('Authorization', 'Bearer ' + this.#token);
|
||||
return fetch(req);
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@ const {
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
type: 'personal' | 'shared';
|
||||
comment: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
env: keyof typeof envMapping;
|
||||
setFileState: any;
|
||||
@@ -34,12 +42,12 @@ const getSecretsForProject = async ({
|
||||
} catch (error) {
|
||||
console.log('ERROR: Not able to access the latest file');
|
||||
}
|
||||
// This is called isKeyAvilable but what it really means is if a person is able to create new key pairs
|
||||
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
||||
setIsKeyAvailable(!file.key ? file.secrets.length == 0 : true);
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const tempFileState: { key: string; value: string; type: string }[] = [];
|
||||
const tempFileState: SecretProps[] = [];
|
||||
if (file.key) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
@@ -64,10 +72,25 @@ const getSecretsForProject = async ({
|
||||
tag: secretPair.secretValue.tag,
|
||||
key
|
||||
});
|
||||
|
||||
let plainTextComment;
|
||||
if (secretPair.secretComment.ciphertext) {
|
||||
plainTextComment = decryptSymmetric({
|
||||
ciphertext: secretPair.secretComment.ciphertext,
|
||||
iv: secretPair.secretComment.iv,
|
||||
tag: secretPair.secretComment.tag,
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextComment = "";
|
||||
}
|
||||
|
||||
tempFileState.push({
|
||||
id: secretPair._id,
|
||||
key: plainTextKey,
|
||||
value: plainTextValue,
|
||||
type: secretPair.type
|
||||
type: secretPair.type,
|
||||
comment: plainTextComment
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -76,28 +99,30 @@ const getSecretsForProject = async ({
|
||||
setData(
|
||||
tempFileState.map((line, index) => {
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
id: line['id'],
|
||||
pos: index,
|
||||
key: line['key'],
|
||||
value: line['value'],
|
||||
type: line['type']
|
||||
type: line['type'],
|
||||
comment: line['comment']
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return tempFileState.map((line, index) => {
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
id: line['id'],
|
||||
pos: index,
|
||||
key: line['key'],
|
||||
value: line['value'],
|
||||
type: line['type']
|
||||
type: line['type'],
|
||||
comment: line['comment']
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during accessing or decripting secrets.');
|
||||
}
|
||||
return true;
|
||||
return [];
|
||||
};
|
||||
|
||||
export default getSecretsForProject;
|
||||
|
||||
@@ -47,35 +47,49 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
|
||||
const secrets = Object.keys(obj).map((key) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: ciphertextKey,
|
||||
iv: ivKey,
|
||||
tag: tagKey,
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: key,
|
||||
plaintext: key.slice(1),
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: ciphertextValue,
|
||||
iv: ivValue,
|
||||
tag: tagValue,
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key as keyof typeof obj][0],
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const visibility = obj[key as keyof typeof obj][1] != null ? obj[key as keyof typeof obj][1] : "personal";
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: obj[key as keyof typeof obj][1],
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const visibility = key.charAt(0) == "p" ? "personal" : "shared";
|
||||
|
||||
return {
|
||||
ciphertextKey,
|
||||
ivKey,
|
||||
tagKey,
|
||||
hashKey: crypto.createHash("sha256").update(key).digest("hex"),
|
||||
ciphertextValue,
|
||||
ivValue,
|
||||
tagValue,
|
||||
hashValue: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyHash: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][0]).digest("hex"),
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentHash: crypto.createHash("sha256").update(obj[key as keyof typeof obj][1]).digest("hex"),
|
||||
type: visibility,
|
||||
};
|
||||
});
|
||||
|
||||
40
frontend/ee/api/secrets/GetSecretVersions.ts
Normal file
40
frontend/ee/api/secrets/GetSecretVersions.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface secretVersionProps {
|
||||
secretId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the versions of a specific secret
|
||||
* @param {object} obj
|
||||
* @param {string} obj.secretId - the id of a secret for which we are fetching the version history
|
||||
* @param {number} obj.offset - the start of our query
|
||||
* @param {number} obj.limit - how far our query goes
|
||||
* @returns
|
||||
*/
|
||||
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?'+
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to get project secrets');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretVersions;
|
||||
101
frontend/ee/components/SecretVersionList.tsx
Normal file
101
frontend/ee/components/SecretVersionList.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import getSecretVersions from 'ee/api/secrets/GetSecretVersions';
|
||||
|
||||
import { decryptAssymmetric, decryptSymmetric } from '~/components/utilities/cryptography/crypto';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
interface DecryptedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns a list of versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
const router = useRouter();
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([{createdAt: "123", value: "124"}]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSecretVersionHistory = async () => {
|
||||
try {
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = encryptedSecretVersions.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
|
||||
return {
|
||||
createdAt: encryptedSecretVersion.createdAt,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
};
|
||||
getSecretVersionHistory();
|
||||
}, []);
|
||||
|
||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||
<p className=''>Version History</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>
|
||||
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
{/* <div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div> */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
@@ -10,13 +10,7 @@ const checkAuth = async () => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => {
|
||||
if (res && res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Not authorized');
|
||||
}
|
||||
});
|
||||
}).then((res) => res);
|
||||
};
|
||||
|
||||
export default checkAuth;
|
||||
|
||||
@@ -22,7 +22,7 @@ const uploadSecrets = async ({
|
||||
keys,
|
||||
environment
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v1/secret/' + workspaceId, {
|
||||
return SecurityClient.fetchCall('/api/v2/workspace/' + workspaceId + '/secrets', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user