Fixing merge conflicts for pages/users

This commit is contained in:
Vladyslav Matsiiako
2022-12-27 18:19:56 -05:00
120 changed files with 3842 additions and 399 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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
};

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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]

View File

@@ -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({

View File

@@ -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,

View File

@@ -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 = {};

View File

@@ -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
});
};
};

View File

@@ -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

View File

@@ -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);

View File

@@ -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]

View File

@@ -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]

View File

@@ -0,0 +1,5 @@
import * as workspaceController from './workspaceController';
export {
workspaceController
}

View 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
});
};

View File

@@ -1,5 +0,0 @@
import * as stripeController from './stripeController';
export {
stripeController
}

View File

@@ -0,0 +1,9 @@
import * as stripeController from './stripeController';
import * as secretController from './secretController';
import * as workspaceController from './workspaceController';
export {
stripeController,
secretController,
workspaceController
}

View 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
});
}

View File

@@ -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'
});

View 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
});
}

View File

@@ -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
}

View 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
}

View File

@@ -0,0 +1,9 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion";
export {
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion
}

View 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;

View 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;

View File

@@ -0,0 +1,7 @@
import secret from './secret';
import workspace from './workspace';
export {
secret,
workspace
}

View 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;

View File

@@ -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);

View 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;

View 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);

View 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;

View File

@@ -0,0 +1,7 @@
import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService";
export {
EELicenseService,
EESecretService
}

View File

@@ -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';

View File

@@ -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');

View File

@@ -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
};

View File

@@ -21,7 +21,6 @@ import {
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config';
import { user } from '../routes';
interface ExchangeCodeHerokuResponse {
token_type: string;

View File

@@ -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
}
},
{

View File

@@ -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);

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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

View File

@@ -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(
'/',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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

View File

@@ -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',

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(
'/',

View File

@@ -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',

View File

@@ -0,0 +1,7 @@
import secret from './secret';
import workspace from './workspace';
export {
secret,
workspace
}

View File

@@ -0,0 +1,4 @@
import express from 'express';
const router = express.Router();
export default router;

View 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;

View File

@@ -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
};

View File

@@ -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.

View File

@@ -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 ...\")")
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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` |

View File

@@ -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>

View File

@@ -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:

View File

@@ -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` |

View File

@@ -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) {

View File

@@ -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);

View 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>
)
}

View File

@@ -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",

View File

@@ -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"

View File

@@ -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';

View File

@@ -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';

View 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;

View File

@@ -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);

View File

@@ -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">

View 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;

View 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);

View 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;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faArrowRight,

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faAngleRight,

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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,
};
});

View 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;

View 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;

View File

@@ -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;

View File

@@ -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