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/.gitignore b/.gitignore index 6c4414313..b8341e81b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ node_modules .DS_Store /dist +/completions/ +/manpages/ # frontend diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5e94e0de6..51154a4f3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -6,6 +6,11 @@ # - cd cli && go mod tidy # # you may remove this if you don't need go generate # - cd cli && go generate ./... +before: + hooks: + - ./cli/scripts/completions.sh + - ./cli/scripts/manpages.sh + builds: - id: darwin-build binary: infisical @@ -30,13 +35,13 @@ builds: - openbsd - windows goarch: - - 386 + - "386" - amd64 - arm - arm64 goarm: - - 6 - - 7 + - "6" + - "7" ignore: - goos: windows goarch: "386" @@ -44,6 +49,16 @@ builds: goarch: "386" dir: ./cli +archives: + - format_overrides: + - goos: windows + format: zip + files: + - README* + - LICENSE* + - manpages/* + - completions/* + release: replace_existing_draft: true mode: 'replace' @@ -85,13 +100,22 @@ nfpms: homepage: https://infisical.com/ maintainer: Infisical, Inc description: The offical Infisical CLI - license: Apache 2.0 + license: MIT formats: - rpm - deb - apk - archlinux bindir: /usr/bin + contents: + - src: ./completions/infisical.bash + dst: /etc/bash_completion.d/infisical + - src: ./completions/infisical.fish + dst: /usr/share/fish/vendor_completions.d/infisical.fish + - src: ./completions/infisical.zsh + dst: /usr/share/zsh/site-functions/_infisical + - src: ./manpages/infisical.1.gz + dst: /usr/share/man/man1/infisical.1.gz scoop: bucket: owner: Infisical @@ -101,7 +125,31 @@ 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" + # completions + mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" + mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" + mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" + install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical" + install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical" + install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" + # man pages + install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" # dockers: # - dockerfile: goreleaser.dockerfile # goos: linux diff --git a/README.md b/README.md index 292f5fe94..b3bcd7ac6 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** @@ -270,13 +270,13 @@ We're currently setting the foundation and building [integrations](https://infis - - โœ”๏ธ Ruby on Rails + + โœ”๏ธ Vue - - โœ”๏ธ Vue + + โœ”๏ธ Ruby on Rails @@ -292,6 +292,16 @@ We're currently setting the foundation and building [integrations](https://infis + + + + โœ”๏ธ .NET + + + + And more... + + @@ -321,4 +331,10 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of - + + +## ๐ŸŒŽ Translations + +Infisical is currently aviable in English and Korean. Help us translate Infisical to your language! + +You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 89d371e2a..fc03ec328 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6677,9 +6677,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -17175,9 +17175,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true }, "jsonwebtoken": { diff --git a/backend/src/app.ts b/backend/src/app.ts index 7a263acee..f37fd7560 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,28 +14,31 @@ import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, secret as eeSecretRouter -} from './ee/routes'; - +} from './ee/routes/v1'; 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, - serviceTokenData as serviceTokenDataRouter -} from './routes'; + 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, + serviceTokenData as v1ServiceTokenDataRouter, + 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'; @@ -64,29 +67,33 @@ if (NODE_ENV === 'production') { app.use(helmet()); } -// /ee routers +// (EE) routes app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); -// 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); -app.use('/api/v1/service-token-data', serviceTokenDataRouter); +// 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); // deprecate +app.use('/api/v1/service-token-data', v1ServiceTokenDataRouter); +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 app.use((req, res, next)=>{ 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 91% rename from backend/src/controllers/index.ts rename to backend/src/controllers/v1/index.ts index 1da61835f..56c6071c5 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -14,6 +14,7 @@ import * as stripeController from './stripeController'; import * as userActionController from './userActionController'; import * as userController from './userController'; import * as workspaceController from './workspaceController'; +import * as serviceTokenDataController from './serviceTokenDataController'; export { authController, @@ -31,5 +32,6 @@ export { stripeController, userActionController, userController, - workspaceController + workspaceController, + serviceTokenDataController }; 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 6672c4b49..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, Secret } 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'; } diff --git a/backend/src/controllers/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts similarity index 73% rename from backend/src/controllers/serviceTokenController.ts rename to backend/src/controllers/v1/serviceTokenController.ts index 43a48b558..244a58783 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 @@ -11,7 +11,6 @@ import { JWT_SERVICE_SECRET } from '../config'; * @returns */ export const getServiceToken = async (req: Request, res: Response) => { - // get service token return res.status(200).send({ serviceToken: req.serviceToken }); @@ -73,13 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => { return res.status(200).send({ token }); -}; - -/** - * SERVICE_TOKEN: , - * - authorizes the service token for "service token"-only endpoints. - * - authorizes the service token to pull secrets via that endpoint. - * - * - * - */ \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v1/serviceTokenDataController.ts new file mode 100644 index 000000000..83701abcd --- /dev/null +++ b/backend/src/controllers/v1/serviceTokenDataController.ts @@ -0,0 +1,102 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import { + ServiceTokenData +} from '../../models'; +import { + SALT_ROUNDS +} from '../../config'; + +/** + * Return service token data associated with service token on request + * @param req + * @param res + * @returns + */ +export const getServiceTokenData = async (req: Request, res: Response) => ({ + serviceTokenData: req.serviceTokenData +}); + +/** + * Create new service token data for workspace with id [workspaceId] and + * environment [environment]. + * @param req + * @param res + * @returns + */ +export const createServiceTokenData = async (req: Request, res: Response) => { + let serviceToken, serviceTokenData; + try { + const { + name, + workspaceId, + environment, + encryptedKey, + iv, + tag, + expiresIn + } = req.body; + + // create 41-char service token with first 9-char being the prefix + serviceToken = `st.${crypto.randomBytes(19).toString('hex')}`; + + const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS); + + // compute access token expiration date + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + + serviceTokenData = await new ServiceTokenData({ + name, + workspace: workspaceId, + environment, + user: req.user._id, + expiresAt, + prefix: serviceToken.substring(0, 9), + serviceTokenHash, + encryptedKey, + iv, + tag + }).save(); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create service token data' + }); + } + + return res.status(200).send({ + serviceToken, + serviceTokenData + }); +} + +/** + * Delete service token data with id [serviceTokenDataId]. + * @param req + * @param res + * @returns + */ +export const deleteServiceTokenData = async (req: Request, res: Response) => { + let serviceTokenData; + try { + const { serviceTokenDataId } = req.params; + + serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); +} \ No newline at end of file diff --git a/backend/src/controllers/signupController.ts b/backend/src/controllers/v1/signupController.ts similarity index 95% rename from backend/src/controllers/signupController.ts rename to backend/src/controllers/v1/signupController.ts index 42bef4973..62e5a62a3 100644 --- a/backend/src/controllers/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,15 +1,15 @@ 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'; /** 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 a843ed9d6..d34fecbd7 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -8,14 +8,14 @@ import { IntegrationAuth, IUser, ServiceToken, - ServiceTokenData, -} from '../models'; + ServiceTokenData +} 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..fa0ffa5af --- /dev/null +++ b/backend/src/controllers/v2/workspaceController.ts @@ -0,0 +1,194 @@ +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; +} + +/** + * 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] + * @param req + * @param res + * @returns + */ +export const pullSecrets = async (req: Request, res: Response) => { + let secrets; + try { + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; + + let userId; + if (req.user) { + userId = req.user._id.toString(); + } else if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id + } + + secrets = await pull({ + userId, + workspaceId, + environment + }); + + if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs + 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 + }); +}; + +export const getWorkspaceKey = async (req: Request, res: Response) => { + let key; + try { + const { workspaceId } = req.params; + + key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }).populate('sender', '+publicKey'); + + if (!key) throw new Error('Failed to find workspace key'); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace key' + }); + } + + return res.status(200).send({ + key + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/index.ts b/backend/src/ee/controllers/v1/index.ts similarity index 100% rename from backend/src/ee/controllers/index.ts rename to backend/src/ee/controllers/v1/index.ts diff --git a/backend/src/ee/controllers/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts similarity index 94% rename from backend/src/ee/controllers/secretController.ts rename to backend/src/ee/controllers/v1/secretController.ts index 503a81c51..b2d66ab33 100644 --- a/backend/src/ee/controllers/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { SecretVersion } from '../models'; +import { SecretVersion } from '../../models'; /** * Return secret versions for secret with id [secretId] 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/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts similarity index 94% rename from backend/src/ee/controllers/workspaceController.ts rename to backend/src/ee/controllers/v1/workspaceController.ts index 423e71793..8b7ba422e 100644 --- a/backend/src/ee/controllers/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { SecretSnapshot } from '../models'; +import { SecretSnapshot } from '../../models'; /** * Return secret snapshots for workspace with id [workspaceId] diff --git a/backend/src/ee/routes/index.ts b/backend/src/ee/routes/v1/index.ts similarity index 100% rename from backend/src/ee/routes/index.ts rename to backend/src/ee/routes/v1/index.ts diff --git a/backend/src/ee/routes/secret.ts b/backend/src/ee/routes/v1/secret.ts similarity index 71% rename from backend/src/ee/routes/secret.ts rename to backend/src/ee/routes/v1/secret.ts index d8f1cb05b..4133c9525 100644 --- a/backend/src/ee/routes/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -4,14 +4,16 @@ import { requireAuth, requireWorkspaceAuth, validateRequest -} from '../../middleware'; +} from '../../../middleware'; import { body, query, param } from 'express-validator'; -import { secretController } from '../controllers'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { secretController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; router.get( '/:secretId/secret-versions', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] 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/workspace.ts b/backend/src/ee/routes/v1/workspace.ts similarity index 72% rename from backend/src/ee/routes/workspace.ts rename to backend/src/ee/routes/v1/workspace.ts index e27300eb3..d127599e1 100644 --- a/backend/src/ee/routes/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -4,14 +4,16 @@ import { requireAuth, requireWorkspaceAuth, validateRequest -} from '../../middleware'; +} from '../../../middleware'; import { param, query } from 'express-validator'; -import { ADMIN, MEMBER, GRANTED } from '../../variables'; -import { workspaceController } from '../controllers'; +import { ADMIN, MEMBER, GRANTED } from '../../../variables'; +import { workspaceController } from '../../controllers/v1'; router.get( '/:workspaceId/secret-snapshots', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 7d5f2461f..9845e9c74 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,15 +1,69 @@ import jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; +import bcrypt from 'bcrypt'; import { - User + User, + ServiceTokenData } from '../models'; import { JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, - JWT_REFRESH_SECRET + JWT_REFRESH_SECRET, + SALT_ROUNDS } from '../config'; +/** + * Attach auth payload + * @param {Object} obj + * @param {String} obj.authTokenValue + */ +const attachAuthPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let serviceTokenHash, decodedToken; // intermediate variables + let serviceTokenData, user; // payloads + try { + switch (authTokenValue.split('.', 1)[0]) { + case 'st': + // case: service token auth mode + serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); + serviceTokenData = await ServiceTokenData + .findOne({ + serviceTokenHash + }) + .select('+encryptedKey +iv +tag'); + + if (!serviceTokenData) { + throw new Error('Account not found error'); + } + + return serviceTokenData; + default: + // case: JWT token auth mode + decodedToken = ( + jwt.verify(authTokenValue, JWT_AUTH_SECRET) + ); + + user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); + + if (!user) + throw new Error('Account not found error'); + + if (!user?.publicKey) + throw new Error('Unable to authenticate due to partially set up account'); + + return user; + } + } catch (err) { + throw new Error('Failed to attach auth payload'); + } +} + /** * Return newly issued (JWT) auth and refresh tokens to user with id [userId] * @param {Object} obj @@ -99,4 +153,9 @@ const createToken = ({ } }; -export { createToken, issueTokens, clearTokens }; +export { + attachAuthPayload, + createToken, + issueTokens, + clearTokens +}; 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 4b3585c40..f055971ae 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -14,9 +14,8 @@ import { } from '../ee/helpers/secret'; import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; -import { LICENSE_KEY } from '../config'; -interface PushSecret { +interface V1PushSecret { ciphertextKey: string; ivKey: string; tagKey: string; @@ -25,9 +24,29 @@ 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]: any; } @@ -45,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, @@ -54,7 +73,7 @@ const pushSecrets = async ({ userId: string; workspaceId: string; environment: string; - secrets: PushSecret[]; + secrets: V1PushSecret[]; }): Promise => { // TODO: clean up function and fix up types try { @@ -93,8 +112,9 @@ const pushSecrets = async ({ const toUpdate = oldSecrets .filter((s) => { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { - if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue) { - // case: filter secrets where value changed + 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; } @@ -113,14 +133,22 @@ const pushSecrets = async ({ ciphertextValue, ivValue, tagValue, - hashValue + hashValue, + ciphertextComment, + ivComment, + tagComment, + hashComment } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; const update: Update = { secretValueCiphertext: ciphertextValue, secretValueIV: ivValue, secretValueTag: tagValue, - secretValueHash: hashValue + secretValueHash: hashValue, + secretCommentCiphertext: ciphertextComment, + secretCommentIV: ivComment, + secretCommentTag: tagComment, + secretCommentHash: hashComment, } if (!s.version) { @@ -192,7 +220,254 @@ 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') { + 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); + 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') { @@ -315,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) { @@ -326,73 +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/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index d917d362a..52f69d260 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,6 +1,9 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; -import { User } from '../models'; +import { User, ServiceTokenData } from '../models'; +import { + attachAuthPayload +} from '../helpers/auth'; import { JWT_AUTH_SECRET } from '../config'; import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; @@ -11,34 +14,58 @@ declare module 'jsonwebtoken' { } /** - * Validate if JWT (auth) token on request is valid (e.g. not expired), - * if there is an associated user, and if that user is fully setup. - * @param req - express request object - * @param res - express response object - * @param next - express next function + * Validate if token on request is valid (e.g. not expired) for various auth modes: + * - If token is a JWT token, then check if there is an associated user + * and if user is fully setup. + * - If token is a service token (st), then check if there is associated + * service token data. + * @param {Object} obj + * @param {String[]} obj.acceptedAuthModes - accepted modes of authentication (jwt/st) * @returns */ -const requireAuth = async (req: Request, res: Response, next: NextFunction) => { - // JWT authentication middleware - const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] - if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) - if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) - if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) +const requireAuth = ({ + acceptedAuthModes = ['jwt'] +}: { + acceptedAuthModes: string[]; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) + return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') + return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) + return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + + // validate auth mode + let authMode; + switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) { + case 'st': + authMode = 'st'; + break; + default: + authMode = 'jwt'; + break; + } + + if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); - const decodedToken = ( - jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) - ); + // attach auth request payload + const payload = await attachAuthPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); + + switch (authMode) { + case 'st': + req.serviceTokenData = payload; + break; + default: + req.user = payload; + break; + } - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); - - if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) - if (!user?.publicKey) - return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'})) - - req.user = user; - return next(); -}; + return next(); + } +} export default requireAuth; diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts index 1f2e33d7a..48eaac3dc 100644 --- a/backend/src/middleware/requireServiceTokenDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { ServiceToken, ServiceTokenData } from '../models'; import { validateMembership } from '../helpers/membership'; -import { AccountNotFoundError } from '../utils/errors'; +import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; @@ -15,21 +15,25 @@ const requireServiceTokenDataAuth = ({ location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - + const { serviceTokenDataId } = req[location]; + const serviceTokenData = await ServiceTokenData .findById(req[location].serviceTokenDataId) .select('+encryptedKey +iv +tag'); - + if (!serviceTokenData) { return next(AccountNotFoundError({message: 'Failed to locate service token data'})); } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: serviceTokenData.workspace.toString(), - acceptedRoles, - acceptedStatuses - }); + + if (req.user) { + // case: jwt auth + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: serviceTokenData.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + } req.serviceTokenData = serviceTokenData; diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index e5b8898f3..2a9110c2c 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -8,8 +8,8 @@ type req = 'params' | 'body' | 'query'; * Validate if user on request is a member with proper roles for workspace * on request params. * @param {Object} obj - * @param {String[]} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.acceptedStatuses - accepted workspace statuses + * @param {String[]} obj.acceptedRoles - accepted workspace roles for JWT auth + * @param {String[]} obj.acceptedStatuses - accepted workspace statuses for JWT auth * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireWorkspaceAuth = ({ @@ -22,17 +22,28 @@ const requireWorkspaceAuth = ({ location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // workspace authorization middleware - try { - const membership = await validateMembership({ - userId: req.user._id.toString(), - workspaceId: req[location].workspaceId, - acceptedRoles, - acceptedStatuses - }); + const { workspaceId } = req[location]; - req.membership = membership; + if (req.user) { + // case: jwt auth + const membership = await validateMembership({ + userId: req.user._id.toString(), + workspaceId, + acceptedRoles, + acceptedStatuses + }); + + req.membership = membership; + } + + if ( + req.serviceTokenData + && req.serviceTokenData.workspace !== workspaceId + && req.serviceTokenData.environment !== req.body.environment + ) + // case: st auth + next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) return next(); } catch (err) { diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index d34139ecb..d36e32b91 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -23,6 +23,10 @@ export interface ISecret { secretValueIV: string; secretValueTag: string; secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; } const secretSchema = new Schema( @@ -82,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/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts index 01bac8d58..32520dda9 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData .ts @@ -5,6 +5,7 @@ export interface IServiceTokenData { name: string; workspace: Types.ObjectId; environment: string; // TODO: adapt to upcoming environment id + user: Types.ObjectId; expiresAt: Date; prefix: string; serviceTokenHash: string; @@ -28,6 +29,11 @@ const serviceTokenDataSchema = new Schema( type: String, required: true }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, expiresAt: { type: Date }, @@ -38,7 +44,8 @@ const serviceTokenDataSchema = new Schema( serviceTokenHash: { type: String, unique: true, - required: true + required: true, + select: true }, encryptedKey: { type: String, diff --git a/backend/src/routes/serviceTokenData.ts b/backend/src/routes/serviceTokenData.ts deleted file mode 100644 index 903e72771..000000000 --- a/backend/src/routes/serviceTokenData.ts +++ /dev/null @@ -1,144 +0,0 @@ -import express from 'express'; -const router = express.Router(); -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; -import * as Sentry from '@sentry/node'; -import { - requireAuth, - requireWorkspaceAuth, - requireServiceTokenDataAuth, - validateRequest -} from '../middleware'; -import { - ServiceTokenData -} from '../models'; -import { param, body, query } from 'express-validator'; -import { - SALT_ROUNDS -} from '../config'; -import { - ADMIN, - MEMBER, - COMPLETED, - GRANTED -} from '../variables'; - -// TODO: move logic into separate controller (probably after pull with latest routing) - -/** - * 2 different concepts that we should distinguish between: - * - API key (user) - allows user to perform queries and mutations on whatever - * their account could access (better than JWT because it has ACL and scoping). - * - Service token (bound to a workspace and environment). - */ - -/** - * Service token flow? - * 1. Post service token data details including project key encrypted under on cient-side. - * 2. Construct on client-side as =. - * 3. Need for CLI to be able to get back service token details - */ - -router.post( - '/', - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED], - location: 'body' - }), - requireAuth, - body('name').exists().trim(), - body('workspace'), - body('environment'), - body('encryptedKey'), - body('iv'), - body('tag'), - body('expiresAt'), - validateRequest, - async (req, res) => { - let serviceToken, serviceTokenData; - try { - const { - name, - workspace, - environment, - encryptedKey, - iv, - tag, - expiresAt - } = req.body; - - // create 38-char service token with first 6-char being the prefix - serviceToken = crypto.randomBytes(19).toString('hex'); - - const serviceTokenHash = await bcrypt.hash(serviceToken, SALT_ROUNDS); - - serviceTokenData = await new ServiceTokenData({ - name, - workspace, - environment, - expiresAt, - prefix: serviceToken.substring(0, 6), - serviceTokenHash, - encryptedKey, - iv, - tag - }).save(); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create service token data' - }); - } - - return res.status(200).send({ - serviceToken, - serviceTokenData - }); - } -); - -// TODO: CLI has to get service token details without needing a JWT -router.get( - '/:serviceTokenDataId', - requireAuth, - requireServiceTokenDataAuth, - param('serviceTokenDataId').exists().trim(), - validateRequest, - async (req, res) => { - return ({ - serviceTokenData: req.serviceTokenData - }); - } -); - -router.delete( - '/:serviceTokenDataId', - requireAuth, - requireServiceTokenDataAuth, - param('serviceTokenDataId').exists().trim(), - validateRequest, - async (req, res) => { - let serviceTokenData; - try { - const { serviceTokenDataId } = req.params; - - serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete service token data' - }); - } - - return res.status(200).send({ - serviceTokenData - }); - } -); - -export default router; \ No newline at end of file diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts deleted file mode 100644 index 4a25c48fb..000000000 --- a/backend/src/routes/user.ts +++ /dev/null @@ -1,8 +0,0 @@ -import express from 'express'; -const router = express.Router(); -import { requireAuth } from '../middleware'; -import { userController } from '../controllers'; - -router.get('/', requireAuth, userController.getUser); - -export default router; diff --git a/backend/src/routes/auth.ts b/backend/src/routes/v1/auth.ts similarity index 58% rename from backend/src/routes/auth.ts rename to backend/src/routes/v1/auth.ts index 6be9c09f7..66816788b 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); @@ -25,7 +25,19 @@ router.post( authController.login2 ); -router.post('/logout', requireAuth, authController.logout); -router.post('/checkAuth', requireAuth, authController.checkAuth); +router.post( + '/logout', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + authController.logout +); +router.post( + '/checkAuth', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + authController.checkAuth +); export default router; diff --git a/backend/src/routes/bot.ts b/backend/src/routes/v1/bot.ts similarity index 73% rename from backend/src/routes/bot.ts rename to backend/src/routes/v1/bot.ts index 3189bec44..0d631b8eb 100644 --- a/backend/src/routes/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -6,13 +6,15 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -24,7 +26,9 @@ router.get( router.patch( '/:botId/active', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireBotAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] 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 76% rename from backend/src/routes/integration.ts rename to backend/src/routes/v1/integration.ts index e6738a803..3715bef46 100644 --- a/backend/src/routes/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -4,14 +4,16 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -29,7 +31,9 @@ router.patch( router.delete( '/:integrationId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] diff --git a/backend/src/routes/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts similarity index 75% rename from backend/src/routes/integrationAuth.ts rename to backend/src/routes/v1/integrationAuth.ts index ef80a2dcc..c613ce62f 100644 --- a/backend/src/routes/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -6,19 +6,23 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), integrationAuthController.getIntegrationOptions ); router.post( '/oauth-token', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED], @@ -33,7 +37,9 @@ router.post( router.get( '/:integrationAuthId/apps', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -45,7 +51,9 @@ router.get( router.delete( '/:integrationAuthId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED], diff --git a/backend/src/routes/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts similarity index 73% rename from backend/src/routes/inviteOrg.ts rename to backend/src/routes/v1/inviteOrg.ts index 16ab64f85..d8e2d67cc 100644 --- a/backend/src/routes/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -1,12 +1,14 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('inviteeEmail').exists().trim().notEmpty().isEmail(), body('organizationId').exists().trim().notEmpty(), validateRequest, diff --git a/backend/src/routes/key.ts b/backend/src/routes/v1/key.ts similarity index 71% rename from backend/src/routes/key.ts rename to backend/src/routes/v1/key.ts index a67a729b1..d8bc7c29e 100644 --- a/backend/src/routes/key.ts +++ b/backend/src/routes/v1/key.ts @@ -4,14 +4,16 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -24,7 +26,9 @@ router.post( router.get( '/:workspaceId/latest', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/backend/src/routes/membership.ts b/backend/src/routes/v1/membership.ts similarity index 65% rename from backend/src/routes/membership.ts rename to backend/src/routes/v1/membership.ts index 04d94f576..76bf3e57a 100644 --- a/backend/src/routes/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -1,12 +1,14 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('workspaceId').exists().trim(), validateRequest, membershipController.validateMembership @@ -14,7 +16,9 @@ router.get( // used for CLI (deprecate) router.delete( '/:membershipId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('membershipId').exists().trim(), validateRequest, membershipController.deleteMembership @@ -22,7 +26,9 @@ router.delete( router.post( '/:membershipId/change-role', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('role').exists().trim(), validateRequest, membershipController.changeMembershipRole diff --git a/backend/src/routes/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts similarity index 66% rename from backend/src/routes/membershipOrg.ts rename to backend/src/routes/v1/membershipOrg.ts index b6cd6313f..255b836aa 100644 --- a/backend/src/routes/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -1,13 +1,15 @@ 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 '/membershipOrg/:membershipOrgId/change-role', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('membershipOrgId'), validateRequest, membershipOrgController.changeMembershipOrgRole @@ -15,7 +17,9 @@ router.post( router.delete( '/:membershipOrgId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('membershipOrgId').exists().trim(), validateRequest, membershipOrgController.deleteMembershipOrg diff --git a/backend/src/routes/organization.ts b/backend/src/routes/v1/organization.ts similarity index 80% rename from backend/src/routes/organization.ts rename to backend/src/routes/v1/organization.ts index 276e26095..6404c3246 100644 --- a/backend/src/routes/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -5,19 +5,23 @@ 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( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), organizationController.getOrganizations ); router.post( // not used on frontend '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('organizationName').exists().trim().notEmpty(), validateRequest, organizationController.createOrganization @@ -25,7 +29,9 @@ router.post( // not used on frontend router.get( '/:organizationId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -37,7 +43,9 @@ router.get( router.get( '/:organizationId/users', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -49,7 +57,9 @@ router.get( router.get( '/:organizationId/my-workspaces', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -61,7 +71,9 @@ router.get( router.patch( '/:organizationId/name', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -74,7 +86,9 @@ router.patch( router.get( '/:organizationId/incidentContactOrg', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -86,7 +100,9 @@ router.get( router.post( '/:organizationId/incidentContactOrg', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -99,7 +115,9 @@ router.post( router.delete( '/:organizationId/incidentContactOrg', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -112,7 +130,9 @@ router.delete( router.post( '/:organizationId/customer-portal-session', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] @@ -124,7 +144,9 @@ router.post( router.get( '/:organizationId/subscriptions', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED] diff --git a/backend/src/routes/password.ts b/backend/src/routes/v1/password.ts similarity index 89% rename from backend/src/routes/password.ts rename to backend/src/routes/v1/password.ts index 8032cba83..784ef1813 100644 --- a/backend/src/routes/password.ts +++ b/backend/src/routes/v1/password.ts @@ -1,13 +1,15 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('clientPublicKey').exists().trim().notEmpty(), validateRequest, passwordController.srp1 @@ -16,7 +18,9 @@ router.post( router.post( '/change-password', passwordLimiter, - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('clientProof').exists().trim().notEmpty(), body('encryptedPrivateKey').exists().trim().notEmpty().notEmpty(), // private key encrypted under new pwd body('iv').exists().trim().notEmpty(), // new iv for private key @@ -54,7 +58,9 @@ router.get( router.post( '/backup-private-key', passwordLimiter, - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('clientProof').exists().trim().notEmpty(), body('encryptedPrivateKey').exists().trim().notEmpty(), // (backup) private key encrypted under a strong key body('iv').exists().trim().notEmpty(), // new iv for (backup) private key diff --git a/backend/src/routes/secret.ts b/backend/src/routes/v1/secret.ts similarity index 81% rename from backend/src/routes/secret.ts rename to backend/src/routes/v1/secret.ts index 26224fd87..c3f0626ce 100644 --- a/backend/src/routes/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -5,14 +5,16 @@ import { requireWorkspaceAuth, requireServiceTokenAuth, validateRequest -} from '../middleware'; +} from '../../middleware'; import { body, query, param } from 'express-validator'; -import { secretController } from '../controllers'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; +import { secretController } from '../../controllers/v1'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; router.post( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -28,7 +30,9 @@ router.post( router.get( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/backend/src/routes/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts similarity index 74% rename from backend/src/routes/serviceToken.ts rename to backend/src/routes/v1/serviceToken.ts index f9452db63..edc00a939 100644 --- a/backend/src/routes/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -5,12 +5,12 @@ 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'; -// Note to devs: service-token to be deprecated in favor of api-key +// note: deprecate service-token routes in favor of service-token data routes/structure router.get( '/', @@ -20,7 +20,9 @@ router.get( router.post( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED], diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v1/serviceTokenData.ts new file mode 100644 index 000000000..fa9f4fdbd --- /dev/null +++ b/backend/src/routes/v1/serviceTokenData.ts @@ -0,0 +1,63 @@ +import express, { Request, Response } from 'express'; +const router = express.Router(); +import { + requireAuth, + requireWorkspaceAuth, + requireServiceTokenDataAuth, + validateRequest +} from '../../middleware'; +import { param, body } from 'express-validator'; +import { + ADMIN, + MEMBER, + COMPLETED, + GRANTED +} from '../../variables'; +import { serviceTokenDataController } from '../../controllers/v1'; + +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['st'] + }), + param('serviceTokenDataId').exists().trim(), + validateRequest, + serviceTokenDataController.getServiceTokenData +); + +router.post( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED], + location: 'body' + }), + body('name').exists().trim(), + body('workspaceId'), + body('environment'), + body('encryptedKey'), + body('iv'), + body('tag'), + body('expiresIn'), // measured in ms + validateRequest, + serviceTokenDataController.createServiceTokenData +); + +router.delete( + '/:serviceTokenDataId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireServiceTokenDataAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED], + }), + param('serviceTokenDataId').exists().trim(), + validateRequest, + serviceTokenDataController.deleteServiceTokenData +); + +export default router; \ No newline at end of file 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/v1/user.ts b/backend/src/routes/v1/user.ts new file mode 100644 index 000000000..e73a6e0f2 --- /dev/null +++ b/backend/src/routes/v1/user.ts @@ -0,0 +1,14 @@ +import express from 'express'; +const router = express.Router(); +import { requireAuth } from '../../middleware'; +import { userController } from '../../controllers/v1'; + +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + userController.getUser +); + +export default router; diff --git a/backend/src/routes/userAction.ts b/backend/src/routes/v1/userAction.ts similarity index 59% rename from backend/src/routes/userAction.ts rename to backend/src/routes/v1/userAction.ts index 5ea454156..4caa890c0 100644 --- a/backend/src/routes/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -1,12 +1,14 @@ 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( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('action'), validateRequest, userActionController.addUserAction @@ -14,7 +16,9 @@ router.post( router.get( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), query('action'), validateRequest, userActionController.getUserAction diff --git a/backend/src/routes/workspace.ts b/backend/src/routes/v1/workspace.ts similarity index 77% rename from backend/src/routes/workspace.ts rename to backend/src/routes/v1/workspace.ts index 3551841c7..7a1f8c5b8 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -5,13 +5,15 @@ 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', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -23,7 +25,9 @@ router.get( router.get( '/:workspaceId/users', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -33,11 +37,19 @@ router.get( workspaceController.getWorkspaceMemberships ); -router.get('/', requireAuth, workspaceController.getWorkspaces); +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + workspaceController.getWorkspaces +); router.get( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -49,7 +61,9 @@ router.get( router.post( '/', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('workspaceName').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(), validateRequest, @@ -58,7 +72,9 @@ router.post( router.delete( '/:workspaceId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], acceptedStatuses: [GRANTED] @@ -70,7 +86,9 @@ router.delete( router.post( '/:workspaceId/name', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] @@ -83,7 +101,9 @@ router.post( router.post( '/:workspaceId/invite-signup', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -96,7 +116,9 @@ router.post( router.get( '/:workspaceId/integrations', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -108,7 +130,9 @@ router.get( router.get( '/:workspaceId/authorizations', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -120,7 +144,9 @@ router.get( router.get( '/:workspaceId/service-tokens', // deprecate - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] @@ -132,7 +158,9 @@ router.get( router.get( '/:workspaceId/service-token-data', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] 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..6fc81bbc5 --- /dev/null +++ b/backend/src/routes/v2/workspace.ts @@ -0,0 +1,62 @@ +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.post( // unfinished + '/:workspaceId/secrets', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + 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( // unfinished, check that it works with st + '/:workspaceId/secrets', + requireAuth({ + acceptedAuthModes: ['jwt', 'st'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + query('environment').exists().trim(), + query('channel'), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.pullSecrets +); + +router.get( + '/:workspaceId/key', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceKey +); + +export default router; diff --git a/cli/go.mod b/cli/go.mod index 86cc1763f..c48e3e2f9 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -4,6 +4,8 @@ go 1.19 require ( github.com/99designs/keyring v1.2.2 + github.com/muesli/mango-cobra v1.2.0 + github.com/muesli/roff v0.1.0 github.com/spf13/cobra v1.6.1 golang.org/x/crypto v0.3.0 golang.org/x/term v0.3.0 @@ -22,6 +24,8 @@ require ( github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/mtibben/percent v0.2.1 // indirect + github.com/muesli/mango v0.1.0 // indirect + github.com/muesli/mango-pflag v0.1.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 3419b8051..d169d7b89 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -56,6 +56,14 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= +github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= +github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= +github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= +github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= +github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= diff --git a/cli/packages/cmd/man.go b/cli/packages/cmd/man.go new file mode 100644 index 000000000..efe8686b3 --- /dev/null +++ b/cli/packages/cmd/man.go @@ -0,0 +1,35 @@ +/* +Copyright ยฉ 2022 NAME HERE +*/ +package cmd + +import ( + "fmt" + "os" + + mcobra "github.com/muesli/mango-cobra" + "github.com/muesli/roff" + "github.com/spf13/cobra" +) + +var manCmd = &cobra.Command{ + Use: "man", + Short: "generates the manpages", + SilenceUsage: true, + DisableFlagsInUseLine: true, + Hidden: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + manPage, err := mcobra.NewManPage(1, rootCmd) + if err != nil { + return err + } + + _, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument())) + return err + }, +} + +func init() { + rootCmd.AddCommand(manCmd) +} diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 3aa9c5658..f09f08800 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -14,8 +14,8 @@ var rootCmd = &cobra.Command{ Use: "infisical", 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.15", + CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true}, + Version: "0.1.16", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cli/scripts/completions.sh b/cli/scripts/completions.sh new file mode 100755 index 000000000..6e69a1508 --- /dev/null +++ b/cli/scripts/completions.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +rm -rf completions +mkdir completions +cd cli +for sh in bash zsh fish; do + go run . completion "$sh" > "../completions/infisical.$sh" +done \ No newline at end of file diff --git a/cli/scripts/manpages.sh b/cli/scripts/manpages.sh new file mode 100755 index 000000000..db7e5c1b5 --- /dev/null +++ b/cli/scripts/manpages.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e +rm -rf manpages +mkdir manpages +cd cli +go run . man | gzip -c > "../manpages/infisical.1.gz" \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 15a200783..6cf75ad47 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -48,6 +48,8 @@ services: - ./frontend/public:/app/public - ./frontend/styles:/app/styles - ./frontend/components:/app/components + - ./frontend/locales:/app/locales + - ./frontend/next-i18next.config.js:/app/next-i18next.config.js env_file: .env environment: - NEXT_PUBLIC_ENV=development 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/integrations/frameworks/dotnet.mdx b/docs/integrations/frameworks/dotnet.mdx new file mode 100644 index 000000000..7a1d358c1 --- /dev/null +++ b/docs/integrations/frameworks/dotnet.mdx @@ -0,0 +1,27 @@ +--- +title: ".NET" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) +- [Install the CLI](/cli/overview) + +## Initialize Infisical for your [.NET](https://dotnet.microsoft.com) app + +```bash +# navigate to the root of your of your project +cd /path/to/project + +# then initialize infisical +infisical init +``` + +## Start your application as usual but with Infisical + +```bash +infisical run -- + +# Example +infisical run -- dotnet run +``` diff --git a/docs/mint.json b/docs/mint.json index cbac56d5a..e94b70a6b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -166,7 +166,8 @@ "integrations/frameworks/django", "integrations/frameworks/flask", "integrations/frameworks/laravel", - "integrations/frameworks/rails" + "integrations/frameworks/rails", + "integrations/frameworks/dotnet" ] }, { diff --git a/frontend/.gitignore b/frontend/.gitignore index 83e774c8c..5edd5a7fa 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -32,3 +32,5 @@ yarn-error.log* .env.production.local .vercel .env.infisical + +.vscode \ No newline at end of file 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/Layout.tsx b/frontend/components/basic/Layout.tsx index 56a16cb3d..5a8d4098c 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -1,38 +1,39 @@ /* eslint-disable no-unexpected-multiline */ /* eslint-disable react-hooks/exhaustive-deps */ -import { useEffect, useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; import { faBookOpen, faGear, faKey, faMobile, faPlug, - faUser -} from '@fortawesome/free-solid-svg-icons'; -import { faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + faUser, +} from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import getOrganizations from '~/pages/api/organization/getOrgs'; -import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; -import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers'; -import checkUserAction from '~/pages/api/userActions/checkUserAction'; -import addUserToWorkspace from '~/pages/api/workspace/addUserToWorkspace'; -import createWorkspace from '~/pages/api/workspace/createWorkspace'; -import getWorkspaces from '~/pages/api/workspace/getWorkspaces'; -import uploadKeys from '~/pages/api/workspace/uploadKeys'; +import getOrganizations from "~/pages/api/organization/getOrgs"; +import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects"; +import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers"; +import checkUserAction from "~/pages/api/userActions/checkUserAction"; +import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace"; +import createWorkspace from "~/pages/api/workspace/createWorkspace"; +import getWorkspaces from "~/pages/api/workspace/getWorkspaces"; +import uploadKeys from "~/pages/api/workspace/uploadKeys"; -import NavBarDashboard from '../navigation/NavBarDashboard'; -import onboardingCheck from '../utilities/checks/OnboardingCheck'; -import { tempLocalStorage } from '../utilities/checks/tempLocalStorage'; +import NavBarDashboard from "../navigation/NavBarDashboard"; +import onboardingCheck from "../utilities/checks/OnboardingCheck"; +import { tempLocalStorage } from "../utilities/checks/tempLocalStorage"; import { decryptAssymmetric, - encryptAssymmetric -} from '../utilities/cryptography/crypto'; -import Button from './buttons/Button'; -import AddWorkspaceDialog from './dialog/AddWorkspaceDialog'; -import Listbox from './Listbox'; + encryptAssymmetric, +} from "../utilities/cryptography/crypto"; +import Button from "./buttons/Button"; +import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog"; +import Listbox from "./Listbox"; interface LayoutProps { children: React.ReactNode; @@ -41,15 +42,17 @@ interface LayoutProps { export default function Layout({ children }: LayoutProps) { const router = useRouter(); const [workspaceList, setWorkspaceList] = useState([]); - const [workspaceMapping, setWorkspaceMapping] = useState([{ '1': '2' }]); - const [workspaceSelected, setWorkspaceSelected] = useState('โˆž'); - const [newWorkspaceName, setNewWorkspaceName] = useState(''); + const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]); + const [workspaceSelected, setWorkspaceSelected] = useState("โˆž"); + const [newWorkspaceName, setNewWorkspaceName] = useState(""); const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0); + const { t } = useTranslation(); + function closeModal() { setIsOpen(false); } @@ -75,35 +78,35 @@ export default function Layout({ children }: LayoutProps) { if (!currentWorkspaces.includes(workspaceName)) { const newWorkspace = await createWorkspace({ workspaceName, - organizationId: tempLocalStorage('orgData.id') + organizationId: tempLocalStorage("orgData.id"), }); const newWorkspaceId = newWorkspace._id; if (addAllUsers) { const orgUsers = await getOrganizationUsers({ - orgId: tempLocalStorage('orgData.id') + orgId: tempLocalStorage("orgData.id"), }); orgUsers.map(async (user: any) => { - if (user.status == 'accepted') { + if (user.status == "accepted") { const result = await addUserToWorkspace( user.user.email, newWorkspaceId ); if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY'); + const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY"); // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ ciphertext: result.latestKey.encryptedKey, nonce: result.latestKey.nonce, publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY + privateKey: PRIVATE_KEY, }); const { ciphertext, nonce } = encryptAssymmetric({ plaintext: key, publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY + privateKey: PRIVATE_KEY, }) as { ciphertext: string; nonce: string }; uploadKeys( @@ -116,11 +119,11 @@ export default function Layout({ children }: LayoutProps) { } }); } - router.push('/dashboard/' + newWorkspaceId + '?Development'); + router.push("/dashboard/" + newWorkspaceId + "?Development"); setIsOpen(false); - setNewWorkspaceName(''); + setNewWorkspaceName(""); } else { - console.error('A project with this name already exists.'); + console.error("A project with this name already exists."); setError(true); setLoading(false); } @@ -131,62 +134,65 @@ export default function Layout({ children }: LayoutProps) { } } - const menuItems = [ - { - href: - '/dashboard/' + - workspaceMapping[workspaceSelected as any] + - '?Development', - title: 'Secrets', - emoji: - }, - { - href: '/users/' + workspaceMapping[workspaceSelected as any], - title: 'Members', - emoji: - }, - { - href: '/integrations/' + workspaceMapping[workspaceSelected as any], - title: 'Integrations', - emoji: - }, - { - href: '/settings/project/' + workspaceMapping[workspaceSelected as any], - title: 'Project Settings', - emoji: - } - ]; + const menuItems = useMemo( + () => [ + { + href: + "/dashboard/" + + workspaceMapping[workspaceSelected as any] + + "?Development", + title: t("nav:menu.secrets"), + emoji: , + }, + { + href: "/users/" + workspaceMapping[workspaceSelected as any], + title: t("nav:menu.members"), + emoji: , + }, + { + href: "/integrations/" + workspaceMapping[workspaceSelected as any], + title: t("nav:menu.integrations"), + emoji: , + }, + { + href: "/settings/project/" + workspaceMapping[workspaceSelected as any], + title: t("nav:menu.project-settings"), + emoji: , + }, + ], + [t, workspaceMapping, workspaceSelected] + ); useEffect(() => { // Put a user in a workspace if they're not in one yet const putUserInWorkSpace = async () => { - if (tempLocalStorage('orgData.id') === '') { + if (tempLocalStorage("orgData.id") === "") { const userOrgs = await getOrganizations(); - localStorage.setItem('orgData.id', userOrgs[0]._id); + localStorage.setItem("orgData.id", userOrgs[0]._id); } const orgUserProjects = await getOrganizationUserProjects({ - orgId: tempLocalStorage('orgData.id') + orgId: tempLocalStorage("orgData.id"), }); const userWorkspaces = orgUserProjects; if ( userWorkspaces.length == 0 && - router.asPath != '/noprojects' && - !router.asPath.includes('settings') + router.asPath != "/noprojects" && + !router.asPath.includes("settings") ) { - router.push('/noprojects'); - } else if (router.asPath != '/noprojects') { + router.push("/noprojects"); + } else if (router.asPath != "/noprojects") { const intendedWorkspaceId = router.asPath - .split('/') - [router.asPath.split('/').length - 1].split('?')[0]; + .split("/") + [router.asPath.split("/").length - 1].split("?")[0]; // If a user is not a member of a workspace they are trying to access, just push them to one of theirs if ( - intendedWorkspaceId != 'heroku' && + intendedWorkspaceId != "heroku" && !userWorkspaces .map((workspace: { _id: string }) => workspace._id) .includes(intendedWorkspaceId) ) { - router.push('/dashboard/' + userWorkspaces[0]._id + '?Development'); + router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); } else { setWorkspaceList( userWorkspaces.map((workspace: any) => workspace.name) @@ -195,7 +201,7 @@ export default function Layout({ children }: LayoutProps) { Object.fromEntries( userWorkspaces.map((workspace: any) => [ workspace.name, - workspace._id + workspace._id, ]) ) as any ); @@ -203,12 +209,12 @@ export default function Layout({ children }: LayoutProps) { Object.fromEntries( userWorkspaces.map((workspace: any) => [ workspace._id, - workspace.name + workspace.name, ]) )[ router.asPath - .split('/') - [router.asPath.split('/').length - 1].split('?')[0] + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] ] ); } @@ -224,16 +230,16 @@ export default function Layout({ children }: LayoutProps) { workspaceMapping[workspaceSelected as any] && `${workspaceMapping[workspaceSelected as any]}` !== router.asPath - .split('/') - [router.asPath.split('/').length - 1].split('?')[0] + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] ) { router.push( - '/dashboard/' + + "/dashboard/" + workspaceMapping[workspaceSelected as any] + - '?Development' + "?Development" ); localStorage.setItem( - 'projectData.id', + "projectData.id", `${workspaceMapping[workspaceSelected as any]}` ); } @@ -257,7 +263,7 @@ export default function Layout({ children }: LayoutProps) {
- PROJECT + {t("nav:menu.project")}
{workspaceList.length > 0 ? ( 0 && menuItems.map(({ href, title, emoji }) => (
  • - {router.asPath.split('/')[1] === href.split('/')[1] && - (['project', 'billing', 'org', 'personal'].includes( - router.asPath.split('/')[2] + {router.asPath.split("/")[1] === href.split("/")[1] && + (["project", "billing", "org", "personal"].includes( + router.asPath.split("/")[2] ) - ? router.asPath.split('/')[2] === href.split('/')[2] + ? router.asPath.split("/")[2] === href.split("/")[2] : true) ? (
    {title}
    - ) : router.asPath == '/noprojects' ? ( + ) : router.asPath == "/noprojects" ? (
    @@ -323,7 +329,7 @@ export default function Layout({ children }: LayoutProps) {
    - {router.asPath.split('/')[1] === 'home' ? ( + {router.asPath.split("/")[1] === "home" ? (
    @@ -334,12 +340,12 @@ export default function Layout({ children }: LayoutProps) { Infisical Guide progress bar

    - {' '} - To use Infisical, please log in through a device with larger - dimensions.{' '} + {` ${t("common:no-mobile")} `}

    diff --git a/frontend/components/basic/Toggle.tsx b/frontend/components/basic/Toggle.tsx index fa69e4419..d15aed622 100644 --- a/frontend/components/basic/Toggle.tsx +++ b/frontend/components/basic/Toggle.tsx @@ -7,6 +7,7 @@ interface OverrideProps { keyName: string; value: string; pos: number; + comment: string; } interface ToggleProps { @@ -17,6 +18,7 @@ interface ToggleProps { value: string; pos: number; id: string; + comment: string; deleteOverride: (id: string) => void; sharedToHide: string[]; setSharedToHide: (values: string[]) => void; @@ -46,6 +48,7 @@ export default function Toggle ({ value, pos, id, + comment, deleteOverride, sharedToHide, setSharedToHide @@ -55,13 +58,12 @@ export default function Toggle ({ checked={enabled} onChange={() => { if (enabled == false) { - addOverride({ id, keyName, value, pos }); + addOverride({ id, keyName, value, pos, comment }); setSharedToHide([ ...sharedToHide!, id ]) } else { - setSharedToHide(sharedToHide!.filter(tempId => tempId != id)) deleteOverride(id); } setEnabled(!enabled); diff --git a/frontend/components/basic/dialog/ActivateBotDialog.js b/frontend/components/basic/dialog/ActivateBotDialog.js index 79d8cd693..7b362867f 100644 --- a/frontend/components/basic/dialog/ActivateBotDialog.js +++ b/frontend/components/basic/dialog/ActivateBotDialog.js @@ -1,12 +1,7 @@ import { Fragment } from "react"; +import { useTranslation } from "next-i18next"; import { Dialog, Transition } from "@headlessui/react"; -import setBotActiveStatus from "../../../pages/api/bot/setBotActiveStatus"; -import getLatestFileKey from "../../../pages/api/workspace/getLatestFileKey"; -import { - decryptAssymmetric, - encryptAssymmetric -} from "../../utilities/cryptography/crypto"; import Button from "../buttons/Button"; const ActivateBotDialog = ({ @@ -16,6 +11,7 @@ const ActivateBotDialog = ({ handleBotActivate, handleIntegrationOption }) => { + const { t } = useTranslation(); const submit = async () => { try { @@ -64,18 +60,18 @@ const ActivateBotDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400" > - Grant Infisical access to your secrets + {t("integrations:grant-access-to-secrets")}

    - Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over. + {t("integrations:why-infisical-needs-access")}

    diff --git a/frontend/components/basic/dialog/AddIncidentContactDialog.js b/frontend/components/basic/dialog/AddIncidentContactDialog.js index fad13cdee..535004045 100644 --- a/frontend/components/basic/dialog/AddIncidentContactDialog.js +++ b/frontend/components/basic/dialog/AddIncidentContactDialog.js @@ -1,4 +1,5 @@ import { Fragment, useState } from "react"; +import { useTranslation } from "next-i18next"; import { Dialog, Transition } from "@headlessui/react"; import addIncidentContact from "~/pages/api/organization/addIncidentContact"; @@ -14,6 +15,7 @@ const AddIncidentContactDialog = ({ setIncidentContacts, }) => { let [incidentContactEmail, setIncidentContactEmail] = useState(""); + const { t } = useTranslation(); const submit = () => { setIncidentContacts( @@ -59,17 +61,16 @@ const AddIncidentContactDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400" > - Add an Incident Contact + {t("section-incident:add-dialog.title")}

    - This contact will be notified in the unlikely event of a - severe incident. + {t("section-incident:add-dialog.description")}

    diff --git a/frontend/components/basic/dialog/AddProjectMemberDialog.js b/frontend/components/basic/dialog/AddProjectMemberDialog.js index 0d18bc3ca..e8dd610de 100644 --- a/frontend/components/basic/dialog/AddProjectMemberDialog.js +++ b/frontend/components/basic/dialog/AddProjectMemberDialog.js @@ -1,5 +1,6 @@ import { Fragment, useState } from "react"; import { useRouter } from "next/router"; +import { Trans, useTranslation } from "next-i18next"; import { Dialog, Transition } from "@headlessui/react"; import Button from "../buttons/Button"; @@ -15,6 +16,7 @@ const AddProjectMemberDialog = ({ setEmail, }) => { const router = useRouter(); + const { t } = useTranslation(); return (
    @@ -49,48 +51,55 @@ const AddProjectMemberDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400 z-50" > - Add a member to your project + {t("section-members:add-dialog.add-member-to-project")} ) : ( - All the users in your organization are already invited. + {t("section-members:add-dialog.already-all-invited")} )}
    {data?.length > 0 ? (

    - The user will receive an email with the instructions. + {t("section-members:add-dialog.user-will-email")}

    - - + + router.push( + "/settings/org/" + router.query.id + ) + } + />, + // eslint-disable-next-line react/jsx-key +
    ) : (

    - Add more users to the organization first. + {t("section-members:add-dialog.add-user-org-first")}

    )}
    @@ -110,7 +119,7 @@ const AddProjectMemberDialog = ({
    @@ -120,7 +129,7 @@ const AddProjectMemberDialog = ({ router.push("/settings/org/" + router.query.id) } color="mineshaft" - text="Add Users to Organization" + text={t("section-members:add-dialog.add-user-to-org")} size="md" /> )} diff --git a/frontend/components/basic/dialog/AddServiceTokenDialog.js b/frontend/components/basic/dialog/AddServiceTokenDialog.js index ddb20cce3..b02a804b7 100644 --- a/frontend/components/basic/dialog/AddServiceTokenDialog.js +++ b/frontend/components/basic/dialog/AddServiceTokenDialog.js @@ -1,40 +1,42 @@ -import { Fragment, useState } from 'react'; -import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Dialog, Transition } from '@headlessui/react'; -import nacl from 'tweetnacl'; +import { Fragment, useState } from "react"; +import { useTranslation } from "next-i18next"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dialog, Transition } from "@headlessui/react"; +import nacl from "tweetnacl"; -import addServiceToken from '~/pages/api/serviceToken/addServiceToken'; -import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; +import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; -import { envMapping } from '../../../public/data/frequentConstants'; +import { envMapping } from "../../../public/data/frequentConstants"; import { decryptAssymmetric, - encryptAssymmetric -} from '../../utilities/cryptography/crypto'; -import Button from '../buttons/Button'; -import InputField from '../InputField'; -import ListBox from '../Listbox'; + encryptAssymmetric, +} from "../../utilities/cryptography/crypto"; +import Button from "../buttons/Button"; +import InputField from "../InputField"; +import ListBox from "../Listbox"; const expiryMapping = { - '1 day': 86400, - '7 days': 604800, - '1 month': 2592000, - '6 months': 15552000, - '12 months': 31104000 + "1 day": 86400, + "7 days": 604800, + "1 month": 2592000, + "6 months": 15552000, + "12 months": 31104000, }; const AddServiceTokenDialog = ({ isOpen, closeModal, workspaceId, - workspaceName + workspaceName, }) => { - const [serviceToken, setServiceToken] = useState(''); - const [serviceTokenName, setServiceTokenName] = useState(''); - const [serviceTokenEnv, setServiceTokenEnv] = useState('Development'); - const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day'); + const [serviceToken, setServiceToken] = useState(""); + const [serviceTokenName, setServiceTokenName] = useState(""); + const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); + const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenCopied, setServiceTokenCopied] = useState(false); + const { t } = useTranslation(); const generateServiceToken = async () => { const latestFileKey = await getLatestFileKey({ workspaceId }); @@ -43,7 +45,7 @@ const AddServiceTokenDialog = ({ ciphertext: latestFileKey.latestKey.encryptedKey, nonce: latestFileKey.latestKey.nonce, publicKey: latestFileKey.latestKey.sender.publicKey, - privateKey: localStorage.getItem('PRIVATE_KEY') + privateKey: localStorage.getItem("PRIVATE_KEY"), }); // generate new public/private key pair @@ -55,7 +57,7 @@ const AddServiceTokenDialog = ({ const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({ plaintext: key, publicKey, - privateKey + privateKey, }); let newServiceToken = await addServiceToken({ @@ -65,16 +67,16 @@ const AddServiceTokenDialog = ({ expiresIn: expiryMapping[serviceTokenExpiresIn], publicKey, encryptedKey, - nonce + nonce, }); - const serviceToken = newServiceToken + ',' + privateKey; + const serviceToken = newServiceToken + "," + privateKey; setServiceToken(serviceToken); }; function copyToClipboard() { // Get the text field - var copyText = document.getElementById('serviceToken'); + var copyText = document.getElementById("serviceToken"); // Select the text field copyText.select(); @@ -91,8 +93,8 @@ const AddServiceTokenDialog = ({ const closeAddServiceTokenModal = () => { closeModal(); - setServiceTokenName(''); - setServiceToken(''); + setServiceTokenName(""); + setServiceToken(""); }; return ( @@ -122,27 +124,26 @@ const AddServiceTokenDialog = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - {serviceToken == '' ? ( + {serviceToken == "" ? ( - Add a service token for {workspaceName} + {t("section-token:add-dialog.title", { + target: workspaceName, + })}

    - Specify the name, environment, and expiry period. When - a token is generated, you will only be able to see it - once before it disappears. Make sure to save it - somewhere. + {t("section-token:add-dialog.description")}

    @@ -169,14 +170,14 @@ const AddServiceTokenDialog = ({ selected={serviceTokenExpiresIn} onChange={setServiceTokenExpiresIn} data={[ - '1 day', - '7 days', - '1 month', - '6 months', - '12 months' + "1 day", + "7 days", + "1 month", + "6 months", + "12 months", ]} - isFull={true} - text="Expires in: " + width="full" + text={`${t("common:expired-in")}: `} />
    @@ -184,10 +185,10 @@ const AddServiceTokenDialog = ({
    @@ -198,13 +199,14 @@ const AddServiceTokenDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400 z-50" > - Copy your service token + {t("section-token:add-dialog.copy-service-token")}

    - Once you close this popup, you will never see your - service token again + {t( + "section-token:add-dialog.copy-service-token-description" + )}

    @@ -234,7 +236,7 @@ const AddServiceTokenDialog = ({ )} - Click to Copy + {t("common.click-to-copy")}
  • diff --git a/frontend/components/dashboard/CommentField.tsx b/frontend/components/dashboard/CommentField.tsx new file mode 100644 index 000000000..ea29aa73c --- /dev/null +++ b/frontend/components/dashboard/CommentField.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "next-i18next"; + +/** + * This is the text field where people can add comments to particular secrets. + */ +const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => { + const { t } = useTranslation(); + + return
    +

    {t("dashboard:sidebar.comments")}

    +