diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index c5fd9034f..da2a29435 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -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 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5e94e0de6..0b348b71a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -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 + 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 diff --git a/README.md b/README.md index 3851b3c53..457cf27af 100644 --- a/README.md +++ b/README.md @@ -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 - + diff --git a/backend/src/app.ts b/backend/src/app.ts index e49551a91..8320a7d76 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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 diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index dfbc2111c..3fb475099 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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 }; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/v1/authController.ts similarity index 97% rename from backend/src/controllers/authController.ts rename to backend/src/controllers/v1/authController.ts index 20ac813d4..defd03d8a 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -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 { diff --git a/backend/src/controllers/botController.ts b/backend/src/controllers/v1/botController.ts similarity index 96% rename from backend/src/controllers/botController.ts rename to backend/src/controllers/v1/botController.ts index 7819e32df..ab86897cb 100644 --- a/backend/src/controllers/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -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; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/v1/index.ts similarity index 100% rename from backend/src/controllers/index.ts rename to backend/src/controllers/v1/index.ts diff --git a/backend/src/controllers/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts similarity index 92% rename from backend/src/controllers/integrationAuthController.ts rename to backend/src/controllers/v1/integrationAuthController.ts index c242c239a..95d0066ae 100644 --- a/backend/src/controllers/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -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, diff --git a/backend/src/controllers/integrationController.ts b/backend/src/controllers/v1/integrationController.ts similarity index 94% rename from backend/src/controllers/integrationController.ts rename to backend/src/controllers/v1/integrationController.ts index 910c7e825..c05794959 100644 --- a/backend/src/controllers/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -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; diff --git a/backend/src/controllers/keyController.ts b/backend/src/controllers/v1/keyController.ts similarity index 93% rename from backend/src/controllers/keyController.ts rename to backend/src/controllers/v1/keyController.ts index 70446a76c..332215894 100644 --- a/backend/src/controllers/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -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 diff --git a/backend/src/controllers/membershipController.ts b/backend/src/controllers/v1/membershipController.ts similarity index 95% rename from backend/src/controllers/membershipController.ts rename to backend/src/controllers/v1/membershipController.ts index f2ed3db6e..187e8127c 100644 --- a/backend/src/controllers/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -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] diff --git a/backend/src/controllers/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts similarity index 86% rename from backend/src/controllers/membershipOrgController.ts rename to backend/src/controllers/v1/membershipOrgController.ts index a2159bcd0..5628cda1a 100644 --- a/backend/src/controllers/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -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({ diff --git a/backend/src/controllers/organizationController.ts b/backend/src/controllers/v1/organizationController.ts similarity index 97% rename from backend/src/controllers/organizationController.ts rename to backend/src/controllers/v1/organizationController.ts index 056990acc..44e140b4e 100644 --- a/backend/src/controllers/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -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, diff --git a/backend/src/controllers/passwordController.ts b/backend/src/controllers/v1/passwordController.ts similarity index 96% rename from backend/src/controllers/passwordController.ts rename to backend/src/controllers/v1/passwordController.ts index b029bc0be..27d712a6b 100644 --- a/backend/src/controllers/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -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 = {}; diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/v1/secretController.ts similarity index 91% rename from backend/src/controllers/secretController.ts rename to backend/src/controllers/v1/secretController.ts index bfd9aee1f..238b38ced 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -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 }); -}; +}; \ No newline at end of file diff --git a/backend/src/controllers/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts similarity index 87% rename from backend/src/controllers/serviceTokenController.ts rename to backend/src/controllers/v1/serviceTokenController.ts index 4cc53c4f9..e21cd7695 100644 --- a/backend/src/controllers/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -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 diff --git a/backend/src/controllers/signupController.ts b/backend/src/controllers/v1/signupController.ts similarity index 89% rename from backend/src/controllers/signupController.ts rename to backend/src/controllers/v1/signupController.ts index 90fad4744..62e5a62a3 100644 --- a/backend/src/controllers/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -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); diff --git a/backend/src/ee/controllers/stripeController.ts b/backend/src/controllers/v1/stripeController.ts similarity index 100% rename from backend/src/ee/controllers/stripeController.ts rename to backend/src/controllers/v1/stripeController.ts diff --git a/backend/src/controllers/userActionController.ts b/backend/src/controllers/v1/userActionController.ts similarity index 97% rename from backend/src/controllers/userActionController.ts rename to backend/src/controllers/v1/userActionController.ts index 8203aa427..c7a3c2337 100644 --- a/backend/src/controllers/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -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] diff --git a/backend/src/controllers/userController.ts b/backend/src/controllers/v1/userController.ts similarity index 100% rename from backend/src/controllers/userController.ts rename to backend/src/controllers/v1/userController.ts diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts similarity index 97% rename from backend/src/controllers/workspaceController.ts rename to backend/src/controllers/v1/workspaceController.ts index a402834d7..bd2ecb15a 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -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] diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts new file mode 100644 index 000000000..dc6977c91 --- /dev/null +++ b/backend/src/controllers/v2/index.ts @@ -0,0 +1,5 @@ +import * as workspaceController from './workspaceController'; + +export { + workspaceController +} diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts new file mode 100644 index 000000000..86693b6c4 --- /dev/null +++ b/backend/src/controllers/v2/workspaceController.ts @@ -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 + }); +}; \ No newline at end of file diff --git a/backend/src/ee/controllers/index.ts b/backend/src/ee/controllers/index.ts deleted file mode 100644 index e4fb89a8e..000000000 --- a/backend/src/ee/controllers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as stripeController from './stripeController'; - -export { - stripeController -} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts new file mode 100644 index 000000000..23880070d --- /dev/null +++ b/backend/src/ee/controllers/v1/index.ts @@ -0,0 +1,9 @@ +import * as stripeController from './stripeController'; +import * as secretController from './secretController'; +import * as workspaceController from './workspaceController'; + +export { + stripeController, + secretController, + workspaceController +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts new file mode 100644 index 000000000..b2d66ab33 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -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 + }); +} \ No newline at end of file diff --git a/backend/src/controllers/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts similarity index 91% rename from backend/src/controllers/stripeController.ts rename to backend/src/ee/controllers/v1/stripeController.ts index 99a85ee2b..faef14cea 100644 --- a/backend/src/controllers/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -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' }); diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts new file mode 100644 index 000000000..8b7ba422e --- /dev/null +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -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 + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/license.ts b/backend/src/ee/helpers/license.ts deleted file mode 100644 index 256bdc23a..000000000 --- a/backend/src/ee/helpers/license.ts +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts new file mode 100644 index 000000000..a688a108f --- /dev/null +++ b/backend/src/ee/helpers/secret.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts new file mode 100644 index 000000000..35d41c19a --- /dev/null +++ b/backend/src/ee/models/index.ts @@ -0,0 +1,9 @@ +import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; +import SecretVersion, { ISecretVersion } from "./secretVersion"; + +export { + SecretSnapshot, + ISecretSnapshot, + SecretVersion, + ISecretVersion +} \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts new file mode 100644 index 000000000..69633a92e --- /dev/null +++ b/backend/src/ee/models/secretSnapshot.ts @@ -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( + { + 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('SecretSnapshot', secretSnapshotSchema); + +export default SecretSnapshot; \ No newline at end of file diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts new file mode 100644 index 000000000..a93a037f6 --- /dev/null +++ b/backend/src/ee/models/secretVersion.ts @@ -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( + { + 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('SecretVersion', secretVersionSchema); + +export default SecretVersion; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts new file mode 100644 index 000000000..960665f4a --- /dev/null +++ b/backend/src/ee/routes/v1/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts new file mode 100644 index 000000000..a866a6320 --- /dev/null +++ b/backend/src/ee/routes/v1/secret.ts @@ -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; \ No newline at end of file diff --git a/backend/src/ee/routes/stripe.ts b/backend/src/ee/routes/v1/stripe.ts similarity index 71% rename from backend/src/ee/routes/stripe.ts rename to backend/src/ee/routes/v1/stripe.ts index 6f89f5655..02d68c4ea 100644 --- a/backend/src/ee/routes/stripe.ts +++ b/backend/src/ee/routes/v1/stripe.ts @@ -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); diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts new file mode 100644 index 000000000..6756269e5 --- /dev/null +++ b/backend/src/ee/routes/v1/workspace.ts @@ -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; \ No newline at end of file diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts new file mode 100644 index 000000000..f31482dde --- /dev/null +++ b/backend/src/ee/services/EELicenseService.ts @@ -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); \ No newline at end of file diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts new file mode 100644 index 000000000..643f763f1 --- /dev/null +++ b/backend/src/ee/services/EESecretService.ts @@ -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; \ No newline at end of file diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts new file mode 100644 index 000000000..3cec256bb --- /dev/null +++ b/backend/src/ee/services/index.ts @@ -0,0 +1,7 @@ +import EELicenseService from "./EELicenseService"; +import EESecretService from "./EESecretService"; + +export { + EELicenseService, + EESecretService +} \ No newline at end of file diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index abaf73af4..b3f276b53 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -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'; diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index b06460cde..b237803f1 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -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'); diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 042aba4fa..f055971ae 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -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 => { + // 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 => { + // 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 }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index cb0ff84e0..3e7076118 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -21,7 +21,6 @@ import { CLIENT_SECRET_NETLIFY, CLIENT_SECRET_GITHUB } from '../config'; -import { user } from '../routes'; interface ExchangeCodeHerokuResponse { token_type: string; diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index b83ef728d..d36e32b91 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -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( { + version: { + type: Number, + required: true + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace', @@ -77,6 +86,22 @@ const secretSchema = new Schema( 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 } }, { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/v1/auth.ts similarity index 79% rename from backend/src/routes/auth.ts rename to backend/src/routes/v1/auth.ts index 6be9c09f7..ace15421e 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -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); diff --git a/backend/src/routes/bot.ts b/backend/src/routes/v1/bot.ts similarity index 83% rename from backend/src/routes/bot.ts rename to backend/src/routes/v1/bot.ts index 3189bec44..90a0aa285 100644 --- a/backend/src/routes/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -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', diff --git a/backend/src/routes/index.ts b/backend/src/routes/v1/index.ts similarity index 100% rename from backend/src/routes/index.ts rename to backend/src/routes/v1/index.ts diff --git a/backend/src/routes/integration.ts b/backend/src/routes/v1/integration.ts similarity index 85% rename from backend/src/routes/integration.ts rename to backend/src/routes/v1/integration.ts index e6738a803..f16aad23b 100644 --- a/backend/src/routes/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -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', diff --git a/backend/src/routes/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts similarity index 89% rename from backend/src/routes/integrationAuth.ts rename to backend/src/routes/v1/integrationAuth.ts index ef80a2dcc..004706786 100644 --- a/backend/src/routes/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -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', diff --git a/backend/src/routes/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts similarity index 80% rename from backend/src/routes/inviteOrg.ts rename to backend/src/routes/v1/inviteOrg.ts index 16ab64f85..5c89b022f 100644 --- a/backend/src/routes/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -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', diff --git a/backend/src/routes/key.ts b/backend/src/routes/v1/key.ts similarity index 82% rename from backend/src/routes/key.ts rename to backend/src/routes/v1/key.ts index a67a729b1..094d18e72 100644 --- a/backend/src/routes/key.ts +++ b/backend/src/routes/v1/key.ts @@ -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', diff --git a/backend/src/routes/membership.ts b/backend/src/routes/v1/membership.ts similarity index 83% rename from backend/src/routes/membership.ts rename to backend/src/routes/v1/membership.ts index 04d94f576..4d81aa6dc 100644 --- a/backend/src/routes/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -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', diff --git a/backend/src/routes/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts similarity index 78% rename from backend/src/routes/membershipOrg.ts rename to backend/src/routes/v1/membershipOrg.ts index b6cd6313f..c74d20449 100644 --- a/backend/src/routes/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -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 diff --git a/backend/src/routes/organization.ts b/backend/src/routes/v1/organization.ts similarity index 95% rename from backend/src/routes/organization.ts rename to backend/src/routes/v1/organization.ts index 276e26095..1fff9b597 100644 --- a/backend/src/routes/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -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( '/', diff --git a/backend/src/routes/password.ts b/backend/src/routes/v1/password.ts similarity index 94% rename from backend/src/routes/password.ts rename to backend/src/routes/v1/password.ts index 8032cba83..2cb811de5 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/v1/password.ts @@ -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', diff --git a/backend/src/routes/secret.ts b/backend/src/routes/v1/secret.ts similarity index 87% rename from backend/src/routes/secret.ts rename to backend/src/routes/v1/secret.ts index 98b3009de..e2dcb3368 100644 --- a/backend/src/routes/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -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', diff --git a/backend/src/routes/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts similarity index 85% rename from backend/src/routes/serviceToken.ts rename to backend/src/routes/v1/serviceToken.ts index 00195edee..57031f3e0 100644 --- a/backend/src/routes/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -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 diff --git a/backend/src/routes/signup.ts b/backend/src/routes/v1/signup.ts similarity index 89% rename from backend/src/routes/signup.ts rename to backend/src/routes/v1/signup.ts index 40b5929b0..3ea3b2738 100644 --- a/backend/src/routes/signup.ts +++ b/backend/src/routes/v1/signup.ts @@ -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', diff --git a/backend/src/routes/stripe.ts b/backend/src/routes/v1/stripe.ts similarity index 71% rename from backend/src/routes/stripe.ts rename to backend/src/routes/v1/stripe.ts index ba5706562..cfcca77cb 100644 --- a/backend/src/routes/stripe.ts +++ b/backend/src/routes/v1/stripe.ts @@ -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); diff --git a/backend/src/routes/user.ts b/backend/src/routes/v1/user.ts similarity index 58% rename from backend/src/routes/user.ts rename to backend/src/routes/v1/user.ts index 4a25c48fb..393978922 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/v1/user.ts @@ -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); diff --git a/backend/src/routes/userAction.ts b/backend/src/routes/v1/userAction.ts similarity index 73% rename from backend/src/routes/userAction.ts rename to backend/src/routes/v1/userAction.ts index 5ea454156..544b948e8 100644 --- a/backend/src/routes/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -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( '/', diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/v1/workspace.ts similarity index 92% rename from backend/src/routes/workspace.ts rename to backend/src/routes/v1/workspace.ts index 1d20c102a..6a1ae9034 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -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', diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts new file mode 100644 index 000000000..6e6758753 --- /dev/null +++ b/backend/src/routes/v2/index.ts @@ -0,0 +1,7 @@ +import secret from './secret'; +import workspace from './workspace'; + +export { + secret, + workspace +} diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts new file mode 100644 index 000000000..17a91d39c --- /dev/null +++ b/backend/src/routes/v2/secret.ts @@ -0,0 +1,4 @@ +import express from 'express'; +const router = express.Router(); + +export default router; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts new file mode 100644 index 000000000..98eacd9ea --- /dev/null +++ b/backend/src/routes/v2/workspace.ts @@ -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; diff --git a/backend/src/utils/patchAsyncRoutes.js b/backend/src/utils/patchAsyncRoutes.js index 6f6d2367f..24fe007f9 100644 --- a/backend/src/utils/patchAsyncRoutes.js +++ b/backend/src/utils/patchAsyncRoutes.js @@ -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; }, -}); \ No newline at end of file +}); + +module.exports = { + patchRouterParam +}; diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 6a0bdb80c..173df36df 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -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. diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 7518fe98d..6e0133c8e 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -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 ...\")") } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 0d1f96a05..8ba1c4627 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -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 { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index c127111b1..f88aed96b 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -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 +} diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 2c65ef53e..4afd7586d 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -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` | diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 144259023..0c998feb4 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -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 ``` + + + Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin) + + ```bash + yay -S infisical-bin + ``` + diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index aac5c1b74..fe08b60b3 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -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: diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 0b9fd5e71..f598336b6 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -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` | diff --git a/frontend/components/RouteGuard.js b/frontend/components/RouteGuard.js index d08972b99..c5f6feb35 100644 --- a/frontend/components/RouteGuard.js +++ b/frontend/components/RouteGuard.js @@ -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) { diff --git a/frontend/components/basic/InputField.tsx b/frontend/components/basic/InputField.tsx index 08afa975d..241410959 100644 --- a/frontend/components/basic/InputField.tsx +++ b/frontend/components/basic/InputField.tsx @@ -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); diff --git a/frontend/components/basic/Toggle.tsx b/frontend/components/basic/Toggle.tsx new file mode 100644 index 000000000..d15aed622 --- /dev/null +++ b/frontend/components/basic/Toggle.tsx @@ -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 ( + { + 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`} + > + Enable notifications + + + ) +} diff --git a/frontend/components/basic/buttons/Button.tsx b/frontend/components/basic/buttons/Button.tsx index 81672adc1..9197ccf13 100644 --- a/frontend/components/basic/buttons/Button.tsx +++ b/frontend/components/basic/buttons/Button.tsx @@ -16,7 +16,7 @@ type ButtonProps = { size: string; icon?: IconProp; active?: boolean; - iconDisabled?: string; + iconDisabled?: IconProp; textDisabled?: string; type?: ButtonHTMLAttributes['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", diff --git a/frontend/components/basic/popups/BottomRightPopup.js b/frontend/components/basic/popups/BottomRightPopup.tsx similarity index 71% rename from frontend/components/basic/popups/BottomRightPopup.js rename to frontend/components/basic/popups/BottomRightPopup.tsx index 85aaba725..e6ea60f92 100644 --- a/frontend/components/basic/popups/BottomRightPopup.js +++ b/frontend/components/basic/popups/BottomRightPopup.tsx @@ -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 (