From 4ad4efe9a5aef1141c76ec7df6a52c41caa8a3cd Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Thu, 15 Dec 2022 23:35:52 -0500 Subject: [PATCH 01/91] Added a basic framework for activity logs --- frontend/components/basic/Layout.tsx | 6 + .../components/basic/table/ActivityTable.tsx | 144 ++++++++++++++++++ frontend/pages/activity/[id].tsx | 62 ++++++++ 3 files changed, 212 insertions(+) create mode 100644 frontend/components/basic/table/ActivityTable.tsx create mode 100644 frontend/pages/activity/[id].tsx diff --git a/frontend/components/basic/Layout.tsx b/frontend/components/basic/Layout.tsx index 56a16cb3d..91f257ff9 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { + faBook, faBookOpen, faGear, faKey, @@ -150,6 +151,11 @@ export default function Layout({ children }: LayoutProps) { title: 'Integrations', emoji: }, + { + href: '/activity/' + workspaceMapping[workspaceSelected as any], + title: 'Activity', + emoji: + }, { href: '/settings/project/' + workspaceMapping[workspaceSelected as any], title: 'Project Settings', diff --git a/frontend/components/basic/table/ActivityTable.tsx b/frontend/components/basic/table/ActivityTable.tsx new file mode 100644 index 000000000..fe98277e7 --- /dev/null +++ b/frontend/components/basic/table/ActivityTable.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import { + faAngleDown, + faAngleRight, + faX +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import guidGenerator from '../../utilities/randomId'; + +interface ActivityTableProps { + eventName: string; + user: string; + source: string; + time: Date; +} + +function timeSince(date: Date) { + const seconds = Math.floor( + ((new Date() as any) - (date as any)) / 1000 + ) as number; + + let interval = seconds / 31536000; + + if (interval > 1) { + return Math.floor(interval) + ' years ago'; + } + interval = seconds / 2592000; + if (interval > 1) { + return Math.floor(interval) + ' months ago'; + } + interval = seconds / 86400; + if (interval > 1) { + return Math.floor(interval) + ' days ago'; + } + interval = seconds / 3600; + if (interval > 1) { + return Math.floor(interval) + ' hours ago'; + } + interval = seconds / 60; + if (interval > 1) { + return Math.floor(interval) + ' minutes ago'; + } + return Math.floor(seconds) + ' seconds ago'; +} + +const ActivityLogsRow = ({ row }: { row: ActivityTableProps }): JSX.Element => { + const [payloadOpened, setPayloadOpened] = useState(false); + return ( + <> + +
setPayloadOpened(!payloadOpened)} + className="border-mineshaft-700 border-t text-gray-300 flex items-center" + > + +
+ + {row.eventName} + + + {row.user} + + + {row.source} + + + {timeSince(row.time)} + + {/* +
+
+ */} + + {payloadOpened && ( + + +
+
Timestamp
+
2022-12-16T04:02:44.517Z
+
+
+
Number of Secrets
+
32
+
+
+
IP Address
+
159.223.164.24
+
+ + + )} + + ); +}; + +/** + * This is the table for activity logs (one of the tabs) + * @param {*} props + * @returns + */ +const ActivityTable = ({ data }: { data: ActivityTableProps[] }) => { + return ( +
+
+
+ + + + + + + + + + + + + {data.map((row, index) => { + return ; + })} + +
EventUserSourceTime
+
+
+ ); +}; + +export default ActivityTable; diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx new file mode 100644 index 000000000..5ffba0328 --- /dev/null +++ b/frontend/pages/activity/[id].tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; + +import ActivityTable from '~/components/basic/table/ActivityTable'; +import NavHeader from '~/components/navigation/NavHeader'; +import onboardingCheck from '~/components/utilities/checks/OnboardingCheck'; + +const data = [ + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + } +]; + +/** + * This tab is called Home because in the future it will include some company news, + * updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction + * for the new users + */ +export default function Activity() { + const router = useRouter(); + const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); + const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); + const [hasUserStarred, setHasUserStarred] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); + const [usersInOrg, setUsersInOrg] = useState(false); + + useEffect(() => { + onboardingCheck({ + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setHasUserStarred, + setUsersInOrg + }); + }, []); + + return ( +
+ +
+
+

Activity Logs

+
+

+ Manage your integrations of Infisical with third-party services. +

+
+ +
+ ); +} + +Activity.requireAuth = true; From 9218d2a6535a617157742d183be687e07fbb6d1e Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sat, 17 Dec 2022 08:48:10 -0500 Subject: [PATCH 02/91] Fixed the padding issue in the login page --- frontend/pages/login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 63dae65a7..42fb755c9 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -101,7 +101,7 @@ export default function Login() { autoComplete="current-password" id="current-password" /> -
+
Forgot password?
From 2e84b7e3549b4a56255b7fd9945cf72c6a26f10a Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 17 Dec 2022 15:10:30 -0500 Subject: [PATCH 03/91] Initial schema ideas for logging --- backend/src/controllers/index.ts | 4 ++- backend/src/controllers/logController.ts | 30 ++++++++++++++++ backend/src/index.ts | 4 ++- backend/src/models/index.ts | 5 ++- backend/src/models/log.ts | 46 ++++++++++++++++++++++++ backend/src/routes/index.ts | 4 ++- backend/src/routes/log.ts | 17 +++++++++ 7 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 backend/src/controllers/logController.ts create mode 100644 backend/src/models/log.ts create mode 100644 backend/src/routes/log.ts diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index 2d3debfb5..e52d021b1 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -13,6 +13,7 @@ import * as stripeController from './stripeController'; import * as userActionController from './userActionController'; import * as userController from './userController'; import * as workspaceController from './workspaceController'; +import * as logController from './logController'; export { authController, @@ -29,5 +30,6 @@ export { stripeController, userActionController, userController, - workspaceController + workspaceController, + logController }; diff --git a/backend/src/controllers/logController.ts b/backend/src/controllers/logController.ts new file mode 100644 index 000000000..3e1d7d535 --- /dev/null +++ b/backend/src/controllers/logController.ts @@ -0,0 +1,30 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { + Log +} from '../models'; + + +export const getLogs = async (req: Request, res: Response) => { + // get logs + + console.log('getLogs'); + let logs; + try { + const { workspaceId } = req.params; + + logs = await Log.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get audit logs' + }); + } + + return res.status(200).send({ + logs + }); +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 28e27cc9a..fd4f867af 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -38,7 +38,8 @@ import { password as passwordRouter, stripe as stripeRouter, integration as integrationRouter, - integrationAuth as integrationAuthRouter + integrationAuth as integrationAuthRouter, + log as logRouter } from './routes'; const connectWithRetry = () => { @@ -92,6 +93,7 @@ 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/log', logRouter); const server = http.createServer(app); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 9b07f6766..53f5a395a 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -12,6 +12,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; +import Log, { ILog } from './log'; export { BackupPrivateKey, @@ -41,5 +42,7 @@ export { UserAction, IUserAction, Workspace, - IWorkspace + IWorkspace, + Log, + ILog }; diff --git a/backend/src/models/log.ts b/backend/src/models/log.ts new file mode 100644 index 000000000..7078ae52f --- /dev/null +++ b/backend/src/models/log.ts @@ -0,0 +1,46 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ILog { + _id: Types.ObjectId; + user: Types.ObjectId; + workspace: Types.ObjectId; + event: string; + source: string; + ipAddress: string; +} + +// TODO: need a way to store payload info for each +// log + +// which secret is being ref etc. + +const logSchema = new Schema( + { + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + event: { + type: String, + required: true + }, + source: { // should this just be a payload attr? + type: String, + required: true + }, + ipAddress: { // store in bytes? + type: String, + required: true + } + }, { + timestamps: true + } +); + +const Log = model('Log', logSchema); + +export default Log; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index cf015abfb..97dc72c83 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -14,6 +14,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; +import log from './log'; export { signup, @@ -31,5 +32,6 @@ export { password, stripe, integration, - integrationAuth + integrationAuth, + log }; diff --git a/backend/src/routes/log.ts b/backend/src/routes/log.ts new file mode 100644 index 000000000..43d91d7ac --- /dev/null +++ b/backend/src/routes/log.ts @@ -0,0 +1,17 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + validateRequest +} from '../middleware'; +import { logController } from '../controllers'; + +// TODO: workspaceId validation +router.get( + '/:workspaceId', + requireAuth, + validateRequest, + logController.getLogs +); + +export default router; \ No newline at end of file From fae27a0b6e9645a7ff02ef4450b434c04688e531 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sat, 17 Dec 2022 20:21:25 -0500 Subject: [PATCH 04/91] Changed text for the activity page --- frontend/pages/activity/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index 5ffba0328..3dc55799f 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -51,7 +51,7 @@ export default function Activity() {

Activity Logs

- Manage your integrations of Infisical with third-party services. + Event history limited to the last 12 months.

From 9d41f753f4b7f84aad6c5fccf655d07c9786d5b2 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sat, 17 Dec 2022 21:43:13 -0500 Subject: [PATCH 05/91] Added Intercom to Docs --- docs/mint.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/mint.json b/docs/mint.json index 7343622db..2ac025ca6 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -21,7 +21,9 @@ "to": "#F8B7BD" } }, - "topbarLinks": [{ "name": "Log In", "url": "https://app.infisical.com/login" }], + "topbarLinks": [ + { "name": "Log In", "url": "https://app.infisical.com/login" } + ], "topbarCtaButton": { "name": "Start for Free", "url": "https://app.infisical.com/signup" @@ -113,12 +115,10 @@ "pages": ["self-hosting/configuration/envars"] } ] - }, + }, { "group": "Integrations", - "pages": [ - "integrations/overview" - ] + "pages": ["integrations/overview"] }, { "group": "Platforms", @@ -138,9 +138,7 @@ }, { "group": "CI/CD", - "pages": [ - "integrations/cicd/circleci" - ] + "pages": ["integrations/cicd/circleci"] }, { "group": "Frameworks", @@ -179,5 +177,8 @@ ] } ], - "backgroundImage": "/images/background.png" + "backgroundImage": "/images/background.png", + "integrations": { + "intercom": "hsg644ru" + } } From 648e3e3bbf7c1f0e8fc0bd6b7cdddd28837ab082 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 18 Dec 2022 17:19:09 -0500 Subject: [PATCH 06/91] Continue developing log schema --- backend/src/models/log.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/src/models/log.ts b/backend/src/models/log.ts index 7078ae52f..e3716ab84 100644 --- a/backend/src/models/log.ts +++ b/backend/src/models/log.ts @@ -2,11 +2,11 @@ import { Schema, model, Types } from 'mongoose'; export interface ILog { _id: Types.ObjectId; - user: Types.ObjectId; + user?: Types.ObjectId; workspace: Types.ObjectId; event: string; source: string; - ipAddress: string; + ipAddress?: string; } // TODO: need a way to store payload info for each @@ -14,6 +14,8 @@ export interface ILog { // which secret is being ref etc. +// user logged in + const logSchema = new Schema( { user: { @@ -24,17 +26,25 @@ const logSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Workspace' }, - event: { + event: { // push, pull type: String, required: true }, - source: { // should this just be a payload attr? + payload: { // should this just be a payload attr? + numberOfSecrets: { + type: Number + }, + environment: { + type: String + } + }, + channel: { type: String, + enum: ['web', 'cli', 'auto'], required: true }, ipAddress: { // store in bytes? - type: String, - required: true + type: String } }, { timestamps: true From 7d280d4e30e258abe0efbdd9f52c3a6e051bfe78 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 18 Dec 2022 21:53:21 -0500 Subject: [PATCH 07/91] Added event filter for logs --- frontend/components/basic/EventFilter.tsx | 112 ++++++++++++++++++ frontend/components/basic/Layout.tsx | 6 +- frontend/components/basic/Listbox.tsx | 30 ++--- .../components/basic/table/ActivityTable.tsx | 9 +- frontend/pages/activity/[id].tsx | 101 +++++++++++++++- 5 files changed, 233 insertions(+), 25 deletions(-) create mode 100644 frontend/components/basic/EventFilter.tsx diff --git a/frontend/components/basic/EventFilter.tsx b/frontend/components/basic/EventFilter.tsx new file mode 100644 index 000000000..5a9506f0e --- /dev/null +++ b/frontend/components/basic/EventFilter.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { Fragment } from 'react'; +import { + faAngleDown, + faCheck, + faDownload, + faPlus, + faUpload, + faX +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Listbox, Transition } from '@headlessui/react'; + +import guidGenerator from '../utilities/randomId'; +import Button from './buttons/Button'; + +interface ListBoxProps { + selected: string; + select: (event: string) => void; + data: string[]; + text?: string; + buttonAction?: () => void; + isFull?: boolean; +} + +const eventOptions = [ + { + name: 'Secrets Pushed', + icon: faUpload + }, + { + name: 'Secrets Pulled', + icon: faDownload + } +]; + +/** + * This is the component that we use for drop down lists. + * @param {object} obj + * @param {string} obj.selected - the item that is currently selected + * @param {function} obj.select - what happends if you select the item inside a list + * @param {string[]} obj.data - all the options available + * @param {string} obj.text - the text that shows us in front of the select option + * @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button + * @param {string} obj.width - button width + * @returns + */ +export default function EventFilter({ + selected, + select, + data, + text, + buttonAction, + isFull +}: ListBoxProps): JSX.Element { + return ( + +
+ + {selected != '' ? ( +

{selected}

+ ) : ( +

Select an event

+ )} + {selected != '' ? ( + select('')} + /> + ) : ( + + )} +
+ + + {eventOptions.map((event, id) => { + return ( + + {({ selected }) => ( + <> + + {' '} + {event.name} + + + )} + {/* {event.name} */} + + ); + })} + + +
+
+ ); +} diff --git a/frontend/components/basic/Layout.tsx b/frontend/components/basic/Layout.tsx index 91f257ff9..bf3b8bf51 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -4,12 +4,12 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { - faBook, faBookOpen, faGear, faKey, faMobile, faPlug, + faTimeline, faUser } from '@fortawesome/free-solid-svg-icons'; import { faPlus } from '@fortawesome/free-solid-svg-icons'; @@ -153,8 +153,8 @@ export default function Layout({ children }: LayoutProps) { }, { href: '/activity/' + workspaceMapping[workspaceSelected as any], - title: 'Activity', - emoji: + title: 'Activity Logs', + emoji: }, { href: '/settings/project/' + workspaceMapping[workspaceSelected as any], diff --git a/frontend/components/basic/Listbox.tsx b/frontend/components/basic/Listbox.tsx index aa8c41f85..e87bab208 100644 --- a/frontend/components/basic/Listbox.tsx +++ b/frontend/components/basic/Listbox.tsx @@ -1,19 +1,19 @@ -import React from "react"; -import { Fragment } from "react"; +import React from 'react'; +import { Fragment } from 'react'; import { faAngleDown, faCheck, - faPlus, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Listbox, Transition } from "@headlessui/react"; + faPlus +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Listbox, Transition } from '@headlessui/react'; interface ListBoxProps { selected: string; onChange: () => void; data: string[]; - text: string; - buttonAction: () => void; + text?: string; + buttonAction?: () => void; isFull?: boolean; } @@ -34,20 +34,20 @@ export default function ListBox({ data, text, buttonAction, - isFull, + isFull }: ListBoxProps): JSX.Element { return (
{text} - {" "} + {' '} {selected}
@@ -70,11 +70,11 @@ export default function ListBox({ key={personIdx} className={({ active, selected }) => `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${ - selected ? "bg-white/10 text-gray-400 font-bold" : "" + selected ? 'bg-white/10 text-gray-400 font-bold' : '' } ${ active && !selected - ? "bg-white/5 text-mineshaft-200 cursor-pointer" - : "text-gray-400" + ? 'bg-white/5 text-mineshaft-200 cursor-pointer' + : 'text-gray-400' } ` } value={person} @@ -83,7 +83,7 @@ export default function ListBox({ <> {person} diff --git a/frontend/components/basic/table/ActivityTable.tsx b/frontend/components/basic/table/ActivityTable.tsx index fe98277e7..0f5f3eb6b 100644 --- a/frontend/components/basic/table/ActivityTable.tsx +++ b/frontend/components/basic/table/ActivityTable.tsx @@ -49,17 +49,14 @@ const ActivityLogsRow = ({ row }: { row: ActivityTableProps }): JSX.Element => { const [payloadOpened, setPayloadOpened] = useState(false); return ( <> - +
setPayloadOpened(!payloadOpened)} - className="border-mineshaft-700 border-t text-gray-300 flex items-center" + className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" > diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index 3dc55799f..9b51d7f74 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -1,11 +1,96 @@ import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; +import EventFilter from '~/components/basic/EventFilter'; import ActivityTable from '~/components/basic/table/ActivityTable'; import NavHeader from '~/components/navigation/NavHeader'; import onboardingCheck from '~/components/utilities/checks/OnboardingCheck'; const data = [ + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, + { + eventName: 'Secrets Pulled', + user: 'matsiiako@gmail.com', + source: 'CLI', + time: new Date() + }, + { + eventName: 'Secrets Pushed', + user: 'matsiiako@gmail.com', + source: 'Web', + time: new Date() + }, { eventName: 'Secrets Pulled', user: 'matsiiako@gmail.com', @@ -32,6 +117,7 @@ export default function Activity() { const [hasUserStarred, setHasUserStarred] = useState(false); const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); const [usersInOrg, setUsersInOrg] = useState(false); + const [eventChosen, setEventChosen] = useState(''); useEffect(() => { onboardingCheck({ @@ -54,7 +140,20 @@ export default function Activity() { Event history limited to the last 12 months.

- + {/* Licence Required +
+ +
*/} + + eventChosen != '' ? event.eventName == eventChosen : event + )} + />
); } From 009f9c684217a0b8384d22149cb35575677bd6d1 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 21 Dec 2022 16:27:04 -0500 Subject: [PATCH 08/91] Continue developing activity logs backend --- backend/src/events/secret.ts | 27 ++++++++++++-- backend/src/helpers/log.ts | 25 +++++++++++++ backend/src/logs/index.ts | 0 backend/src/logs/secret.ts | 0 backend/src/models/log.ts | 65 +++++++++++++++++++++++++++++----- backend/src/models/logGroup.ts | 25 +++++++++++++ 6 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 backend/src/helpers/log.ts create mode 100644 backend/src/logs/index.ts create mode 100644 backend/src/logs/secret.ts create mode 100644 backend/src/models/logGroup.ts diff --git a/backend/src/events/secret.ts b/backend/src/events/secret.ts index 8bb3a86c3..479255a31 100644 --- a/backend/src/events/secret.ts +++ b/backend/src/events/secret.ts @@ -1,4 +1,7 @@ -import { EVENT_PUSH_SECRETS } from '../variables'; +import { + EVENT_PUSH_SECRETS, + EVENT_PULL_SECRETS +} from '../variables'; interface PushSecret { ciphertextKey: string; @@ -19,7 +22,7 @@ interface PushSecret { * @returns */ const eventPushSecrets = ({ - workspaceId, + workspaceId }: { workspaceId: string; }) => { @@ -32,6 +35,26 @@ const eventPushSecrets = ({ }); } +/** + * Return event for pulling secrets + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace to pull secrets from + * @returns + */ +const eventPullSecrets = ({ + workspaceId, +}: { + workspaceId: string; +}) => { + return ({ + name: EVENT_PULL_SECRETS, + workspaceId, + payload: { + + } + }); +} + export { eventPushSecrets } diff --git a/backend/src/helpers/log.ts b/backend/src/helpers/log.ts new file mode 100644 index 000000000..2a2f1530b --- /dev/null +++ b/backend/src/helpers/log.ts @@ -0,0 +1,25 @@ +import { Log, ILog } from '../models'; +import * as Sentry from '@sentry/node'; +import { + EVENT_PUSH_SECRETS, + EVENT_PULL_SECRETS +} from '../variables'; + + +const handleLogHelper = async ({ + log +}: { + log: ILog +}) => { + try { + switch (log.event) { + case EVENT_PULL_SECRETS: + // TODO + break; + } + + } catch (err){ + Sentry.setUser(null); + Sentry.captureException(err); + } +} \ No newline at end of file diff --git a/backend/src/logs/index.ts b/backend/src/logs/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/logs/secret.ts b/backend/src/logs/secret.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/models/log.ts b/backend/src/models/log.ts index 81dd9fb52..e232d68a7 100644 --- a/backend/src/models/log.ts +++ b/backend/src/models/log.ts @@ -5,6 +5,7 @@ export interface ILog { user?: Types.ObjectId; workspace: Types.ObjectId; event: string; + groupId: string; payload: { numberofSecrets?: number; environment?: string; @@ -13,6 +14,52 @@ export interface ILog { ipAddress?: string; } +// log group consists of logs (each log is associated with 1 event) +// scenario: + +// do we in the future record old and new values for secrets? (when you log update secret, +// do you want to know what the old secret value was changed to?) + +// Option 1: + +// action 1: pushed secrets (top-level event) +// - log 1 (groupId: ABC): modified 10 secrets (sub-level event) +// ---- array of secret ids that were modified +// - log 2 (groupId: ABC): deleted 5 secrets +// ---- array of secret ids that were deleted +// - log 3 (groupId: ABC): created 10 secrets +// ---- array of secret ids that were created + +// action 2: pull secrets +// - log 4 (groupId: DEF): read 20 secrets +// ---- array of secret ids that were read + +// Option 2 (many logs): + +// action 1: pushed secrets (top-level event) +// - log 1 (groupId: ABC): modified secret abc +// - log 2 (groupId: ABC): modified secret def +// - log 3 (groupId: ABC): modified secret ghi +// - log 4 (groupId: ABC): created secret jkl +// - log 5 (groupId: ABC): created secret mno +// - log 6 (groupId: ABC): deleted secret pqr + +// action 2: pull secrets (pulling 100 secrets = 100 logs; 10 times per day, 5 people => 5000 logs) +// - log 7 (groupId: DEF): read secret abc +// - log 8 (groupId: DEF): read secret def +// - log 9 (groupId: DEF): read secret ghi +// - log 10 (groupId: DEF): read secret jkl +// - log 11 (groupId: DEF): read secret mno + +// logGroup +// ---- log (query for log groups by person and by secret etc.) + +/** + * Action: save secrets + * - + * + */ + const logSchema = new Schema( { user: { @@ -23,17 +70,19 @@ const logSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Workspace' }, - event: { // push, pull + event: { // CRUD secrets type: String, required: true }, - payload: { - numberOfSecrets: { - type: Number - }, - environment: { - type: String - } + groupId: { + type: String, + required: true, + }, + payload: { + secrets: [{ + type: Schema.Types.ObjectId, + ref: 'Secret' + }] }, channel: { type: String, diff --git a/backend/src/models/logGroup.ts b/backend/src/models/logGroup.ts new file mode 100644 index 000000000..29527f690 --- /dev/null +++ b/backend/src/models/logGroup.ts @@ -0,0 +1,25 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ILogGroup { + workspace: Types.ObjectId, + logs: [Types.ObjectId] +} + +const logGroupSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + logs: [{ + type: Schema.Types.ObjectId, + ref: 'Log' + }] + }, { + timestamps: true + } +); + +const LogGroup = model('LogGroup', logGroupSchema); + +export default LogGroup; \ No newline at end of file From d869968f8817b7baf88edcc4761dcabd747870f3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 25 Dec 2022 12:45:43 -0500 Subject: [PATCH 09/91] Begin api-key functionality on backend --- backend/package-lock.json | 522 +++++++++++++++++- backend/package.json | 3 + backend/src/app.ts | 5 +- .../src/controllers/serviceTokenController.ts | 9 + backend/src/models/apiKey.ts | 63 +++ backend/src/models/index.ts | 5 +- backend/src/routes/apiKey.ts | 80 +++ backend/src/routes/index.ts | 4 +- backend/src/routes/serviceToken.ts | 2 +- 9 files changed, 658 insertions(+), 35 deletions(-) create mode 100644 backend/src/models/apiKey.ts create mode 100644 backend/src/routes/apiKey.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 85b168c1e..89d371e2a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", + "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -44,6 +45,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", + "@types/bcrypt": "^5.0.0", + "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", @@ -2576,6 +2579,39 @@ "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@maxmind/geoip2-node": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", @@ -3033,6 +3069,21 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3483,8 +3534,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -3586,7 +3636,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3619,6 +3668,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3803,6 +3869,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -4132,6 +4211,14 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", @@ -4218,6 +4305,14 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4262,6 +4357,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -4452,6 +4552,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -4482,6 +4587,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4585,8 +4698,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enabled": { "version": "2.0.0", @@ -4601,6 +4713,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5259,6 +5394,28 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5283,6 +5440,25 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5476,6 +5652,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -5726,7 +5907,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -6696,7 +6876,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -6711,7 +6890,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -6880,11 +7058,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -7004,6 +7215,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -9731,6 +9947,17 @@ "inBundle": true, "license": "ISC" }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10514,6 +10741,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10573,8 +10805,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-swizzle": { "version": "0.2.2", @@ -10795,7 +11026,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -10809,7 +11039,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10935,6 +11164,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11403,6 +11648,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", @@ -13768,6 +14021,32 @@ "resolved": "https://registry.npmjs.org/@juanelas/base64/-/base64-1.0.5.tgz", "integrity": "sha512-gTIElNo4ohMcYUZzol/Hb6DYJzphxl0b1B4egJJ+JiqxqcOcWx4XLMAB+lhWuMsMX3uR1oc5hwPusU3lgc1FkQ==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + } + } + }, "@maxmind/geoip2-node": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@maxmind/geoip2-node/-/geoip2-node-3.5.0.tgz", @@ -14152,6 +14431,21 @@ "@babel/types": "^7.3.0" } }, + "@types/bcrypt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -14513,8 +14807,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.8", @@ -14584,8 +14877,7 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", @@ -14606,6 +14898,20 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -14746,6 +15052,15 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + } + }, "before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -14973,6 +15288,11 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, "ci-info": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", @@ -15064,6 +15384,11 @@ "simple-swizzle": "^0.2.2" } }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -15092,6 +15417,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -15237,6 +15567,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -15257,6 +15592,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -15336,8 +15676,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "enabled": { "version": "2.0.0", @@ -15349,6 +15688,28 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "peer": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -15856,6 +16217,24 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -15873,6 +16252,22 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -16003,6 +16398,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -16181,8 +16581,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -16944,7 +17343,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "requires": { "semver": "^6.0.0" }, @@ -16952,8 +17350,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -17078,11 +17475,37 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "mmdb-lib": { "version": "2.0.2", @@ -17173,6 +17596,11 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -19082,6 +19510,17 @@ "path-key": "^3.0.0" } }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -19637,6 +20076,11 @@ "send": "0.18.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -19684,8 +20128,7 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-swizzle": { "version": "0.2.2", @@ -19860,7 +20303,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -19871,7 +20313,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -19960,6 +20401,19 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -20277,6 +20731,14 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "winston": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", diff --git a/backend/package.json b/backend/package.json index d1a03a74c..174e7add5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,7 @@ "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "axios": "^1.1.3", + "bcrypt": "^5.1.0", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -62,6 +63,8 @@ "devDependencies": { "@jest/globals": "^29.3.1", "@posthog/plugin-scaffold": "^1.3.4", + "@types/bcrypt": "^5.0.0", + "@types/bcryptjs": "^2.4.2", "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", diff --git a/backend/src/app.ts b/backend/src/app.ts index e49551a91..6358293ca 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -27,7 +27,8 @@ import { password as passwordRouter, stripe as stripeRouter, integration as integrationRouter, - integrationAuth as integrationAuthRouter + integrationAuth as integrationAuthRouter, + apiKey as apiKeyRouter } from './routes'; import { getLogger } from './utils/logger'; import { RouteNotFoundError } from './utils/errors'; @@ -74,7 +75,7 @@ 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/api-key', apiKeyRouter); //* 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/serviceTokenController.ts b/backend/src/controllers/serviceTokenController.ts index 4cc53c4f9..43a48b558 100644 --- a/backend/src/controllers/serviceTokenController.ts +++ b/backend/src/controllers/serviceTokenController.ts @@ -74,3 +74,12 @@ export const createServiceToken = async (req: Request, res: Response) => { 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 diff --git a/backend/src/models/apiKey.ts b/backend/src/models/apiKey.ts new file mode 100644 index 000000000..488b24694 --- /dev/null +++ b/backend/src/models/apiKey.ts @@ -0,0 +1,63 @@ +import { Schema, model, Types } from 'mongoose'; +import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; + +// TODO: add scopes + +export interface IAPIKey { + name: string; + workspace: string; + environment: string; + expiresAt: Date; + prefix: string; + apiKeyHash: string; + encryptedKey: string; + iv: string; + tag: string; +} + +const apiKeySchema = new Schema( + { + name: { + type: String, + required: true + }, + workspace: { + type: String + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] + }, + expiresAt: { + type: Date + }, + prefix: { + type: String, + required: true + }, + apiKeyHash: { + type: String, + unique: true, + required: true + }, + encryptedKey: { + type: String, + select: true + }, + iv: { + type: String, + select: true + }, + tag: { + type: String, + select: true + } + }, + { + timestamps: true + } +); + +const APIKey = model('APIKey', apiKeySchema); + +export default APIKey; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 78c38060b..0c40f155d 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,6 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; +import APIKey, { IAPIKey } from './apiKey'; export { BackupPrivateKey, @@ -47,5 +48,7 @@ export { UserAction, IUserAction, Workspace, - IWorkspace + IWorkspace, + APIKey, + IAPIKey, }; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts new file mode 100644 index 000000000..c5fc7dd5d --- /dev/null +++ b/backend/src/routes/apiKey.ts @@ -0,0 +1,80 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth +} from '../middleware'; +import { + APIKey +} from '../models'; +import { body } from 'express-validator'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +// import * as bcrypt from 'bcrypt'; +// const bcrypt = require('bcrypt'); +import * as Sentry from '@sentry/node'; + +// POST /api/v1/api-key +router.post( + '/', + requireAuth, + body('name').exists().trim(), + body('workspace'), + body('environment'), + body('encryptedKey'), + body('iv'), + body('tag'), + body('expiresAt'), + async (req, res) => { + let savedAPIKey; + try { + const { + name, + workspace, + environment, + encryptedKey, + iv, + tag, + expiresAt + } = req.body; + + // api-key: 38 characters + // 6-char: prefix + // 32-char: remaining + const apiKey = crypto.randomBytes(19).toString('hex'); + const saltRounds = 10; // config? + const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); + + savedAPIKey = await new APIKey({ + name, + workspace, + environment, + expiresAt, + prefix: apiKey.substring(0, 6), + apiKeyHash, + encryptedKey, + iv, + tag + }).save(); + + // 1. generate api key + // 2. hash api key with bcrypt + // 3. store hash and api key info in db + // 4. return api key + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'xxx' + }); + } + + return res.status(200).send({ + apiKey: savedAPIKey + }); + } +); + +// INFISICAL TOKEN = . + +export default router; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 2dfe58baa..bc0ff776d 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -15,6 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; +import apiKey from './apiKey'; export { signup, @@ -33,5 +34,6 @@ export { password, stripe, integration, - integrationAuth + integrationAuth, + apiKey }; diff --git a/backend/src/routes/serviceToken.ts b/backend/src/routes/serviceToken.ts index 00195edee..f9452db63 100644 --- a/backend/src/routes/serviceToken.ts +++ b/backend/src/routes/serviceToken.ts @@ -10,7 +10,7 @@ import { body } from 'express-validator'; import { ADMIN, MEMBER, GRANTED } from '../variables'; import { serviceTokenController } from '../controllers'; -// TODO: revoke service token +// Note to devs: service-token to be deprecated in favor of api-key router.get( '/', From 888d28d6b9dcdb184b406826e980e00c04d27b28 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 25 Dec 2022 19:19:56 -0500 Subject: [PATCH 10/91] Continue work on API key --- backend/Dockerfile | 5 +- backend/package.json | 1 - .../src/middleware/requireAPIKeyDataAuth.ts | 40 +++++++++ .../src/models/{apiKey.ts => apiKeyData.ts} | 33 +++---- backend/src/models/index.ts | 6 +- backend/src/routes/apiKey.ts | 85 ++++++++++++++----- 6 files changed, 131 insertions(+), 39 deletions(-) create mode 100644 backend/src/middleware/requireAPIKeyDataAuth.ts rename backend/src/models/{apiKey.ts => apiKeyData.ts} (61%) diff --git a/backend/Dockerfile b/backend/Dockerfile index 85b7204fe..e4d283a77 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,7 +4,10 @@ WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --only-production --ignore-scripts +# RUN npm ci --only-production --ignore-scripts +# "prepare": "cd .. && npm install" + +RUN npm ci --only-production COPY . . diff --git a/backend/package.json b/backend/package.json index 174e7add5..0bcb36e49 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,6 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { - "prepare": "cd .. && npm install", "start": "npm run build && node build/index.js", "dev": "nodemon", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", diff --git a/backend/src/middleware/requireAPIKeyDataAuth.ts b/backend/src/middleware/requireAPIKeyDataAuth.ts new file mode 100644 index 000000000..8dafb5a9c --- /dev/null +++ b/backend/src/middleware/requireAPIKeyDataAuth.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from 'express'; +import { APIKeyData } from '../models'; +import { validateMembership } from '../helpers/membership'; +import { AccountNotFoundError } from '../utils/errors'; + +type req = 'params' | 'body' | 'query'; + +const requireAPIKeyDataAuth = ({ + acceptedRoles, + acceptedStatuses, + location = 'params' +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; + location?: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + + // req.user + + const apiKeyData = await APIKeyData.findById(req[location].apiKeyDataId); + + if (!apiKeyData) { + return next(AccountNotFoundError({message: 'Failed to locate API Key data'})); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: apiKeyData?.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.apiKeyData = '' // ?? + + next(); + } +} + +export default requireAPIKeyDataAuth; \ No newline at end of file diff --git a/backend/src/models/apiKey.ts b/backend/src/models/apiKeyData.ts similarity index 61% rename from backend/src/models/apiKey.ts rename to backend/src/models/apiKeyData.ts index 488b24694..8b064bf08 100644 --- a/backend/src/models/apiKey.ts +++ b/backend/src/models/apiKeyData.ts @@ -1,12 +1,12 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; -// TODO: add scopes - -export interface IAPIKey { +export interface IAPIKeyData { name: string; - workspace: string; - environment: string; + workspaces: { + workspace: Types.ObjectId, + environments: string[] + }[]; expiresAt: Date; prefix: string; apiKeyHash: string; @@ -15,19 +15,22 @@ export interface IAPIKey { tag: string; } -const apiKeySchema = new Schema( +const apiKeyDataSchema = new Schema( { name: { type: String, required: true }, - workspace: { - type: String - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] - }, + workspaces: [{ + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + environments: [{ + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] + }] + }], expiresAt: { type: Date }, @@ -58,6 +61,6 @@ const apiKeySchema = new Schema( } ); -const APIKey = model('APIKey', apiKeySchema); +const APIKeyData = model('APIKeyData', apiKeyDataSchema); -export default APIKey; +export default APIKeyData; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 0c40f155d..fd9578523 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import APIKey, { IAPIKey } from './apiKey'; +import APIKeyData, { IAPIKeyData } from './apiKeyData'; export { BackupPrivateKey, @@ -49,6 +49,6 @@ export { IUserAction, Workspace, IWorkspace, - APIKey, - IAPIKey, + APIKeyData, + IAPIKeyData, }; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts index c5fc7dd5d..a40c852ac 100644 --- a/backend/src/routes/apiKey.ts +++ b/backend/src/routes/apiKey.ts @@ -4,16 +4,14 @@ import { requireAuth } from '../middleware'; import { - APIKey + APIKeyData } from '../models'; -import { body } from 'express-validator'; +import { param, body, query } from 'express-validator'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; -// import * as bcrypt from 'bcrypt'; -// const bcrypt = require('bcrypt'); import * as Sentry from '@sentry/node'; -// POST /api/v1/api-key +// TODO: middleware router.post( '/', requireAuth, @@ -25,7 +23,7 @@ router.post( body('tag'), body('expiresAt'), async (req, res) => { - let savedAPIKey; + let apiKey, apiKeyData; try { const { name, @@ -37,14 +35,13 @@ router.post( expiresAt } = req.body; - // api-key: 38 characters - // 6-char: prefix - // 32-char: remaining - const apiKey = crypto.randomBytes(19).toString('hex'); - const saltRounds = 10; // config? + // create 38-char API key with first 6-char being the prefix + apiKey = crypto.randomBytes(19).toString('hex'); + + const saltRounds = 10; // TODO: add as config envar const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); - savedAPIKey = await new APIKey({ + apiKeyData = await new APIKeyData({ name, workspace, environment, @@ -55,22 +52,72 @@ router.post( iv, tag }).save(); - - // 1. generate api key - // 2. hash api key with bcrypt - // 3. store hash and api key info in db - // 4. return api key } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ - message: 'xxx' + message: 'Failed to create workspace API Key' }); } return res.status(200).send({ - apiKey: savedAPIKey + apiKey, + apiKeyData + }); + } +); + +// TODO: middleware +router.get( + '/', + requireAuth, + query('workspaceId').exists().trim(), + async (req, res) => { + let apiKeyData; + try { + const { workspaceId } = req.query; + + apiKeyData = await APIKeyData.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace API Key data' + }); + } + + return res.status(200).send({ + apiKeyData + }); + } +); + +// TODO: middleware +router.delete( + ':apiKeyDataId', + requireAuth, + // TODO: requireAPIKeyDataAuth, + param('apiKeyDataId').exists().trim(), + async (req, res) => { + let apiKeyData; + try { + const { apiKeyDataId } = req.params; + + apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete API key data' + }); + } + + return res.status(200).send({ + apiKeyData }); } ); From 9497a26eb2064b453da60f96870f9bb0aac0f5c5 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 27 Dec 2022 12:12:39 -0500 Subject: [PATCH 11/91] Add v1 audit log backend models and wiring to push secrets --- backend/src/app.ts | 4 +- backend/src/controllers/logController.ts | 30 -- backend/src/controllers/v1/index.ts | 4 +- .../src/controllers/v2/workspaceController.ts | 4 +- .../ee/controllers/v1/workspaceController.ts | 34 ++- backend/src/ee/helpers/log.ts | 40 +++ backend/src/ee/models/action.ts | 40 +++ backend/src/ee/models/index.ts | 12 +- backend/src/ee/models/log.ts | 41 +++ backend/src/ee/routes/v1/index.ts | 4 +- backend/src/ee/routes/v1/log.ts | 4 + backend/src/ee/routes/v1/workspace.ts | 14 + backend/src/ee/services/EELogService.ts | 47 +++ backend/src/ee/services/index.ts | 4 +- backend/src/ee/variables.ts | 0 backend/src/helpers/secret.ts | 278 +++++++++++++----- backend/src/logs/index.ts | 0 backend/src/logs/secret.ts | 0 backend/src/models/index.ts | 5 +- backend/src/models/log.ts | 102 ------- backend/src/models/logGroup.ts | 25 -- backend/src/routes/log.ts | 17 -- backend/src/routes/v1/index.ts | 4 +- backend/src/routes/v1/userAction.ts | 1 + backend/src/variables/action.ts | 9 + backend/src/variables/index.ts | 8 + 26 files changed, 459 insertions(+), 272 deletions(-) delete mode 100644 backend/src/controllers/logController.ts create mode 100644 backend/src/ee/helpers/log.ts create mode 100644 backend/src/ee/models/action.ts create mode 100644 backend/src/ee/models/log.ts create mode 100644 backend/src/ee/routes/v1/log.ts create mode 100644 backend/src/ee/services/EELogService.ts delete mode 100644 backend/src/ee/variables.ts delete mode 100644 backend/src/logs/index.ts delete mode 100644 backend/src/logs/secret.ts delete mode 100644 backend/src/models/log.ts delete mode 100644 backend/src/models/logGroup.ts delete mode 100644 backend/src/routes/log.ts create mode 100644 backend/src/variables/action.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 8320a7d76..4e8e23fc0 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -13,7 +13,8 @@ import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, - secret as eeSecretRouter + secret as eeSecretRouter, + log as eeLogRouter, } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -69,6 +70,7 @@ if (NODE_ENV === 'production') { // (EE) routes app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); +app.use('/api/v1/log', eeLogRouter); // v1 routes app.use('/api/v1/signup', v1SignupRouter); diff --git a/backend/src/controllers/logController.ts b/backend/src/controllers/logController.ts deleted file mode 100644 index 3e1d7d535..000000000 --- a/backend/src/controllers/logController.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; -import { - Log -} from '../models'; - - -export const getLogs = async (req: Request, res: Response) => { - // get logs - - console.log('getLogs'); - let logs; - try { - const { workspaceId } = req.params; - - logs = await Log.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get audit logs' - }); - } - - return res.status(200).send({ - logs - }); -} \ No newline at end of file diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index a8756b4f3..1da61835f 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -14,7 +14,6 @@ import * as stripeController from './stripeController'; import * as userActionController from './userActionController'; import * as userController from './userController'; import * as workspaceController from './workspaceController'; -import * as logController from './logController'; export { authController, @@ -32,6 +31,5 @@ export { stripeController, userActionController, userController, - workspaceController, - logController + workspaceController }; diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 86693b6c4..b9aa6406f 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -389,7 +389,9 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { userId: req.user._id, workspaceId, environment, - secrets + secrets, + channel: channel ? channel : 'cli', + ipAddress: req.ip }); await pushKeys({ diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 8b7ba422e..1c9a71bc8 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,6 +1,9 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { SecretSnapshot } from '../../models'; +import { + SecretSnapshot, + Log +} from '../../models'; /** * Return secret snapshots for workspace with id [workspaceId] @@ -32,4 +35,33 @@ import { SecretSnapshot } from '../../models'; return res.status(200).send({ secretSnapshots }); +} + +export const getWorkspaceLogs = async (req: Request, res: Response) => { + let logs + try { + const { workspaceId } = req.params; + + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + const filters: any = req.query.filters || {}; + + filters.workspace = workspaceId; + + logs = await Log.find(filters) + .skip(offset) + .limit(limit) + .populate('actions'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace logs' + }); + } + + return res.status(200).send({ + logs + }); } \ No newline at end of file diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts new file mode 100644 index 000000000..a075cc91f --- /dev/null +++ b/backend/src/ee/helpers/log.ts @@ -0,0 +1,40 @@ +import * as Sentry from '@sentry/node'; +import { + Log, + IAction +} from '../models'; + +const createLogHelper = async ({ + userId, + workspaceId, + actions, + channel, + ipAddress +}: { + userId: string; + workspaceId: string; + actions: IAction[]; + channel: string; + ipAddress: string; +}) => { + let log; + try { + log = await new Log({ + user: userId, + workspace: workspaceId, + actions, + channel, + ipAddress + }).save(); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to create log'); + } + + return log; +} + +export { + createLogHelper +} \ No newline at end of file diff --git a/backend/src/ee/models/action.ts b/backend/src/ee/models/action.ts new file mode 100644 index 000000000..8ffdfe4bb --- /dev/null +++ b/backend/src/ee/models/action.ts @@ -0,0 +1,40 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface IAction { + name: string; + user?: Types.ObjectId, + workspace?: Types.ObjectId, + payload: { + secretVersions?: Types.ObjectId[] + } +} + +const actionSchema = new Schema( + { + name: { + type: String, + required: true + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + payload: { + secretVersions: [{ + type: Schema.Types.ObjectId, + ref: 'SecretVersion' + }] + } + }, { + timestamps: true + } +); + +const Action = model('Action', actionSchema); + +export default Action; \ No newline at end of file diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts index 35d41c19a..a6cee725e 100644 --- a/backend/src/ee/models/index.ts +++ b/backend/src/ee/models/index.ts @@ -1,9 +1,15 @@ -import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; -import SecretVersion, { ISecretVersion } from "./secretVersion"; +import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot'; +import SecretVersion, { ISecretVersion } from './secretVersion'; +import Log, { ILog } from './log'; +import Action, { IAction } from './action'; export { SecretSnapshot, ISecretSnapshot, SecretVersion, - ISecretVersion + ISecretVersion, + Log, + ILog, + Action, + IAction } \ No newline at end of file diff --git a/backend/src/ee/models/log.ts b/backend/src/ee/models/log.ts new file mode 100644 index 000000000..abfadb223 --- /dev/null +++ b/backend/src/ee/models/log.ts @@ -0,0 +1,41 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ILog { + _id: Types.ObjectId; + user?: Types.ObjectId; + workspace?: Types.ObjectId; + actions: Types.ObjectId[]; + channel: string; + ipAddress?: string; +} + +const logSchema = new Schema( + { + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + actions: [{ + type: Schema.Types.ObjectId, + ref: 'Action' + }], + channel: { + type: String, + enum: ['web', 'cli', 'auto'], + required: true + }, + ipAddress: { + type: String + } + }, { + timestamps: true + } +); + +const Log = model('Log', logSchema); + +export default Log; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 960665f4a..810a050a6 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,7 +1,9 @@ import secret from './secret'; import workspace from './workspace'; +import log from './log'; export { secret, - workspace + workspace, + log } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/log.ts b/backend/src/ee/routes/v1/log.ts new file mode 100644 index 000000000..d90cb867c --- /dev/null +++ b/backend/src/ee/routes/v1/log.ts @@ -0,0 +1,4 @@ +import express from 'express'; +const router = express.Router(); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 6756269e5..5deb7dd85 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -23,5 +23,19 @@ router.get( workspaceController.getWorkspaceSecretSnapshots ); +router.get( + '/:workspaceId/logs', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + query('offset').exists().isInt(), + query('limit').exists().isInt(), + query('filters').exists(), + validateRequest, + workspaceController.getWorkspaceLogs +); export default router; \ No newline at end of file diff --git a/backend/src/ee/services/EELogService.ts b/backend/src/ee/services/EELogService.ts new file mode 100644 index 000000000..7e6ca4acf --- /dev/null +++ b/backend/src/ee/services/EELogService.ts @@ -0,0 +1,47 @@ +import { + Action, + IAction +} from '../models'; +import { + createLogHelper +} from '../helpers/log'; +import EELicenseService from './EELicenseService'; + +/** + * Class to handle Enterprise Edition log actions + */ +class EELogService { + /** + * Create an (audit) log + * @param {Object} obj + * @param {String} obj.userId - id of user associated with the log + * @param {String} obj.workspaceId - id of workspace associated with the log + * @param {Action} obj.actions - actions to include in log + * @param {String} obj.channel - channel (web/cli/auto) associated with the log + * @param {String} obj.ipAddress - ip address associated with the log + */ + static async createLog({ + userId, + workspaceId, + actions, + channel, + ipAddress + }: { + userId: string; + workspaceId: string; + actions: IAction[]; + channel: string; + ipAddress: string; + }) { + if (!EELicenseService.isLicenseValid) return; + return await createLogHelper({ + userId, + workspaceId, + actions, + channel, + ipAddress + }) + } +} + +export default EELogService; \ No newline at end of file diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts index 3cec256bb..b3544bcff 100644 --- a/backend/src/ee/services/index.ts +++ b/backend/src/ee/services/index.ts @@ -1,7 +1,9 @@ import EELicenseService from "./EELicenseService"; import EESecretService from "./EESecretService"; +import EELogService from "./EELogService"; export { EELicenseService, - EESecretService + EESecretService, + EELogService } \ No newline at end of file diff --git a/backend/src/ee/variables.ts b/backend/src/ee/variables.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index f055971ae..79d99960f 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,19 +1,29 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Secret, ISecret, } from '../models'; import { - EESecretService + EESecretService, + EELogService } from '../ee/services'; import { - SecretVersion + SecretVersion, + Action, + IAction } from '../ee/models'; import { takeSecretSnapshotHelper } from '../ee/helpers/secret'; import { decryptSymmetric } from '../utils/crypto'; -import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ACTION_ADD_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS +} from '../variables'; interface V1PushSecret { ciphertextKey: string; @@ -284,20 +294,28 @@ const v1PushSecrets = async ({ * @param {String} obj.workspaceId - id of workspace to push to * @param {String} obj.environment - environment for secrets * @param {Object[]} obj.secrets - secrets to push + * @param {String} obj.channel - channel (web/cli/auto) + * @param {String} obj.ipAddress - ip address of request to push secrets */ const v2PushSecrets = async ({ userId, workspaceId, environment, - secrets + secrets, + channel, + ipAddress }: { userId: string; workspaceId: string; environment: string; secrets: V2PushSecret[]; + channel: string; + ipAddress: string; }): Promise => { // TODO: clean up function and fix up types try { + const actions: IAction[] = []; + // construct useful data structures const oldSecrets = await pullSecrets({ userId, @@ -327,7 +345,37 @@ const v1PushSecrets = async ({ secret: { $in: toDelete } }, { isDeleted: true + }, { + new: true }); + + // add audit log for deleted secrets + const deletedLatestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { secret: { $in: toDelete } } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' } + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => s._id); + + const deleteAction = await new Action({ + name: ACTION_DELETE_SECRETS, + user: new Types.ObjectId(userId), + workspace: new Types.ObjectId(workspaceId), + payload: { + secretVersions: deletedLatestSecretVersions + } + }).save(); + actions.push(deleteAction); } const toUpdate = oldSecrets @@ -348,88 +396,119 @@ const v1PushSecrets = async ({ return false; }); - const operations = toUpdate - .map((s) => { - const { - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentHash, - } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + if (toUpdate.length > 0) { + 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 + const update: Update = { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, } - } - 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 + 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 + }) + }) }); - 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 - }) - }) - }); + // add audit log for updated secrets + const updatedLatestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { secret: { $in: toUpdate.map((u) => u._id) } } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' } + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => s._id); + + const updateAction = await new Action({ + name: ACTION_UPDATE_SECRETS, + user: new Types.ObjectId(userId), + workspace: new Types.ObjectId(workspaceId), + payload: { + secretVersions: updatedLatestSecretVersions + } + }).save(); + + actions.push(updateAction); + } // handle adding new secrets const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj)); @@ -504,12 +583,51 @@ const v1PushSecrets = async ({ secretValueHash })) }); + + // add audit log for new secrets + const newLatestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { secret: { $in: newSecrets.map((n) => n._id) } } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' } + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => s._id); + + const addAction = await new Action({ + name: ACTION_ADD_SECRETS, + user: new Types.ObjectId(userId), + workspace: new Types.ObjectId(workspaceId), + payload: { + secretVersions: newLatestSecretVersions + } + }).save(); + + actions.push(addAction); } // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId }) + + if (actions.length > 0) { + await EELogService.createLog({ + userId, + workspaceId, + actions, + channel, + ipAddress + }); + } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/logs/index.ts b/backend/src/logs/index.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/logs/secret.ts b/backend/src/logs/secret.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 8d125499c..78c38060b 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,6 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import Log, { ILog } from './log'; export { BackupPrivateKey, @@ -48,7 +47,5 @@ export { UserAction, IUserAction, Workspace, - IWorkspace, - Log, - ILog + IWorkspace }; diff --git a/backend/src/models/log.ts b/backend/src/models/log.ts deleted file mode 100644 index e232d68a7..000000000 --- a/backend/src/models/log.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Schema, model, Types } from 'mongoose'; - -export interface ILog { - _id: Types.ObjectId; - user?: Types.ObjectId; - workspace: Types.ObjectId; - event: string; - groupId: string; - payload: { - numberofSecrets?: number; - environment?: string; - }, - channel: string; - ipAddress?: string; -} - -// log group consists of logs (each log is associated with 1 event) -// scenario: - -// do we in the future record old and new values for secrets? (when you log update secret, -// do you want to know what the old secret value was changed to?) - -// Option 1: - -// action 1: pushed secrets (top-level event) -// - log 1 (groupId: ABC): modified 10 secrets (sub-level event) -// ---- array of secret ids that were modified -// - log 2 (groupId: ABC): deleted 5 secrets -// ---- array of secret ids that were deleted -// - log 3 (groupId: ABC): created 10 secrets -// ---- array of secret ids that were created - -// action 2: pull secrets -// - log 4 (groupId: DEF): read 20 secrets -// ---- array of secret ids that were read - -// Option 2 (many logs): - -// action 1: pushed secrets (top-level event) -// - log 1 (groupId: ABC): modified secret abc -// - log 2 (groupId: ABC): modified secret def -// - log 3 (groupId: ABC): modified secret ghi -// - log 4 (groupId: ABC): created secret jkl -// - log 5 (groupId: ABC): created secret mno -// - log 6 (groupId: ABC): deleted secret pqr - -// action 2: pull secrets (pulling 100 secrets = 100 logs; 10 times per day, 5 people => 5000 logs) -// - log 7 (groupId: DEF): read secret abc -// - log 8 (groupId: DEF): read secret def -// - log 9 (groupId: DEF): read secret ghi -// - log 10 (groupId: DEF): read secret jkl -// - log 11 (groupId: DEF): read secret mno - -// logGroup -// ---- log (query for log groups by person and by secret etc.) - -/** - * Action: save secrets - * - - * - */ - -const logSchema = new Schema( - { - user: { - type: Schema.Types.ObjectId, - ref: 'User' - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace' - }, - event: { // CRUD secrets - type: String, - required: true - }, - groupId: { - type: String, - required: true, - }, - payload: { - secrets: [{ - type: Schema.Types.ObjectId, - ref: 'Secret' - }] - }, - channel: { - type: String, - enum: ['web', 'cli', 'auto'], - required: true - }, - ipAddress: { // store in bytes? - type: String - } - }, { - timestamps: true - } -); - -const Log = model('Log', logSchema); - -export default Log; \ No newline at end of file diff --git a/backend/src/models/logGroup.ts b/backend/src/models/logGroup.ts deleted file mode 100644 index 29527f690..000000000 --- a/backend/src/models/logGroup.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Schema, model, Types } from 'mongoose'; - -export interface ILogGroup { - workspace: Types.ObjectId, - logs: [Types.ObjectId] -} - -const logGroupSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace' - }, - logs: [{ - type: Schema.Types.ObjectId, - ref: 'Log' - }] - }, { - timestamps: true - } -); - -const LogGroup = model('LogGroup', logGroupSchema); - -export default LogGroup; \ No newline at end of file diff --git a/backend/src/routes/log.ts b/backend/src/routes/log.ts deleted file mode 100644 index 43d91d7ac..000000000 --- a/backend/src/routes/log.ts +++ /dev/null @@ -1,17 +0,0 @@ -import express from 'express'; -const router = express.Router(); -import { - requireAuth, - validateRequest -} from '../middleware'; -import { logController } from '../controllers'; - -// TODO: workspaceId validation -router.get( - '/:workspaceId', - requireAuth, - validateRequest, - logController.getLogs -); - -export default router; \ No newline at end of file diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 597aa2b34..2dfe58baa 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -15,7 +15,6 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import log from './log'; export { signup, @@ -34,6 +33,5 @@ export { password, stripe, integration, - integrationAuth, - log + integrationAuth }; diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index 544b948e8..bbd488e06 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -4,6 +4,7 @@ import { requireAuth, validateRequest } from '../../middleware'; import { body, query } from 'express-validator'; import { userActionController } from '../../controllers/v1'; +// note: [userAction] will be deprecated in /v2 in favor of [action] router.post( '/', requireAuth, diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts new file mode 100644 index 000000000..c8b0130d2 --- /dev/null +++ b/backend/src/variables/action.ts @@ -0,0 +1,9 @@ +const ACTION_ADD_SECRETS = 'addSecrets'; +const ACTION_DELETE_SECRETS = 'deleteSecrets'; +const ACTION_UPDATE_SECRETS = 'updateSecrets'; + +export { + ACTION_ADD_SECRETS, + ACTION_DELETE_SECRETS, + ACTION_UPDATE_SECRETS +} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index e284d6d5c..1d9241e2b 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -33,6 +33,11 @@ import { } from './organization'; import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; +import { + ACTION_ADD_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS +} from './action'; import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; import { PLAN_STARTER, PLAN_PRO } from './stripe'; @@ -67,6 +72,9 @@ export { INTEGRATION_GITHUB_API_URL, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, + ACTION_ADD_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS, INTEGRATION_OPTIONS, SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN, From 16f240596a591203a2f3beb347f0e52dd9b34761 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 27 Dec 2022 12:30:33 -0500 Subject: [PATCH 12/91] Add audit logs to pulls, still need to refactor --- .../src/controllers/v1/secretController.ts | 8 +- .../src/controllers/v2/workspaceController.ts | 8 +- backend/src/helpers/secret.ts | 89 +++++++++++++++++-- backend/src/variables/action.ts | 4 +- backend/src/variables/index.ts | 4 +- 5 files changed, 100 insertions(+), 13 deletions(-) diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 238b38ced..1b756ecc7 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -123,7 +123,9 @@ export const pullSecrets = async (req: Request, res: Response) => { secrets = await pull({ userId: req.user._id.toString(), workspaceId, - environment + environment, + channel: channel ? channel : 'cli', + ipAddress: req.ip }); key = await Key.findOne({ @@ -188,7 +190,9 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { secrets = await pull({ userId: req.serviceToken.user._id.toString(), workspaceId, - environment + environment, + channel: 'cli', + ipAddress: req.ip }); key = { diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index b9aa6406f..cfe1f239e 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -459,7 +459,9 @@ export const pullSecrets = async (req: Request, res: Response) => { secrets = await pull({ userId: req.user._id.toString(), workspaceId, - environment + environment, + channel, + ipAddress: req.ip }); key = await Key.findOne({ @@ -526,7 +528,9 @@ export const pullSecrets = async (req: Request, res: Response) => { secrets = await pull({ userId: req.serviceToken.user._id.toString(), workspaceId, - environment + environment, + channel: 'cli', + ipAddress: req.ip }); key = { diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 79d99960f..93cbb648a 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -22,7 +22,8 @@ import { SECRET_PERSONAL, ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS + ACTION_DELETE_SECRETS, + ACTION_READ_SECRETS } from '../variables'; interface V1PushSecret { @@ -78,7 +79,7 @@ const v1PushSecrets = async ({ userId, workspaceId, environment, - secrets + secrets, }: { userId: string; workspaceId: string; @@ -88,7 +89,7 @@ const v1PushSecrets = async ({ // TODO: clean up function and fix up types try { // construct useful data structures - const oldSecrets = await pullSecrets({ + const oldSecrets = await getSecrets({ userId, workspaceId, environment @@ -317,7 +318,7 @@ const v1PushSecrets = async ({ const actions: IAction[] = []; // construct useful data structures - const oldSecrets = await pullSecrets({ + const oldSecrets = await getSecrets({ userId, workspaceId, environment @@ -642,9 +643,8 @@ const v1PushSecrets = async ({ * @param {String} obj.userId -id of user to pull secrets for * @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.environment - environment for secrets - * */ -const pullSecrets = async ({ + const getSecrets = async ({ userId, workspaceId, environment @@ -681,9 +681,84 @@ const pullSecrets = async ({ return secrets; }; +/** + * Pull secrets for user with id [userId] for workspace + * with id [workspaceId] with environment [environment] + * @param {Object} obj + * @param {String} obj.userId -id of user to pull secrets for + * @param {String} obj.workspaceId - id of workspace to pull from + * @param {String} obj.environment - environment for secrets + * @param {String} obj.channel - channel (web/cli/auto) + * @param {String} obj.ipAddress - ip address of request to push secrets + */ +const pullSecrets = async ({ + userId, + workspaceId, + environment, + channel, + ipAddress +}: { + userId: string; + workspaceId: string; + environment: string; + channel: string; + ipAddress: string; +}): Promise => { + let secrets: any; // TODO: FIX any + + try { + secrets = await getSecrets({ + userId, + workspaceId, + environment + }) + + // add audit log for new secrets + const readLatestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { secret: { $in: secrets.map((n: any) => n._id) } } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' } + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => s._id); + + const readAction = await new Action({ + name: ACTION_READ_SECRETS, + user: new Types.ObjectId(userId), + workspace: new Types.ObjectId(workspaceId), + payload: { + secretVersions: readLatestSecretVersions + } + }).save(); + + await EELogService.createLog({ + userId, + workspaceId, + actions: [readAction], + channel, + ipAddress + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to pull shared and personal secrets'); + } + + return secrets; +}; + /** * Reformat output of pullSecrets() to be compatible with how existing - * clients handle secrets + * web client handle secrets * @param {Object} obj * @param {Object} obj.secrets */ diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts index c8b0130d2..512eb8e8d 100644 --- a/backend/src/variables/action.ts +++ b/backend/src/variables/action.ts @@ -1,9 +1,11 @@ const ACTION_ADD_SECRETS = 'addSecrets'; const ACTION_DELETE_SECRETS = 'deleteSecrets'; const ACTION_UPDATE_SECRETS = 'updateSecrets'; +const ACTION_READ_SECRETS = 'readSecrets'; export { ACTION_ADD_SECRETS, ACTION_DELETE_SECRETS, - ACTION_UPDATE_SECRETS + ACTION_UPDATE_SECRETS, + ACTION_READ_SECRETS } \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 1d9241e2b..e8a373e74 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -36,7 +36,8 @@ import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; import { ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS + ACTION_DELETE_SECRETS, + ACTION_READ_SECRETS } from './action'; import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; import { PLAN_STARTER, PLAN_PRO } from './stripe'; @@ -75,6 +76,7 @@ export { ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, + ACTION_READ_SECRETS, INTEGRATION_OPTIONS, SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN, From 01d969190bc24f84d2106c1db9d9bc3b22dd2dbd Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 30 Dec 2022 23:57:21 +0300 Subject: [PATCH 13/91] Begin service token data refactor --- backend/src/app.ts | 4 +- backend/src/config/index.ts | 2 + .../src/controllers/workspaceController.ts | 28 ++++ backend/src/middleware/index.ts | 2 + .../src/middleware/requireServiceTokenAuth.ts | 1 + ...Auth.ts => requireServiceTokenDataAuth.ts} | 20 +-- backend/src/models/index.ts | 6 +- backend/src/models/serviceToken.ts | 1 + .../{apiKeyData.ts => serviceTokenData .ts} | 37 +++-- backend/src/routes/apiKey.ts | 127 --------------- backend/src/routes/index.ts | 4 +- backend/src/routes/serviceTokenData.ts | 144 ++++++++++++++++++ backend/src/routes/workspace.ts | 14 +- backend/src/types/express/index.d.ts | 1 + 14 files changed, 226 insertions(+), 165 deletions(-) rename backend/src/middleware/{requireAPIKeyDataAuth.ts => requireServiceTokenDataAuth.ts} (60%) rename backend/src/models/{apiKeyData.ts => serviceTokenData .ts} (57%) delete mode 100644 backend/src/routes/apiKey.ts create mode 100644 backend/src/routes/serviceTokenData.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 500aafe77..7a263acee 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -34,7 +34,7 @@ import { stripe as stripeRouter, integration as integrationRouter, integrationAuth as integrationAuthRouter, - apiKey as apiKeyRouter + serviceTokenData as serviceTokenDataRouter } from './routes'; import { getLogger } from './utils/logger'; @@ -86,7 +86,7 @@ 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/api-key', apiKeyRouter); +app.use('/api/v1/service-token-data', serviceTokenDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next)=>{ diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3fb475099..652b3c7b2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,6 +1,7 @@ const PORT = process.env.PORT || 4000; const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400'; const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; +const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10; const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d'; const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!; const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d'; @@ -47,6 +48,7 @@ export { PORT, EMAIL_TOKEN_LIFETIME, ENCRYPTION_KEY, + SALT_ROUNDS, JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, diff --git a/backend/src/controllers/workspaceController.ts b/backend/src/controllers/workspaceController.ts index 6f3e4bd11..a843ed9d6 100644 --- a/backend/src/controllers/workspaceController.ts +++ b/backend/src/controllers/workspaceController.ts @@ -8,6 +8,7 @@ import { IntegrationAuth, IUser, ServiceToken, + ServiceTokenData, } from '../models'; import { createWorkspace as create, @@ -334,4 +335,31 @@ export const getWorkspaceServiceTokens = async ( return res.status(200).send({ serviceTokens }); +} + +export const getWorkspaceServiceTokenData = async ( + req: Request, + res: Response +) => { + let serviceTokenData; + try { + const { workspaceId } = req.query; + + serviceTokenData = await ServiceTokenData + .find({ + workspace: workspaceId + }) + .select('+encryptedKey +iv +tag'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); } \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 7fcba66e1..57ddbaeb7 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -6,6 +6,7 @@ import requireOrganizationAuth from './requireOrganizationAuth'; import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth'; +import requireServiceTokenDataAuth from './requireServiceTokenDataAuth'; import validateRequest from './validateRequest'; export { @@ -17,5 +18,6 @@ export { requireIntegrationAuth, requireIntegrationAuthorizationAuth, requireServiceTokenAuth, + requireServiceTokenDataAuth, validateRequest }; diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 94e8363ff..904f4d38e 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -4,6 +4,7 @@ import { ServiceToken } from '../models'; import { JWT_SERVICE_SECRET } from '../config'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +// TODO: deprecate declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; diff --git a/backend/src/middleware/requireAPIKeyDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts similarity index 60% rename from backend/src/middleware/requireAPIKeyDataAuth.ts rename to backend/src/middleware/requireServiceTokenDataAuth.ts index 8dafb5a9c..1f2e33d7a 100644 --- a/backend/src/middleware/requireAPIKeyDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from 'express'; -import { APIKeyData } from '../models'; +import { ServiceToken, ServiceTokenData } from '../models'; import { validateMembership } from '../helpers/membership'; import { AccountNotFoundError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; -const requireAPIKeyDataAuth = ({ +const requireServiceTokenDataAuth = ({ acceptedRoles, acceptedStatuses, location = 'params' @@ -16,25 +16,25 @@ const requireAPIKeyDataAuth = ({ }) => { return async (req: Request, res: Response, next: NextFunction) => { - // req.user + const serviceTokenData = await ServiceTokenData + .findById(req[location].serviceTokenDataId) + .select('+encryptedKey +iv +tag'); - const apiKeyData = await APIKeyData.findById(req[location].apiKeyDataId); - - if (!apiKeyData) { - return next(AccountNotFoundError({message: 'Failed to locate API Key data'})); + if (!serviceTokenData) { + return next(AccountNotFoundError({message: 'Failed to locate service token data'})); } await validateMembership({ userId: req.user._id.toString(), - workspaceId: apiKeyData?.workspace.toString(), + workspaceId: serviceTokenData.workspace.toString(), acceptedRoles, acceptedStatuses }); - req.apiKeyData = '' // ?? + req.serviceTokenData = serviceTokenData; next(); } } -export default requireAPIKeyDataAuth; \ No newline at end of file +export default requireServiceTokenDataAuth; \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index fd9578523..8e934d511 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import APIKeyData, { IAPIKeyData } from './apiKeyData'; +import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; export { BackupPrivateKey, @@ -49,6 +49,6 @@ export { IUserAction, Workspace, IWorkspace, - APIKeyData, - IAPIKeyData, + ServiceTokenData, + IServiceTokenData }; diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index 73f705fc8..b5a2f4ec9 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,6 +1,7 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; +// TODO: deprecate export interface IServiceToken { _id: Types.ObjectId; name: string; diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/serviceTokenData .ts similarity index 57% rename from backend/src/models/apiKeyData.ts rename to backend/src/models/serviceTokenData .ts index 8b064bf08..01bac8d58 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/serviceTokenData .ts @@ -1,36 +1,33 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; -export interface IAPIKeyData { +export interface IServiceTokenData { name: string; - workspaces: { - workspace: Types.ObjectId, - environments: string[] - }[]; + workspace: Types.ObjectId; + environment: string; // TODO: adapt to upcoming environment id expiresAt: Date; prefix: string; - apiKeyHash: string; + serviceTokenHash: string; encryptedKey: string; iv: string; tag: string; } -const apiKeyDataSchema = new Schema( +const serviceTokenDataSchema = new Schema( { name: { type: String, required: true }, - workspaces: [{ - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace' - }, - environments: [{ - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] - }] - }], + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { // TODO: adapt to upcoming environment id + type: String, + required: true + }, expiresAt: { type: Date }, @@ -38,7 +35,7 @@ const apiKeyDataSchema = new Schema( type: String, required: true }, - apiKeyHash: { + serviceTokenHash: { type: String, unique: true, required: true @@ -61,6 +58,6 @@ const apiKeyDataSchema = new Schema( } ); -const APIKeyData = model('APIKeyData', apiKeyDataSchema); +const ServiceTokenData = model('ServiceTokenData', serviceTokenDataSchema); -export default APIKeyData; +export default ServiceTokenData; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts deleted file mode 100644 index a40c852ac..000000000 --- a/backend/src/routes/apiKey.ts +++ /dev/null @@ -1,127 +0,0 @@ -import express from 'express'; -const router = express.Router(); -import { - requireAuth -} from '../middleware'; -import { - APIKeyData -} from '../models'; -import { param, body, query } from 'express-validator'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; -import * as Sentry from '@sentry/node'; - -// TODO: middleware -router.post( - '/', - requireAuth, - body('name').exists().trim(), - body('workspace'), - body('environment'), - body('encryptedKey'), - body('iv'), - body('tag'), - body('expiresAt'), - async (req, res) => { - let apiKey, apiKeyData; - try { - const { - name, - workspace, - environment, - encryptedKey, - iv, - tag, - expiresAt - } = req.body; - - // create 38-char API key with first 6-char being the prefix - apiKey = crypto.randomBytes(19).toString('hex'); - - const saltRounds = 10; // TODO: add as config envar - const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); - - apiKeyData = await new APIKeyData({ - name, - workspace, - environment, - expiresAt, - prefix: apiKey.substring(0, 6), - apiKeyHash, - encryptedKey, - iv, - tag - }).save(); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create workspace API Key' - }); - } - - return res.status(200).send({ - apiKey, - apiKeyData - }); - } -); - -// TODO: middleware -router.get( - '/', - requireAuth, - query('workspaceId').exists().trim(), - async (req, res) => { - let apiKeyData; - try { - const { workspaceId } = req.query; - - apiKeyData = await APIKeyData.find({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace API Key data' - }); - } - - return res.status(200).send({ - apiKeyData - }); - } -); - -// TODO: middleware -router.delete( - ':apiKeyDataId', - requireAuth, - // TODO: requireAPIKeyDataAuth, - param('apiKeyDataId').exists().trim(), - async (req, res) => { - let apiKeyData; - try { - const { apiKeyDataId } = req.params; - - apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete API key data' - }); - } - - return res.status(200).send({ - apiKeyData - }); - } -); - -// INFISICAL TOKEN = . - -export default router; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index bc0ff776d..89b02ebc9 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -15,7 +15,7 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import apiKey from './apiKey'; +import serviceTokenData from './serviceTokenData'; export { signup, @@ -35,5 +35,5 @@ export { stripe, integration, integrationAuth, - apiKey + serviceTokenData }; diff --git a/backend/src/routes/serviceTokenData.ts b/backend/src/routes/serviceTokenData.ts new file mode 100644 index 000000000..903e72771 --- /dev/null +++ b/backend/src/routes/serviceTokenData.ts @@ -0,0 +1,144 @@ +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/workspace.ts b/backend/src/routes/workspace.ts index acd2aaf8b..3551841c7 100644 --- a/backend/src/routes/workspace.ts +++ b/backend/src/routes/workspace.ts @@ -119,7 +119,7 @@ router.get( ); router.get( - '/:workspaceId/service-tokens', + '/:workspaceId/service-tokens', // deprecate requireAuth, requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -130,4 +130,16 @@ router.get( workspaceController.getWorkspaceServiceTokens ); +router.get( + '/:workspaceId/service-token-data', + requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokenData +); + export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 319562fd0..a019be1e9 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -15,6 +15,7 @@ declare global { bot: any; serviceToken: any; accessToken: any; + serviceTokenData: any; query?: any; } } From 618dc10e45da2dc563a0765236e47db99f85d808 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 28 Dec 2022 12:02:42 -0500 Subject: [PATCH 14/91] Added .NET to available frameworks --- README.md | 18 +++- backend/src/models/secret.ts | 3 +- backend/src/routes/v2/secret.ts | 135 +++++++++++++++++++++++++++- cli/go.mod | 2 + cli/go.sum | 10 +++ cli/packages/cmd/secrets.go | 103 +++++++++++++++++++++ cli/packages/util/crypto.go | 56 +++++++++++- cli/packages/util/secrets.go | 7 +- cli/packages/visualize/secrets.go | 14 +++ cli/packages/visualize/visualize.go | 12 ++- docs/mint.json | 3 +- 11 files changed, 348 insertions(+), 15 deletions(-) create mode 100644 cli/packages/cmd/secrets.go create mode 100644 cli/packages/visualize/secrets.go diff --git a/README.md b/README.md index ac2f2a5b5..b3bcd7ac6 100644 --- a/README.md +++ b/README.md @@ -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... + + diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index d36e32b91..bbaaff8c3 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -33,7 +33,8 @@ const secretSchema = new Schema( { version: { type: Number, - required: true + required: true, + default: 1 }, workspace: { type: Schema.Types.ObjectId, diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 17a91d39c..fb2a23f42 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,4 +1,137 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; +import { requireAuth, validateRequest } from '../../middleware'; +import { ISecret, Secret } from '../../models'; +import { decryptSymmetric } from '../../utils/crypto'; +import { getLogger } from '../../utils/logger'; +import { body, param, query, check } from 'express-validator'; +import { BadRequestError } from '../../utils/errors'; const router = express.Router(); +/** + * Create a single secret for a given workspace and environment + */ +router.post( + '/', requireAuth, + body('secret').exists().isObject(), + async (req: Request, res: Response) => { + try { + const { secret }: { secret: ISecret[] } = req.body; + const newlyCreatedSecret = await Secret.create(secret) + res.status(200).json(newlyCreatedSecret) + } catch { + throw BadRequestError({ message: "Unable to create the secret" }) + } + } +); + +/** + * Create many secrets + */ +router.post( + '/bulk-create', requireAuth, + body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + async (req: Request, res: Response) => { + try { + const { secrets }: { secrets: ISecret[] } = req.body; + const newlyCreatedSecrets = await Secret.insertMany(secrets) + res.status(200).json(newlyCreatedSecrets) + } catch { + throw BadRequestError({ message: "Unable to create the secret" }) + } + } +); + +/** + * Get a single secret by secret id + */ +router.get( + '/:secretId', requireAuth, param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Get a single secret by secret id + */ +router.get( + '/:bulk', requireAuth, param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Delete a single secret by secret id + */ +router.delete( + '/:secretId', + requireAuth, + param('secretId').exists().trim(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.deleteOne({ + _id: req.params.secretId + }) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } + } +); + +/** + * Delete many secrets by secret ids + */ +router.delete( + '/batch', + requireAuth, + body('secretIds').exists().isArray(), + validateRequest, async (req: Request, res: Response) => { + try { + const secretIdsToDelete: string[] = req.body.secretIds + const secretFromDB = await Secret.deleteMany({ + _id: { $in: secretIdsToDelete } + }) + return res.status(200).send(secretFromDB); + } catch (error) { + throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` }) + } + } +); + +/** + * Apply modifications to many existing secrets + */ +router.patch( + '/bulk-update', + requireAuth, + body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + validateRequest, async (req: Request, res: Response) => { + try { + const { secrets }: { secrets: ISecret[] } = req.body; + + const operations = secrets.map((secretToUpdate: ISecret) => ({ + updateOne: { filter: { _id: secretToUpdate._id }, update: secretToUpdate }, + })); + + const bulkModificationInfo = await Secret.bulkWrite(operations); + + return res.status(200).json(bulkModificationInfo) + } catch (error) { + throw BadRequestError({ message: `Unable to process the bulk update. Double check the ids of the secrets` }) + } + } +); + export default router; diff --git a/cli/go.mod b/cli/go.mod index c48e3e2f9..956e9bb29 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/Luzifer/go-openssl/v4 v4.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.1.2 // indirect @@ -34,6 +35,7 @@ require ( ) require ( + github.com/Luzifer/go-openssl v2.0.0+incompatible github.com/go-resty/resty/v2 v2.7.0 github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible diff --git a/cli/go.sum b/cli/go.sum index d169d7b89..2b8836515 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -2,6 +2,10 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/Luzifer/go-openssl v2.0.0+incompatible h1:EpNNxrPDji4rRzE0KeOeIeV7pHyKe8zF9oNnAXy4mBY= +github.com/Luzifer/go-openssl v2.0.0+incompatible/go.mod h1:t2qnLjT8WQ3usGU1R8uAqjY4T7CK7eMg9vhQ3l9Ue/Y= +github.com/Luzifer/go-openssl/v4 v4.1.0 h1:8qi3Z6f8Aflwub/Cs4FVSmKUEg/lC8GlODbR2TyZ+nM= +github.com/Luzifer/go-openssl/v4 v4.1.0/go.mod h1:3i1T3Pe6eQK19d86WhuQzjLyMwBaNmGmt3ZceWpWVa4= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -96,15 +100,20 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -116,6 +125,7 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go new file mode 100644 index 000000000..f8efd6724 --- /dev/null +++ b/cli/packages/cmd/secrets.go @@ -0,0 +1,103 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/Infisical/infisical-merge/packages/visualize" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var secretsCmd = &cobra.Command{ + Example: `infisical secrets"`, + Short: "Used to create, read update and delete secrets", + Use: "secrets", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + + secrets, err := util.GetAllEnvironmentVariables("", "dev") + secrets = util.SubstituteSecrets(secrets) + if err != nil { + log.Debugln(err) + return + } + visualize.PrintAllSecretDetails(secrets) + }, +} + +var secretsGetCmd = &cobra.Command{ + Example: `secrets get ..."`, + Short: "Used to retrieve secrets by name", + Use: "get [secrets]", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + PreRun: toggleDebug, + Run: getSecretsByNames, +} + +var secretsSetCmd = &cobra.Command{ + Example: `secrets set ..."`, + Short: "Used update retrieve secrets by name", + Use: "set [secrets]", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("set secret") + }, +} + +var secretsDeleteCmd = &cobra.Command{ + Example: `secrets delete ..."`, + Short: "Used to delete secrets by name", + Use: "delete [secrets]", + DisableFlagsInUseLine: true, + PreRun: toggleDebug, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Delete secret") + }, +} + +func init() { + secretsCmd.AddCommand(secretsGetCmd) + secretsCmd.AddCommand(secretsSetCmd) + secretsCmd.AddCommand(secretsDeleteCmd) + rootCmd.AddCommand(secretsCmd) +} + +func getSecretsByNames(cmd *cobra.Command, args []string) { + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + requestedSecrets := []models.SingleEnvironmentVariable{} + + secretsMap := make(map[string]models.SingleEnvironmentVariable) + for _, secret := range secrets { + secretsMap[secret.Key] = secret + } + + for _, secretKeyFromArg := range args { + if value, ok := secretsMap[secretKeyFromArg]; ok { + requestedSecrets = append(requestedSecrets, value) + } else { + requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ + Key: secretKeyFromArg, + Type: "NOT FOUND", + Value: "NOT FOUND", + }) + } + } + + visualize.PrintAllSecretDetails(requestedSecrets) +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index c6eee2d0c..0431989de 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -3,21 +3,26 @@ package util import ( "crypto/aes" "crypto/cipher" + "crypto/rand" + "io" + + "golang.org/x/crypto/nacl/box" ) -func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) { +// will decrypt cipher text to plain text using iv and tag +func DecryptSymmetric(key []byte, cipherText []byte, tag []byte, iv []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV)) + aesgcm, err := cipher.NewGCMWithNonceSize(block, len(iv)) if err != nil { return nil, err } - var nonce = IV - var ciphertext = append(encryptedPrivateKey, tag...) + var nonce = iv + var ciphertext = append(cipherText, tag...) // the aesgcm open method expects auth tag at the end of the cipher text plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) if err != nil { @@ -26,3 +31,46 @@ func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []b return plaintext, nil } + +func GenerateNewKey() (newKey []byte, keyErr error) { + key := make([]byte, 16) // block size defaults to 16 so this is fine + _, err := rand.Read(key) + return key, err +} + +// Will encrypt a plain text with the provided key +func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, nil, err + } + + aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4 + if err != nil { + return nil, nil, nil, err + } + + // create a nonce + nonce := make([]byte, aesgcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + panic(err) + } + + ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil) + + ciphertextOnly := ciphertext[:len(ciphertext)-16] // combines the auth tag with the cipher text so we need to extract it + + authTag := ciphertext[len(ciphertext)-16:] + + return ciphertextOnly, nonce, authTag, nil +} + +func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { + plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return plainTextToReturn +} + +func EncryptAssymmetric(message []byte, nonce []byte, publicKey []byte, privateKey []byte) (encryptedMessage []byte) { + encryptedPlainText := box.Seal(nil, message, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey)) + return encryptedPlainText +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f88aed96b..f41e73a0f 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -11,7 +11,6 @@ import ( "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" - "golang.org/x/crypto/nacl/box" ) const PERSONAL_SECRET_TYPE_NAME = "personal" @@ -56,7 +55,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, } // log.Debugln("workspaceKey", workspaceKey, "nonce", nonce, "senderPublicKey", senderPublicKey, "currentUsersPrivateKey", currentUsersPrivateKey) - workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey) var listOfEnv []models.SingleEnvironmentVariable for _, secret := range pullSecretsRequestResponse.Secrets { @@ -166,7 +165,8 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, return nil, err } - workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + // workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey)) + workspaceKeyInBytes := DecryptAsymmetric(workspaceKey, nonce, senderPublicKey, currentUsersPrivateKey) var listOfEnv []models.SingleEnvironmentVariable for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets { @@ -223,6 +223,7 @@ func GetAllEnvironmentVariables(projectId string, envName string) ([]models.Sing return nil, err } + // TODO: Should be based on flag. I.e only get all workspaces if desired, otherwise only get the one in the current root of project workspaceConfigs, err := GetAllWorkSpaceConfigsStartingFromCurrentPath() if err != nil { return nil, fmt.Errorf("unable to check if you have a %s file in your current directory", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go new file mode 100644 index 000000000..e732d06af --- /dev/null +++ b/cli/packages/visualize/secrets.go @@ -0,0 +1,14 @@ +package visualize + +import "github.com/Infisical/infisical-merge/packages/models" + +func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { + rows := [][]string{} + for _, secret := range secrets { + rows = append(rows, []string{secret.Key, secret.Value, secret.Type}) + } + + headers := []string{"Secret name", "Secret vaule", "Secret type"} + + Table(headers, rows) +} diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 85d946a32..5658197ee 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -6,13 +6,23 @@ import ( "github.com/jedib0t/go-pretty/table" ) +type TableOptions struct { + Title string +} + +// func GetDefaultTableOptions() TableOptions{ +// return TableOptions{ +// Title: "", +// } +// } + // Given headers and rows, this function will print out a table func Table(headers []string, rows [][]string) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) - // t.SetTitle("Title") + // t.SetTitle(tableOptions.Title) t.Style().Options.DrawBorder = true t.Style().Options.SeparateHeader = true t.Style().Options.SeparateColumns = true 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" ] }, { From 60445727e9c33eb60abaa88ac3ebda8f2d06e880 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 31 Dec 2022 17:48:56 -0500 Subject: [PATCH 15/91] merge with own change --- backend/src/routes/v2/secret.ts | 94 +++++++++++++++++++++++++++---- backend/src/types/secret/index.ts | 4 ++ package-lock.json | 30 ++++++++++ package.json | 4 ++ 4 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 backend/src/types/secret/index.ts diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index fb2a23f42..42badbff8 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,10 +1,15 @@ import express, { Request, Response } from 'express'; -import { requireAuth, validateRequest } from '../../middleware'; +import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { ISecret, Secret } from '../../models'; import { decryptSymmetric } from '../../utils/crypto'; import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError } from '../../utils/errors'; +import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; +import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ModifySecretPayload } from '../../types/secret'; +import { AnyBulkWriteOperation } from 'mongodb'; +import to from 'await-to-js'; + const router = express.Router(); /** @@ -13,6 +18,10 @@ const router = express.Router(); router.post( '/', requireAuth, body('secret').exists().isObject(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), async (req: Request, res: Response) => { try { const { secret }: { secret: ISecret[] } = req.body; @@ -29,6 +38,10 @@ router.post( */ router.post( '/bulk-create', requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), async (req: Request, res: Response) => { try { @@ -46,6 +59,10 @@ router.post( */ router.get( '/:secretId', requireAuth, param('secretId').exists().trim(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { try { const secretFromDB = await Secret.findById(req.params.secretId) @@ -61,6 +78,10 @@ router.get( */ router.get( '/:bulk', requireAuth, param('secretId').exists().trim(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { try { const secretFromDB = await Secret.findById(req.params.secretId) @@ -77,6 +98,10 @@ router.get( router.delete( '/:secretId', requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), param('secretId').exists().trim(), validateRequest, async (req: Request, res: Response) => { try { @@ -96,6 +121,10 @@ router.delete( router.delete( '/batch', requireAuth, + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), body('secretIds').exists().isArray(), validateRequest, async (req: Request, res: Response) => { try { @@ -111,25 +140,66 @@ router.delete( ); /** - * Apply modifications to many existing secrets + * Apply modifications to many existing secrets in a given workspace and environment + * Note: although we do not check access for environments, we will in the future */ router.patch( - '/bulk-update', + '/bulk-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + param('workspaceId').exists().trim(), + param('environmentName').exists().trim(), + // requireWorkspaceAuth({ + // acceptedRoles: [ADMIN, MEMBER], + // acceptedStatuses: [COMPLETED, GRANTED] + // }), validateRequest, async (req: Request, res: Response) => { try { - const { secrets }: { secrets: ISecret[] } = req.body; + const { workspaceId, environmentName } = req.params + const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; - const operations = secrets.map((secretToUpdate: ISecret) => ({ - updateOne: { filter: { _id: secretToUpdate._id }, update: secretToUpdate }, - })); + const secretsUserCanModify: ISecret[] = await Secret.find({ workspace: workspaceId, environment: environmentName }) - const bulkModificationInfo = await Secret.bulkWrite(operations); + const secretsUserCanModifyMapBySecretId: Map = new Map(); + secretsUserCanModify.forEach(secret => secretsUserCanModifyMapBySecretId.set(secret._id.toString(), secret)) - return res.status(200).json(bulkModificationInfo) - } catch (error) { - throw BadRequestError({ message: `Unable to process the bulk update. Double check the ids of the secrets` }) + // Check if the entity has access to the secret ids it wants to modify + const updateOperationsToPerform: AnyBulkWriteOperation[] = [] + secretsModificationsRequested.forEach(userModifiedSecret => { + const canModifyRequestedSecret = secretsUserCanModifyMapBySecretId.has(userModifiedSecret._id.toString()) + if (canModifyRequestedSecret) { + const oldSecretInDB = secretsUserCanModifyMapBySecretId.get(userModifiedSecret._id.toString()) + + if (oldSecretInDB !== undefined) { + oldSecretInDB.secretKeyCiphertext = userModifiedSecret.secretKeyCiphertext + oldSecretInDB.secretKeyIV = userModifiedSecret.secretKeyIV + oldSecretInDB.secretKeyTag = userModifiedSecret.secretKeyTag + oldSecretInDB.secretKeyHash = userModifiedSecret.secretKeyHash + oldSecretInDB.secretValueCiphertext = userModifiedSecret.secretValueCiphertext + oldSecretInDB.secretValueIV = userModifiedSecret.secretValueIV + oldSecretInDB.secretValueTag = userModifiedSecret.secretValueTag + oldSecretInDB.secretValueHash = userModifiedSecret.secretValueHash + oldSecretInDB.secretCommentCiphertext = userModifiedSecret.secretCommentCiphertext + oldSecretInDB.secretCommentIV = userModifiedSecret.secretCommentIV + oldSecretInDB.secretCommentTag = userModifiedSecret.secretCommentTag + oldSecretInDB.secretCommentHash = userModifiedSecret.secretCommentHash + + const updateOperation = { updateOne: { filter: { _id: oldSecretInDB._id, workspace: oldSecretInDB.workspace }, update: { $inc: { version: 1 }, $set: oldSecretInDB } } } + updateOperationsToPerform.push(updateOperation) + } + } else { + throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + } + }) + + const bulkModificationInfo = await Secret.bulkWrite(updateOperationsToPerform); + + return res.status(200).json({ + bulkModificationInfo + }) + + } catch (e) { + throw BadRequestError() } } ); diff --git a/backend/src/types/secret/index.ts b/backend/src/types/secret/index.ts new file mode 100644 index 000000000..11696484e --- /dev/null +++ b/backend/src/types/secret/index.ts @@ -0,0 +1,4 @@ +import { Omit } from 'utility-types'; +import { ISecret } from '../../models'; + +export type ModifySecretPayload = Omit; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cf02a1760..0ba1a0659 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,10 @@ "": { "name": "infisical", "license": "ISC", + "dependencies": { + "await-to-js": "^3.0.0", + "utility-types": "^3.10.0" + }, "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" @@ -169,6 +173,14 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1092,6 +1104,14 @@ "punycode": "^2.1.0" } }, + "node_modules/utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1248,6 +1268,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1910,6 +1935,11 @@ "punycode": "^2.1.0" } }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 4d042a13d..5b50c5859 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,9 @@ "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" + }, + "dependencies": { + "await-to-js": "^3.0.0", + "utility-types": "^3.10.0" } } From a5e874144293aa411f4d3e670a0f5a52ce8f15e4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 31 Dec 2022 17:57:07 -0500 Subject: [PATCH 16/91] update json5 --- backend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 85b168c1e..0b557b81c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6497,9 +6497,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.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -16776,9 +16776,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.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonwebtoken": { From 3c6b1e51b59083fba9306993b66ad89fc05fa93a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 31 Dec 2022 20:43:49 -0500 Subject: [PATCH 17/91] Add non try catch error handle and fix bulk patch --- backend/package-lock.json | 28 +++++ backend/package.json | 2 + backend/src/middleware/requireAuth.ts | 12 +-- backend/src/routes/v2/secret.ts | 100 ++++++++---------- .../src/types/secret/{index.ts => types.ts} | 4 +- backend/tsconfig.json | 19 +++- package-lock.json | 30 ------ package.json | 4 - 8 files changed, 100 insertions(+), 99 deletions(-) rename backend/src/types/secret/{index.ts => types.ts} (50%) diff --git a/backend/package-lock.json b/backend/package-lock.json index 0b557b81c..95f146812 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "await-to-js": "^3.0.0", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -38,6 +39,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", + "utility-types": "^3.10.0", "winston": "^3.8.2", "winston-loki": "^6.0.6" }, @@ -3678,6 +3680,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/axios": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", @@ -11276,6 +11286,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -14656,6 +14674,11 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" + }, "axios": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", @@ -20178,6 +20201,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index d1a03a74c..1eb5e3b45 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,6 +6,7 @@ "@sentry/tracing": "^7.19.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "await-to-js": "^3.0.0", "axios": "^1.1.3", "bigint-conversion": "^2.2.2", "cookie-parser": "^1.4.6", @@ -29,6 +30,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", "typescript": "^4.9.3", + "utility-types": "^3.10.0", "winston": "^3.8.2", "winston-loki": "^6.0.6" }, diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index d917d362a..172f7b68e 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -20,10 +20,10 @@ declare module 'jsonwebtoken' { */ 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 [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 decodedToken = ( jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET) @@ -33,9 +33,9 @@ const requireAuth = async (req: Request, res: Response, next: NextFunction) => { _id: decodedToken.userId }).select('+publicKey'); - if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'})) + 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'})) + return next(UnauthorizedRequestError({ message: 'Unable to authenticate due to partially set up account' })) req.user = user; return next(); diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 42badbff8..078a04106 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -4,11 +4,12 @@ import { ISecret, Secret } from '../../models'; import { decryptSymmetric } from '../../utils/crypto'; import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; +import { BadRequestError, InternalServerError, UnauthorizedRequestError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { ModifySecretPayload } from '../../types/secret'; +import { ModifySecretPayload, SafeUpdateSecret } from '../../types/secret/types'; import { AnyBulkWriteOperation } from 'mongodb'; import to from 'await-to-js'; +import { Types } from 'mongoose'; const router = express.Router(); @@ -141,66 +142,59 @@ router.delete( /** * Apply modifications to many existing secrets in a given workspace and environment - * Note: although we do not check access for environments, we will in the future */ router.patch( '/bulk-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), - param('workspaceId').exists().trim(), + param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - // requireWorkspaceAuth({ - // acceptedRoles: [ADMIN, MEMBER], - // acceptedStatuses: [COMPLETED, GRANTED] - // }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { - try { - const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; - const secretsUserCanModify: ISecret[] = await Secret.find({ workspace: workspaceId, environment: environmentName }) - - const secretsUserCanModifyMapBySecretId: Map = new Map(); - secretsUserCanModify.forEach(secret => secretsUserCanModifyMapBySecretId.set(secret._id.toString(), secret)) - - // Check if the entity has access to the secret ids it wants to modify - const updateOperationsToPerform: AnyBulkWriteOperation[] = [] - secretsModificationsRequested.forEach(userModifiedSecret => { - const canModifyRequestedSecret = secretsUserCanModifyMapBySecretId.has(userModifiedSecret._id.toString()) - if (canModifyRequestedSecret) { - const oldSecretInDB = secretsUserCanModifyMapBySecretId.get(userModifiedSecret._id.toString()) - - if (oldSecretInDB !== undefined) { - oldSecretInDB.secretKeyCiphertext = userModifiedSecret.secretKeyCiphertext - oldSecretInDB.secretKeyIV = userModifiedSecret.secretKeyIV - oldSecretInDB.secretKeyTag = userModifiedSecret.secretKeyTag - oldSecretInDB.secretKeyHash = userModifiedSecret.secretKeyHash - oldSecretInDB.secretValueCiphertext = userModifiedSecret.secretValueCiphertext - oldSecretInDB.secretValueIV = userModifiedSecret.secretValueIV - oldSecretInDB.secretValueTag = userModifiedSecret.secretValueTag - oldSecretInDB.secretValueHash = userModifiedSecret.secretValueHash - oldSecretInDB.secretCommentCiphertext = userModifiedSecret.secretCommentCiphertext - oldSecretInDB.secretCommentIV = userModifiedSecret.secretCommentIV - oldSecretInDB.secretCommentTag = userModifiedSecret.secretCommentTag - oldSecretInDB.secretCommentHash = userModifiedSecret.secretCommentHash - - const updateOperation = { updateOne: { filter: { _id: oldSecretInDB._id, workspace: oldSecretInDB.workspace }, update: { $inc: { version: 1 }, $set: oldSecretInDB } } } - updateOperationsToPerform.push(updateOperation) - } - } else { - throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) - } - }) - - const bulkModificationInfo = await Secret.bulkWrite(updateOperationsToPerform); - - return res.status(200).json({ - bulkModificationInfo - }) - - } catch (e) { - throw BadRequestError() + const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanModifyError) { + throw InternalServerError({ message: "Unable to fetch secrets you own" }) } + + const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); + const updateOperationsToPerform: any = [] + + secretsModificationsRequested.forEach(userModifiedSecret => { + if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { + const safeUpdateFields: SafeUpdateSecret = { + secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, + secretKeyIV: userModifiedSecret.secretKeyIV, + secretKeyTag: userModifiedSecret.secretKeyTag, + secretKeyHash: userModifiedSecret.secretKeyHash, + secretValueCiphertext: userModifiedSecret.secretValueCiphertext, + secretValueIV: userModifiedSecret.secretValueIV, + secretValueTag: userModifiedSecret.secretValueTag, + secretValueHash: userModifiedSecret.secretValueHash, + secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, + secretCommentIV: userModifiedSecret.secretCommentIV, + secretCommentTag: userModifiedSecret.secretCommentTag, + secretCommentHash: userModifiedSecret.secretCommentHash, + } + + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: safeUpdateFields } } } + updateOperationsToPerform.push(updateOperation) + } else { + throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) + if (bulkModificationInfoError) { + throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + } + + return res.status(200).send() } ); diff --git a/backend/src/types/secret/index.ts b/backend/src/types/secret/types.ts similarity index 50% rename from backend/src/types/secret/index.ts rename to backend/src/types/secret/types.ts index 11696484e..c4a8d0cc9 100644 --- a/backend/src/types/secret/index.ts +++ b/backend/src/types/secret/types.ts @@ -1,4 +1,6 @@ import { Omit } from 'utility-types'; import { ISecret } from '../../models'; -export type ModifySecretPayload = Omit; \ No newline at end of file +export type ModifySecretPayload = Omit; + +export type SafeUpdateSecret = Partial>; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 0bfe3c372..98ea0808a 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,7 +1,9 @@ { "compilerOptions": { "target": "es2016", - "lib": ["es6"], + "lib": [ + "es6" + ], "module": "commonjs", "rootDir": "src", "resolveJsonModule": true, @@ -13,8 +15,15 @@ "strict": true, "noImplicitAny": true, "skipLibCheck": true, - "typeRoots": ["./src/types", "./node_modules/@types"] + "typeRoots": [ + "./src/types", + "./node_modules/@types" + ] }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0ba1a0659..cf02a1760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,6 @@ "": { "name": "infisical", "license": "ISC", - "dependencies": { - "await-to-js": "^3.0.0", - "utility-types": "^3.10.0" - }, "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" @@ -173,14 +169,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/await-to-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", - "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1104,14 +1092,6 @@ "punycode": "^2.1.0" } }, - "node_modules/utility-types": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", - "engines": { - "node": ">= 4" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1268,11 +1248,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "await-to-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", - "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1935,11 +1910,6 @@ "punycode": "^2.1.0" } }, - "utility-types": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 5b50c5859..4d042a13d 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,5 @@ "devDependencies": { "eslint": "^8.29.0", "husky": "^8.0.2" - }, - "dependencies": { - "await-to-js": "^3.0.0", - "utility-types": "^3.10.0" } } From b8a64714d25970acde15bafe833f6185bbed6c02 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 1 Jan 2023 09:24:20 +0700 Subject: [PATCH 18/91] Refactor auth middleware to accept multiple auth modes --- .../v1/serviceTokenDataController.ts | 10 +- .../src/controllers/v2/workspaceController.ts | 2 +- backend/src/helpers/auth.ts | 130 +++++++++++++----- backend/src/middleware/requireAuth.ts | 37 +++-- backend/src/models/serviceTokenData .ts | 8 +- backend/src/routes/v1/serviceTokenData.ts | 2 +- backend/src/routes/v2/workspace.ts | 6 +- backend/src/utils/errors.ts | 10 ++ 8 files changed, 138 insertions(+), 67 deletions(-) diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v1/serviceTokenDataController.ts index 83701abcd..3a4b94a83 100644 --- a/backend/src/controllers/v1/serviceTokenDataController.ts +++ b/backend/src/controllers/v1/serviceTokenDataController.ts @@ -48,7 +48,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - serviceTokenData = await new ServiceTokenData({ + // create service token data + serviceTokenData = new ServiceTokenData({ name, workspace: workspaceId, environment, @@ -59,7 +60,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => { encryptedKey, iv, tag - }).save(); + }) + + await serviceTokenData.save(); + + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index fa0ffa5af..1b10ebccd 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -139,7 +139,7 @@ export const pullSecrets = async (req: Request, res: Response) => { environment }); - if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs + if (channel !== 'cli') { secrets = reformatPullSecrets({ secrets }); } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 9845e9c74..c3b212916 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -12,56 +12,112 @@ import { JWT_REFRESH_SECRET, SALT_ROUNDS } from '../config'; +import { + AccountNotFoundError, + ServiceTokenDataNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; /** - * Attach auth payload + * Validate that auth token value [authTokenValue] falls under one of + * accepted auth modes [acceptedAuthModes]. * @param {Object} obj - * @param {String} obj.authTokenValue + * @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value) + * @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken) + * @returns {String} authMode - auth mode */ -const attachAuthPayload = async ({ +const validateAuthMode = ({ + authTokenValue, + acceptedAuthModes +}: { + authTokenValue: string; + acceptedAuthModes: string[]; +}) => { + let authMode; + try { + switch (authTokenValue.split('.', 1)[0]) { + case 'st': + authMode = 'serviceToken'; + break; + default: + authMode = 'jwt'; + break; + } + + if (!acceptedAuthModes.includes(authMode)) + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + + } catch (err) { + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + } + + return authMode; +} + +/** + * Return user payload corresponding to JWT token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - JWT token value + * @returns {User} user - user corresponding to JWT token + */ +const getAuthUserPayload = async ({ authTokenValue }: { authTokenValue: string; }) => { - let serviceTokenHash, decodedToken; // intermediate variables - let serviceTokenData, user; // payloads + let user; 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'); - } + const decodedToken = ( + jwt.verify(authTokenValue, JWT_AUTH_SECRET) + ); - return serviceTokenData; - default: - // case: JWT token auth mode - decodedToken = ( - jwt.verify(authTokenValue, JWT_AUTH_SECRET) - ); - - user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - if (!user) - throw new Error('Account not found error'); + if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); - if (!user?.publicKey) - throw new Error('Unable to authenticate due to partially set up account'); + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); - return user; - } } catch (err) { - throw new Error('Failed to attach auth payload'); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate JWT token' + }); } + + return user; +} + +/** + * Return service token data payload corresponding to service token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - service token value + * @returns {ServiceTokenData} serviceTokenData - service token data + */ +const getAuthSTDPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let serviceTokenData; + try { + const serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); + + serviceTokenData = await ServiceTokenData + .findOne({ + serviceTokenHash + }) + .select('+encryptedKey +iv +tag'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + + } catch (err) { + throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); + } + + return serviceTokenData; } /** @@ -154,7 +210,9 @@ const createToken = ({ }; export { - attachAuthPayload, + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload, createToken, issueTokens, clearTokens diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 52f69d260..b91d2cd09 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -2,7 +2,9 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { User, ServiceTokenData } from '../models'; import { - attachAuthPayload + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload } from '../helpers/auth'; import { JWT_AUTH_SECRET } from '../config'; import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; @@ -37,30 +39,25 @@ const requireAuth = ({ 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'); - - // attach auth request payload - const payload = await attachAuthPayload({ - authTokenValue: AUTH_TOKEN_VALUE + // validate auth token against + const authMode = validateAuthMode({ + authTokenValue: AUTH_TOKEN_VALUE, + acceptedAuthModes }); + if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); + + // attach auth payloads switch (authMode) { - case 'st': - req.serviceTokenData = payload; + case 'serviceToken': + req.serviceTokenData = await getAuthSTDPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); break; default: - req.user = payload; + req.user = await getAuthUserPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); break; } diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts index 32520dda9..1faecfaff 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData .ts @@ -45,19 +45,19 @@ const serviceTokenDataSchema = new Schema( type: String, unique: true, required: true, - select: true + select: false }, encryptedKey: { type: String, - select: true + select: false }, iv: { type: String, - select: true + select: false }, tag: { type: String, - select: true + select: false } }, { diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v1/serviceTokenData.ts index fa9f4fdbd..4223172d3 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v1/serviceTokenData.ts @@ -18,7 +18,7 @@ import { serviceTokenDataController } from '../../controllers/v1'; router.get( '/', requireAuth({ - acceptedAuthModes: ['st'] + acceptedAuthModes: ['serviceToken'] }), param('serviceTokenDataId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 6fc81bbc5..df26749f3 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -11,7 +11,7 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { membershipController } from '../../controllers/v1'; import { workspaceController } from '../../controllers/v2'; -router.post( // unfinished +router.post( '/:workspaceId/secrets', requireAuth({ acceptedAuthModes: ['jwt'] @@ -29,10 +29,10 @@ router.post( // unfinished workspaceController.pushWorkspaceSecrets ); -router.get( // unfinished, check that it works with st +router.get( '/:workspaceId/secrets', requireAuth({ - acceptedAuthModes: ['jwt', 'st'] + acceptedAuthModes: ['jwt', 'serviceToken'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 40c467131..2b2ce59a6 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -113,4 +113,14 @@ export const AccountNotFoundError = (error?: Partial) => ne stack: error?.stack }) +//* ----->[SERVICE TOKEN DATA ERRORS]<----- +export const ServiceTokenDataNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'service_token_data_not_found_error', + message: error?.message ?? 'The requested service token data was not found', + context: error?.context, + stack: error?.stack +}) + //* ----->[MISC ERRORS]<----- From 6f054d8f2cf34f9fa7efd637507de1048c498ef3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 1 Jan 2023 10:36:07 +0700 Subject: [PATCH 19/91] Add requireSecretAuth middleware --- backend/src/ee/routes/v1/secret.ts | 4 +- backend/src/middleware/index.ts | 2 + backend/src/middleware/requireBotAuth.ts | 2 +- backend/src/middleware/requireSecretAuth.ts | 50 +++++++++++++++++++ backend/src/types/express/index.d.ts | 4 +- backend/src/utils/errors.ts | 34 ++++++++----- frontend/components/utilities/attemptLogin.js | 1 + 7 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 backend/src/middleware/requireSecretAuth.ts diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index a866a6320..7217c96e2 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -2,7 +2,7 @@ import express from 'express'; const router = express.Router(); import { requireAuth, - requireWorkspaceAuth, + requireSecretAuth, validateRequest } from '../../../middleware'; import { body, query, param } from 'express-validator'; @@ -12,7 +12,7 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; router.get( '/:secretId/secret-versions', requireAuth, - requireWorkspaceAuth({ + requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 7fcba66e1..bb2cc875c 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -6,6 +6,7 @@ import requireOrganizationAuth from './requireOrganizationAuth'; import requireIntegrationAuth from './requireIntegrationAuth'; import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; import requireServiceTokenAuth from './requireServiceTokenAuth'; +import requireSecretAuth from './requireSecretAuth'; import validateRequest from './validateRequest'; export { @@ -17,5 +18,6 @@ export { requireIntegrationAuth, requireIntegrationAuthorizationAuth, requireServiceTokenAuth, + requireSecretAuth, validateRequest }; diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index e39f0d1b5..14c099393 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -15,7 +15,7 @@ const requireBotAuth = ({ location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - const bot = await Bot.findOne({ _id: req[location].botId }); + const bot = await Bot.findById(req[location].botId); if (!bot) { return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts new file mode 100644 index 000000000..8f6fc5305 --- /dev/null +++ b/backend/src/middleware/requireSecretAuth.ts @@ -0,0 +1,50 @@ +import { Request, Response, NextFunction } from 'express'; +import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; +import { Secret } from '../models'; +import { + validateMembership +} from '../helpers/membership'; + +/** + * Validate if user on request has proper membership to modify secret. + * @param {Object} obj + * @param {String[]} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.acceptedStatuses - accepted workspace statuses + * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + */ +const requireSecretAuth = ({ + acceptedRoles, + acceptedStatuses +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const { secretId } = req.params; + + const secret = await Secret.findById(secretId); + + if (!secret) { + return next(SecretNotFoundError({ + message: 'Failed to find secret' + })); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: secret.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.secret = secret as any; + + next(); + } catch (err) { + return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' })); + } + } +} + +export default requireSecretAuth; \ No newline at end of file diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 319562fd0..68961a946 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,6 +1,5 @@ import * as express from 'express'; - // TODO: fix (any) types declare global { namespace Express { @@ -8,11 +7,12 @@ declare global { user: any; workspace: any; membership: any; - organizationt: any; + organization: any; membershipOrg: any; integration: any; integrationAuth: any; bot: any; + secret: any; serviceToken: any; accessToken: any; query?: any; diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 40c467131..49afb217a 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -8,7 +8,7 @@ export const RouteNotFoundError = (error?: Partial) => new message: error?.message ?? 'The requested source was not found', context: error?.context, stack: error?.stack -}) +}); export const MethodNotAllowedError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, @@ -17,7 +17,7 @@ export const MethodNotAllowedError = (error?: Partial) => n message: error?.message ?? 'The requested method is not allowed for the resource', context: error?.context, stack: error?.stack -}) +}); export const UnauthorizedRequestError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, @@ -26,7 +26,7 @@ export const UnauthorizedRequestError = (error?: Partial) = message: error?.message ?? 'You are not authorized to access this resource', context: error?.context, stack: error?.stack -}) +}); export const ForbiddenRequestError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, @@ -35,7 +35,7 @@ export const ForbiddenRequestError = (error?: Partial) => n message: error?.message ?? 'You are not allowed to access this resource', context: error?.context, stack: error?.stack -}) +}); export const BadRequestError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, @@ -44,7 +44,7 @@ export const BadRequestError = (error?: Partial) => new Req message: error?.message ?? 'The request is invalid or cannot be served', context: error?.context, stack: error?.stack -}) +}); export const InternalServerError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -53,7 +53,7 @@ export const InternalServerError = (error?: Partial) => new message: error?.message ?? 'The server encountered an error while processing the request', context: error?.context, stack: error?.stack -}) +}); export const ServiceUnavailableError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -62,7 +62,7 @@ export const ServiceUnavailableError = (error?: Partial) => message: error?.message ?? 'The service is currently unavailable. Please try again later.', context: error?.context, stack: error?.stack -}) +}); export const ValidationError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -71,7 +71,7 @@ export const ValidationError = (error?: Partial) => new Req message: error?.message ?? 'The request failed validation', context: error?.context, stack: error?.stack -}) +}); //* ----->[INTEGRATION ERRORS]<----- export const IntegrationNotFoundError = (error?: Partial) => new RequestError({ @@ -81,7 +81,7 @@ export const IntegrationNotFoundError = (error?: Partial) = message: error?.message ?? 'The requested integration was not found', context: error?.context, stack: error?.stack -}) +}); //* ----->[WORKSPACE ERRORS]<----- export const WorkspaceNotFoundError = (error?: Partial) => new RequestError({ @@ -91,7 +91,7 @@ export const WorkspaceNotFoundError = (error?: Partial) => message: error?.message ?? 'The requested workspace was not found', context: error?.context, stack: error?.stack -}) +}); //* ----->[ORGANIZATION ERRORS]<----- export const OrganizationNotFoundError = (error?: Partial) => new RequestError({ @@ -101,7 +101,7 @@ export const OrganizationNotFoundError = (error?: Partial) message: error?.message ?? 'The requested organization was not found', context: error?.context, stack: error?.stack -}) +}); //* ----->[ACCOUNT ERRORS]<----- export const AccountNotFoundError = (error?: Partial) => new RequestError({ @@ -111,6 +111,16 @@ export const AccountNotFoundError = (error?: Partial) => ne message: error?.message ?? 'The requested account was not found', context: error?.context, stack: error?.stack -}) +}); + +//* ----->[SECRET ERRORS]<----- +export const SecretNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'secret_not_found_error', + message: error?.message ?? 'The requested secret was not found', + context: error?.context, + stack: error?.stack +}); //* ----->[MISC ERRORS]<----- diff --git a/frontend/components/utilities/attemptLogin.js b/frontend/components/utilities/attemptLogin.js index e1fc5f9cf..db4725c0d 100644 --- a/frontend/components/utilities/attemptLogin.js +++ b/frontend/components/utilities/attemptLogin.js @@ -52,6 +52,7 @@ const attemptLogin = async ( // if everything works, go the main dashboard page. const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(email, clientProof); + SecurityClient.setToken(token); const privateKey = Aes256Gcm.decrypt({ From 4dac65eb8a34625603eed3a5b66e7a7564a8fd7c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 1 Jan 2023 10:54:23 +0700 Subject: [PATCH 20/91] Begin action route for getting an action by id --- backend/src/app.ts | 4 +--- .../src/ee/controllers/v1/actionController.ts | 20 +++++++++++++++++++ backend/src/ee/controllers/v1/index.ts | 4 +++- backend/src/ee/routes/v1/action.ts | 17 ++++++++++++++++ backend/src/ee/routes/v1/index.ts | 4 ++-- backend/src/ee/routes/v1/log.ts | 4 ---- 6 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 backend/src/ee/controllers/v1/actionController.ts create mode 100644 backend/src/ee/routes/v1/action.ts delete mode 100644 backend/src/ee/routes/v1/log.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 4e8e23fc0..8320a7d76 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -13,8 +13,7 @@ import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, - secret as eeSecretRouter, - log as eeLogRouter, + secret as eeSecretRouter } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -70,7 +69,6 @@ if (NODE_ENV === 'production') { // (EE) routes app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); -app.use('/api/v1/log', eeLogRouter); // v1 routes app.use('/api/v1/signup', v1SignupRouter); diff --git a/backend/src/ee/controllers/v1/actionController.ts b/backend/src/ee/controllers/v1/actionController.ts new file mode 100644 index 000000000..20b470037 --- /dev/null +++ b/backend/src/ee/controllers/v1/actionController.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { Action } from '../../models'; + +export const getAction = (req: Request, res: Response) => { + let action; + // try { + // const { actionId } = req.params; + + // action = await Action.findById(actionId); + + + // } catch (err) { + + // } + + return res.status(200).send({ + action + }); +} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index 23880070d..2a082de70 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -1,9 +1,11 @@ import * as stripeController from './stripeController'; import * as secretController from './secretController'; import * as workspaceController from './workspaceController'; +import * as actionController from './actionController'; export { stripeController, secretController, - workspaceController + workspaceController, + actionController } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/action.ts b/backend/src/ee/routes/v1/action.ts new file mode 100644 index 000000000..5dca83cf9 --- /dev/null +++ b/backend/src/ee/routes/v1/action.ts @@ -0,0 +1,17 @@ +import express from 'express'; +const router = express.Router(); +import { + validateRequest +} from '../../../middleware'; +import { param } from 'express-validator'; +import { actionController } from '../../controllers/v1'; + +// TODO: put into action controller +router.get( + '/:actionId', + param('actionId').exists().trim(), + validateRequest, + actionController.getAction +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 810a050a6..02fc80939 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,9 +1,9 @@ import secret from './secret'; import workspace from './workspace'; -import log from './log'; +import action from './action'; export { secret, workspace, - log + action } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/log.ts b/backend/src/ee/routes/v1/log.ts deleted file mode 100644 index d90cb867c..000000000 --- a/backend/src/ee/routes/v1/log.ts +++ /dev/null @@ -1,4 +0,0 @@ -import express from 'express'; -const router = express.Router(); - -export default router; \ No newline at end of file From 0aff94cfb39c3ad9c32444525099d9269c49573e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 1 Jan 2023 10:55:23 +0700 Subject: [PATCH 21/91] Add action error --- backend/src/utils/errors.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 49afb217a..ba5611465 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -123,4 +123,14 @@ export const SecretNotFoundError = (error?: Partial) => new stack: error?.stack }); +//* ----->[ACTION ERRORS]<----- +export const ActionNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'action_not_found_error', + message: error?.message ?? 'The requested action was not found', + context: error?.context, + stack: error?.stack +}); + //* ----->[MISC ERRORS]<----- From 9c83808e2e2701c2d1a9c5f3164c26a7f4d75917 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sat, 31 Dec 2022 20:17:40 -0800 Subject: [PATCH 22/91] Added populate statement --- backend/src/ee/controllers/v1/workspaceController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 1c9a71bc8..cff7f4a98 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -51,7 +51,8 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { logs = await Log.find(filters) .skip(offset) .limit(limit) - .populate('actions'); + .populate('actions') + .populate('user'); } catch (err) { Sentry.setUser({ email: req.user.email }); From f015e6be6e465a1fcd271572fe17c3472193d990 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 02:12:24 -0500 Subject: [PATCH 23/91] Add batch delete api and batch create api --- backend/src/middleware/validateRequest.ts | 6 +- backend/src/routes/v2/secret.ts | 180 ++++++++++------------ backend/src/types/secret/types.ts | 9 +- 3 files changed, 95 insertions(+), 100 deletions(-) diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 484b02cab..3bc106189 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { validationResult } from 'express-validator'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors'; /** * Validate intended inputs on [req] via express-validator @@ -15,12 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return next(BadRequestError({context: {errors: errors.array}})) + return next(ValidationError({ context: { errors: `One or more of your paramters are invalid [error=${errors.array}]` } })) } return next(); } catch (err) { - return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'})) + return next(UnauthorizedRequestError({ message: 'Unauthenticated requests are not allowed. Try logging in' })) } }; diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 078a04106..e9c6a9bd9 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,57 +1,70 @@ import express, { Request, Response } from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { ISecret, Secret } from '../../models'; -import { decryptSymmetric } from '../../utils/crypto'; -import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError, InternalServerError, UnauthorizedRequestError } from '../../utils/errors'; +import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { ModifySecretPayload, SafeUpdateSecret } from '../../types/secret/types'; -import { AnyBulkWriteOperation } from 'mongodb'; +import { SanitizedSecretModify, SecretUserInput, SanitizedSecretForCreate } from '../../types/secret/types'; import to from 'await-to-js'; -import { Types } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; +import { AnyBulkWriteOperation } from 'mongodb'; +const { ValidationError } = mongoose.Error; const router = express.Router(); /** - * Create a single secret for a given workspace and environment + * Create many secrets for a given workspace and environmentName */ router.post( - '/', requireAuth, - body('secret').exists().isObject(), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - async (req: Request, res: Response) => { - try { - const { secret }: { secret: ISecret[] } = req.body; - const newlyCreatedSecret = await Secret.create(secret) - res.status(200).json(newlyCreatedSecret) - } catch { - throw BadRequestError({ message: "Unable to create the secret" }) - } - } -); - -/** - * Create many secrets - */ -router.post( - '/bulk-create', requireAuth, + '/batch-create/workspace/:workspaceId/environment/:environmentName', + requireAuth, requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), + param('workspaceId').exists().isMongoId().trim(), + param('environmentName').exists().trim(), body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + validateRequest, async (req: Request, res: Response) => { - try { - const { secrets }: { secrets: ISecret[] } = req.body; - const newlyCreatedSecrets = await Secret.insertMany(secrets) - res.status(200).json(newlyCreatedSecrets) - } catch { - throw BadRequestError({ message: "Unable to create the secret" }) + const secretsToCreate: SecretUserInput[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] + + secretsToCreate.forEach(rawSecret => { + const safeUpdateFields: SanitizedSecretForCreate = { + secretKeyCiphertext: rawSecret.secretKeyCiphertext, + secretKeyIV: rawSecret.secretKeyIV, + secretKeyTag: rawSecret.secretKeyTag, + secretKeyHash: rawSecret.secretKeyHash, + secretValueCiphertext: rawSecret.secretValueCiphertext, + secretValueIV: rawSecret.secretValueIV, + secretValueTag: rawSecret.secretValueTag, + secretValueHash: rawSecret.secretValueHash, + secretCommentCiphertext: rawSecret.secretCommentCiphertext, + secretCommentIV: rawSecret.secretCommentIV, + secretCommentTag: rawSecret.secretCommentTag, + secretCommentHash: rawSecret.secretCommentHash, + workspace: new Types.ObjectId(workspaceId), + environment: environmentName, + type: rawSecret.type, + user: new Types.ObjectId(req.user._id) + } + + sanitizedSecretesToCreate.push(safeUpdateFields) + }) + + const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) + + if (bulkCreateError) { + if (bulkCreateError instanceof ValidationError) { + throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) + } + + throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) } + + res.status(200).send(newlyCreatedSecrets) } ); @@ -75,68 +88,45 @@ router.get( ); /** - * Get a single secret by secret id - */ -router.get( - '/:bulk', requireAuth, param('secretId').exists().trim(), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.findById(req.params.secretId) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } -); - -/** - * Delete a single secret by secret id + * Batch delete secrets in a given workspace and environment name */ router.delete( - '/:secretId', + '/batch/workspace/:workspaceId/environment/:environmentName', requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('secretId').exists().trim(), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.deleteOne({ - _id: req.params.secretId - }) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } -); - -/** - * Delete many secrets by secret ids - */ -router.delete( - '/batch', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), + param('workspaceId').exists().isMongoId().trim(), + param('environmentName').exists().trim(), body('secretIds').exists().isArray(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { - try { - const secretIdsToDelete: string[] = req.body.secretIds - const secretFromDB = await Secret.deleteMany({ - _id: { $in: secretIdsToDelete } - }) - return res.status(200).send(secretFromDB); - } catch (error) { - throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` }) + const { workspaceId, environmentName } = req.params + const secretIdsToDelete: string[] = req.body.secretIds + + const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanDeleteError) { + throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) } + + const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + + secretIdsToDelete.forEach(secretIdToDelete => { + if (secretsUserCanDeleteSet.has(secretIdToDelete)) { + const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } + deleteOperationsToPerform.push(deleteOperation) + } else { + throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkModificationInfoError) { + throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + } + + res.status(200).send() } ); @@ -144,7 +134,7 @@ router.delete( * Apply modifications to many existing secrets in a given workspace and environment */ router.patch( - '/bulk-modify/:workspaceId/:environmentName', + '/batch-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), param('workspaceId').exists().isMongoId().trim(), @@ -155,7 +145,7 @@ router.patch( }), validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; + const secretsModificationsRequested: SecretUserInput[] = req.body.secrets; const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { @@ -167,7 +157,7 @@ router.patch( secretsModificationsRequested.forEach(userModifiedSecret => { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { - const safeUpdateFields: SafeUpdateSecret = { + const sanitizedSecret: SanitizedSecretModify = { secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, secretKeyIV: userModifiedSecret.secretKeyIV, secretKeyTag: userModifiedSecret.secretKeyTag, @@ -182,7 +172,7 @@ router.patch( secretCommentHash: userModifiedSecret.secretCommentHash, } - const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: safeUpdateFields } } } + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } updateOperationsToPerform.push(updateOperation) } else { throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) diff --git a/backend/src/types/secret/types.ts b/backend/src/types/secret/types.ts index c4a8d0cc9..a773d778c 100644 --- a/backend/src/types/secret/types.ts +++ b/backend/src/types/secret/types.ts @@ -1,6 +1,11 @@ import { Omit } from 'utility-types'; import { ISecret } from '../../models'; -export type ModifySecretPayload = Omit; +// User input for CRUD operations on secrets routes +export type SecretUserInput = Omit; -export type SafeUpdateSecret = Partial>; +// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading +export type SanitizedSecretModify = Partial>; + +// Used for modeling sanitized secrets before create. To be used for converting user input for creating new secrets +export type SanitizedSecretForCreate = Omit; From 939e9ba0757a58e6815ccfbc140eb39f85d7a978 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 10:39:23 -0500 Subject: [PATCH 24/91] rename secret route with workspace and environment hierarchy --- backend/src/routes/v2/secret.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index e9c6a9bd9..65a926783 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -55,7 +55,6 @@ router.post( }) const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) - if (bulkCreateError) { if (bulkCreateError instanceof ValidationError) { throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) @@ -64,7 +63,7 @@ router.post( throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) } - res.status(200).send(newlyCreatedSecrets) + res.status(200).send() } ); @@ -134,7 +133,7 @@ router.delete( * Apply modifications to many existing secrets in a given workspace and environment */ router.patch( - '/batch-modify/:workspaceId/:environmentName', + '/batch-modify/workspace/:workspaceId/environment/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), param('workspaceId').exists().isMongoId().trim(), From 776b4c29225c20976179405e0fa8769452b521a2 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 11:18:00 -0500 Subject: [PATCH 25/91] update types for request body in secrets v2 api --- backend/src/routes/v2/secret.ts | 6 +++--- backend/src/types/secret/types.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 65a926783..5691dda49 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -4,7 +4,7 @@ import { ISecret, Secret } from '../../models'; import { body, param, query, check } from 'express-validator'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { SanitizedSecretModify, SecretUserInput, SanitizedSecretForCreate } from '../../types/secret/types'; +import { SanitizedSecretModify, CreateSecretRequestBody, SanitizedSecretForCreate, ModifySecretRequestBody } from '../../types/secret/types'; import to from 'await-to-js'; import mongoose, { Types } from 'mongoose'; import { AnyBulkWriteOperation } from 'mongodb'; @@ -27,7 +27,7 @@ router.post( body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), validateRequest, async (req: Request, res: Response) => { - const secretsToCreate: SecretUserInput[] = req.body.secrets; + const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const { workspaceId, environmentName } = req.params const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] @@ -144,7 +144,7 @@ router.patch( }), validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: SecretUserInput[] = req.body.secrets; + const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { diff --git a/backend/src/types/secret/types.ts b/backend/src/types/secret/types.ts index a773d778c..177df8c0f 100644 --- a/backend/src/types/secret/types.ts +++ b/backend/src/types/secret/types.ts @@ -1,11 +1,14 @@ -import { Omit } from 'utility-types'; +import { Assign, Omit } from 'utility-types'; import { ISecret } from '../../models'; -// User input for CRUD operations on secrets routes -export type SecretUserInput = Omit; +// Everything is required, except the omitted types +export type CreateSecretRequestBody = Omit; + +// Omit the listed properties, then make everything optional and then make _id required +export type ModifySecretRequestBody = Assign>, { _id: string }>; // Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading export type SanitizedSecretModify = Partial>; -// Used for modeling sanitized secrets before create. To be used for converting user input for creating new secrets +// Everything is required, except the omitted types export type SanitizedSecretForCreate = Omit; From f2bd4aec39ca98ef39da9767fb3a011b0f8ccbe4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 13:13:03 -0500 Subject: [PATCH 26/91] Show full validation error --- backend/src/middleware/validateRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 3bc106189..1b0364766 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -15,7 +15,7 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return next(ValidationError({ context: { errors: `One or more of your paramters are invalid [error=${errors.array}]` } })) + return next(ValidationError({ context: { errors: `One or more of your parameters are invalid [error(s)=${(JSON.stringify(errors))}]` } })) } return next(); From ac4b67d98ea3c9ddb099352dfa15ef47fcedcf7a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 19:22:09 -0500 Subject: [PATCH 27/91] delete, get and create via cli --- backend/src/routes/v2/secret.ts | 4 +- cli/packages/cmd/root.go | 2 +- cli/packages/cmd/secrets.go | 173 +++++++++++++++++++++++++++++-- cli/packages/http/api.go | 82 +++++++++++++++ cli/packages/models/api.go | 63 +++++++++++ cli/packages/models/cli.go | 7 ++ cli/packages/util/credentials.go | 53 ++++++++++ cli/packages/util/crypto.go | 13 ++- cli/packages/util/secrets.go | 2 + 9 files changed, 385 insertions(+), 14 deletions(-) create mode 100644 cli/packages/http/api.go diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 5691dda49..69b70c37e 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -24,7 +24,7 @@ router.post( }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')), validateRequest, async (req: Request, res: Response) => { const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; @@ -94,7 +94,7 @@ router.delete( requireAuth, param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), - body('secretIds').exists().isArray(), + body('secretIds').exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index f09f08800..0d505e968 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -30,7 +30,7 @@ func Execute() { func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") - rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") + rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "http://localhost:8080/api", "Point the CLI to your own backend") // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { // } } diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index f8efd6724..a926a4293 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -4,11 +4,17 @@ Copyright © 2022 NAME HERE package cmd import ( + "encoding/base64" "fmt" + "strings" + "crypto/sha256" + + "github.com/Infisical/infisical-merge/packages/http" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/visualize" + "github.com/go-resty/resty/v2" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,14 +27,15 @@ var secretsCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") secrets = util.SubstituteSecrets(secrets) if err != nil { log.Debugln(err) return } + visualize.PrintAllSecretDetails(secrets) + }, } @@ -48,9 +55,95 @@ var secretsSetCmd = &cobra.Command{ Use: "set [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, - Args: cobra.NoArgs, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - fmt.Println("set secret") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + log.Error(err) + return + } + + if !loggedInUserDetails.IsUserLoggedIn { + log.Error("You are not logged in yet. Please run [infisical login] then try again") + return + } + + if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired { + log.Error("Your login has expired. Please run [infisical login] then try again") + return + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + request := models.GetEncryptedWorkspaceKeyRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + } + + workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request) + if err != nil { + log.Errorf("unable to get your encrypted workspace key. [err=%v]", err) + return + } + + encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.EncryptedKey) + encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Sender.PublicKey) + encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.LatestKey.Nonce) + currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) + + // decrypt workspace key + plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) + secretsToUpload := []models.Secret{} + for _, arg := range args { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if len(splitKeyValueFromArg) < 2 { + splitKeyValueFromArg[1] = "" + } + + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) + if err != nil { + log.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) + + encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) + if err != nil { + log.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) + + fullEncryptedSecret := models.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: "shared", + } + secretsToUpload = append(secretsToUpload, fullEncryptedSecret) + } + + batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToUpload, + } + err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + if err != nil { + log.Errorf("Unable to complete your request because %v", err) + return + } + + log.Infof("secret name(s) [%v] have been created", strings.Join(args, ", ")) }, } @@ -60,9 +153,65 @@ var secretsDeleteCmd = &cobra.Command{ Use: "delete [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, - Args: cobra.NoArgs, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Delete secret") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + log.Error(err) + return + } + + if !loggedInUserDetails.IsUserLoggedIn { + log.Error("You are not logged in yet. Please run [infisical login] then try again") + return + } + + if loggedInUserDetails.IsUserLoggedIn && loggedInUserDetails.LoginExpired { + log.Error("Your login has expired. Please run [infisical login] then try again") + return + } + + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + secretByKey := getSecretsByKeys(secrets) + validSecretIdsToDelete := []string{} + invalidSecretNamesThatDoNotExist := []string{} + + for _, secretKeyFromArg := range args { + if value, ok := secretByKey[secretKeyFromArg]; ok { + validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) + } else { + invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) + } + } + + if len(invalidSecretNamesThatDoNotExist) != 0 { + log.Errorf("secret name(s) [%v] does not exist in your project. Please remove and re-run the command", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) + return + } + + request := models.BatchDeleteSecretsBySecretIdsRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + SecretIds: validSecretIdsToDelete, + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + err = http.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request) + if err != nil { + log.Errorf("Unable to complete your request because %v", err) + return + } + + log.Infof("secret name(s) [%v] have been deleted from your project", strings.Join(args, ", ")) + }, } @@ -93,11 +242,21 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } else { requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ Key: secretKeyFromArg, - Type: "NOT FOUND", - Value: "NOT FOUND", + Type: "*not found*", + Value: "*not found*", }) } } visualize.PrintAllSecretDetails(requestedSecrets) } + +func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { + secretMapByName := make(map[string]models.SingleEnvironmentVariable) + + for _, secret := range secrets { + secretMapByName[secret.Key] = secret + } + + return secretMapByName +} diff --git a/cli/packages/http/api.go b/cli/packages/http/api.go new file mode 100644 index 000000000..67f8042c2 --- /dev/null +++ b/cli/packages/http/api.go @@ -0,0 +1,82 @@ +package http + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/models" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" +) + +func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchModifySecretsByWorkspaceAndEnvRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch-modify/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Patch(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchCreateSecretsByWorkspaceAndEnvRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch-create/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Post(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request models.BatchDeleteSecretsBySecretIdsRequest) error { + endpoint := fmt.Sprintf("%v/v2/secret/batch/workspace/%v/environment/%v", util.INFISICAL_URL, request.WorkspaceId, request.EnvironmentName) + response, err := httpClient. + R(). + SetBody(request). + Delete(endpoint) + + if err != nil { + return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) + } + + return nil +} + +func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEncryptedWorkspaceKeyRequest) (models.GetEncryptedWorkspaceKeyResponse, error) { + endpoint := fmt.Sprintf("%v/v1/key/%v/latest", util.INFISICAL_URL, request.WorkspaceId) + var result models.GetEncryptedWorkspaceKeyResponse + response, err := httpClient. + R(). + SetResult(&result). + Get(endpoint) + + if err != nil { + return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return models.GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response) + } + + return result, nil +} diff --git a/cli/packages/models/api.go b/cli/packages/models/api.go index 552c7b9bc..5566cdb8b 100644 --- a/cli/packages/models/api.go +++ b/cli/packages/models/api.go @@ -128,3 +128,66 @@ type Workspace struct { V int `json:"__v"` Organization string `json:"organization,omitempty"` } + +type Secret struct { + SecretKeyCiphertext string `json:"secretKeyCiphertext,omitempty"` + SecretKeyIV string `json:"secretKeyIV,omitempty"` + SecretKeyTag string `json:"secretKeyTag,omitempty"` + SecretKeyHash string `json:"secretKeyHash,omitempty"` + SecretValueCiphertext string `json:"secretValueCiphertext,omitempty"` + SecretValueIV string `json:"secretValueIV,omitempty"` + SecretValueTag string `json:"secretValueTag,omitempty"` + SecretValueHash string `json:"secretValueHash,omitempty"` + SecretCommentCiphertext string `json:"secretCommentCiphertext,omitempty"` + SecretCommentIV string `json:"secretCommentIV,omitempty"` + SecretCommentTag string `json:"secretCommentTag,omitempty"` + SecretCommentHash string `json:"secretCommentHash,omitempty"` + Type string `json:"type,omitempty"` + ID string `json:"_id,omitempty"` +} + +type BatchCreateSecretsByWorkspaceAndEnvRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + Secrets []Secret `json:"secrets"` +} + +type BatchModifySecretsByWorkspaceAndEnvRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + Secrets []Secret `json:"secrets"` +} + +type BatchDeleteSecretsBySecretIdsRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` + SecretIds []string `json:"secretIds"` +} + +type GetEncryptedWorkspaceKeyRequest struct { + WorkspaceId string `json:"workspaceId"` +} + +type GetEncryptedWorkspaceKeyResponse struct { + LatestKey struct { + ID string `json:"_id"` + EncryptedKey string `json:"encryptedKey"` + Nonce string `json:"nonce"` + Sender struct { + ID string `json:"_id"` + Email string `json:"email"` + RefreshVersion int `json:"refreshVersion"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + V int `json:"__v"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + PublicKey string `json:"publicKey"` + } `json:"sender"` + Receiver string `json:"receiver"` + Workspace string `json:"workspace"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"latestKey"` +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 8ba1c4627..70484de81 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -18,8 +18,15 @@ type SingleEnvironmentVariable struct { Key string `json:"key"` Value string `json:"value"` Type string `json:"type"` + ID string `json:"_id"` } type WorkspaceConfigFile struct { WorkspaceId string `json:"workspaceId"` } + +type SymmetricEncryptionResult struct { + CipherText []byte + Nonce []byte + AuthTag []byte +} diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 80c98fa9a..bd0e67deb 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -12,6 +12,12 @@ import ( const SERVICE_NAME = "infisical" +type LoggedInUserDetails struct { + IsUserLoggedIn bool + LoginExpired bool + UserCredentials models.UserCredentials +} + // To do: what happens if the user doesn't have a keyring in their system? func StoreUserCredsInKeyRing(userCred *models.UserCredentials) error { userCredMarshalled, err := json.Marshal(userCred) @@ -102,3 +108,50 @@ func IsUserLoggedIn() (hasUserLoggedIn bool, theUsersEmail string, err error) { return false, "", nil } } + +func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { + if ConfigFileExists() { + configFile, err := GetConfigFile() + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get logged in user from config file [err=%s]", err) + } + + if configFile.LoggedInUserEmail == "" { + return LoggedInUserDetails{}, nil + } + + userCreds, err := GetUserCredsFromKeyRing(configFile.LoggedInUserEmail) + if err != nil { + return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to your credentials from Keyring [err=%s]", err) + } + + // check to to see if the JWT is still valid + httpClient := resty.New(). + SetAuthToken(userCreds.JTWToken). + SetHeader("Accept", "application/json") + + response, err := httpClient. + R(). + Post(fmt.Sprintf("%v/v1/auth/checkAuth", INFISICAL_URL)) + + if err != nil { + return LoggedInUserDetails{}, err + } + + if response.StatusCode() > 299 { + return LoggedInUserDetails{ + IsUserLoggedIn: true, + LoginExpired: true, + UserCredentials: userCreds, + }, nil + } + + return LoggedInUserDetails{ + IsUserLoggedIn: true, + LoginExpired: false, + UserCredentials: userCreds, + }, nil + } else { + return LoggedInUserDetails{}, nil + } +} diff --git a/cli/packages/util/crypto.go b/cli/packages/util/crypto.go index 0431989de..23e117f4b 100644 --- a/cli/packages/util/crypto.go +++ b/cli/packages/util/crypto.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "io" + "github.com/Infisical/infisical-merge/packages/models" "golang.org/x/crypto/nacl/box" ) @@ -39,15 +40,15 @@ func GenerateNewKey() (newKey []byte, keyErr error) { } // Will encrypt a plain text with the provided key -func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byte, tag []byte, err error) { +func EncryptSymmetric(plaintext []byte, key []byte) (result models.SymmetricEncryptionResult, err error) { block, err := aes.NewCipher(key) if err != nil { - return nil, nil, nil, err + return models.SymmetricEncryptionResult{}, err } aesgcm, err := cipher.NewGCMWithNonceSize(block, 16) // default is 12, 16 because https://github.com/Infisical/infisical/blob/bea0ff6e05a4de73a5db625d4ae181a015b50855/backend/src/utils/aes-gcm.ts#L4 if err != nil { - return nil, nil, nil, err + return models.SymmetricEncryptionResult{}, err } // create a nonce @@ -62,7 +63,11 @@ func EncryptSymmetric(plaintext []byte, key []byte) (cipherText []byte, iv []byt authTag := ciphertext[len(ciphertext)-16:] - return ciphertextOnly, nonce, authTag, nil + return models.SymmetricEncryptionResult{ + CipherText: ciphertextOnly, + AuthTag: authTag, + Nonce: nonce, + }, nil } func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index f41e73a0f..8d9719698 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -81,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type), + ID: secret.ID, } listOfEnv = append(listOfEnv, env) @@ -192,6 +193,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, Key: string(plainTextKey), Value: string(plainTextValue), Type: string(secret.Type), + ID: secret.ID, } listOfEnv = append(listOfEnv, env) From 01673427228092a467a97eb68f2c18acaf546e5e Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 1 Jan 2023 18:27:31 -0800 Subject: [PATCH 28/91] Improved frontend for activity logs --- .../ee/controllers/v1/workspaceController.ts | 1 + frontend/components/basic/EventFilter.tsx | 52 ++- .../components/basic/table/ActivityTable.tsx | 141 ------- frontend/ee/api/secrets/GetProjectLogs.ts | 43 +++ frontend/ee/api/secrets/GetSecretVersions.ts | 4 +- frontend/ee/components/ActivitySideBar.tsx | 80 ++++ frontend/ee/components/ActivityTable.tsx | 132 +++++++ frontend/ee/utilities/findTextDifferences.ts | 346 ++++++++++++++++++ frontend/ee/utilities/timeSince.ts | 35 ++ frontend/pages/activity/[id].tsx | 207 +++++------ frontend/public/locales/en/activity.json | 7 + frontend/tailwind.config.js | 9 +- 12 files changed, 757 insertions(+), 300 deletions(-) delete mode 100644 frontend/components/basic/table/ActivityTable.tsx create mode 100644 frontend/ee/api/secrets/GetProjectLogs.ts create mode 100644 frontend/ee/components/ActivitySideBar.tsx create mode 100644 frontend/ee/components/ActivityTable.tsx create mode 100644 frontend/ee/utilities/findTextDifferences.ts create mode 100644 frontend/ee/utilities/timeSince.ts create mode 100644 frontend/public/locales/en/activity.json diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index cff7f4a98..2f8020ef0 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -49,6 +49,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { filters.workspace = workspaceId; logs = await Log.find(filters) + .sort({ createdAt: -1 }) .skip(offset) .limit(limit) .populate('actions') diff --git a/frontend/components/basic/EventFilter.tsx b/frontend/components/basic/EventFilter.tsx index 5a9506f0e..d4fe36193 100644 --- a/frontend/components/basic/EventFilter.tsx +++ b/frontend/components/basic/EventFilter.tsx @@ -1,60 +1,50 @@ import React from 'react'; import { Fragment } from 'react'; +import { useTranslation } from "next-i18next"; import { faAngleDown, - faCheck, - faDownload, + faEye, faPlus, - faUpload, + faShuffle, faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Listbox, Transition } from '@headlessui/react'; -import guidGenerator from '../utilities/randomId'; -import Button from './buttons/Button'; - interface ListBoxProps { selected: string; select: (event: string) => void; - data: string[]; - text?: string; - buttonAction?: () => void; - isFull?: boolean; } const eventOptions = [ { - name: 'Secrets Pushed', - icon: faUpload + name: 'addSecrets', + icon: faPlus }, { - name: 'Secrets Pulled', - icon: faDownload + name: 'readSecrets', + icon: faEye + }, + { + name: 'updateSecrets', + icon: faShuffle } ]; /** - * This is the component that we use for drop down lists. + * This is the component that we use for the event picker in the activity logs tab. * @param {object} obj - * @param {string} obj.selected - the item that is currently selected - * @param {function} obj.select - what happends if you select the item inside a list - * @param {string[]} obj.data - all the options available - * @param {string} obj.text - the text that shows us in front of the select option - * @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button - * @param {string} obj.width - button width - * @returns + * @param {string} obj.selected - the event that is currently selected + * @param {function} obj.select - an action that happens when an item is selected */ export default function EventFilter({ selected, - select, - data, - text, - buttonAction, - isFull + select }: ListBoxProps): JSX.Element { + const { t } = useTranslation(); + return ( - +
{selected != '' ? ( @@ -84,9 +74,9 @@ export default function EventFilter({ {({ selected }) => ( <> @@ -96,7 +86,7 @@ export default function EventFilter({ }`} > {' '} - {event.name} + {t("activity:event." + event.name)} )} diff --git a/frontend/components/basic/table/ActivityTable.tsx b/frontend/components/basic/table/ActivityTable.tsx deleted file mode 100644 index 0f5f3eb6b..000000000 --- a/frontend/components/basic/table/ActivityTable.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useRouter } from 'next/router'; -import { - faAngleDown, - faAngleRight, - faX -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; - -import guidGenerator from '../../utilities/randomId'; - -interface ActivityTableProps { - eventName: string; - user: string; - source: string; - time: Date; -} - -function timeSince(date: Date) { - const seconds = Math.floor( - ((new Date() as any) - (date as any)) / 1000 - ) as number; - - let interval = seconds / 31536000; - - if (interval > 1) { - return Math.floor(interval) + ' years ago'; - } - interval = seconds / 2592000; - if (interval > 1) { - return Math.floor(interval) + ' months ago'; - } - interval = seconds / 86400; - if (interval > 1) { - return Math.floor(interval) + ' days ago'; - } - interval = seconds / 3600; - if (interval > 1) { - return Math.floor(interval) + ' hours ago'; - } - interval = seconds / 60; - if (interval > 1) { - return Math.floor(interval) + ' minutes ago'; - } - return Math.floor(seconds) + ' seconds ago'; -} - -const ActivityLogsRow = ({ row }: { row: ActivityTableProps }): JSX.Element => { - const [payloadOpened, setPayloadOpened] = useState(false); - return ( - <> - -
setPayloadOpened(!payloadOpened)} - className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" - > - -
- - {row.eventName} - - - {row.user} - - - {row.source} - - - {timeSince(row.time)} - - {/* -
-
- */} - - {payloadOpened && ( - - -
-
Timestamp
-
2022-12-16T04:02:44.517Z
-
-
-
Number of Secrets
-
32
-
-
-
IP Address
-
159.223.164.24
-
- - - )} - - ); -}; - -/** - * This is the table for activity logs (one of the tabs) - * @param {*} props - * @returns - */ -const ActivityTable = ({ data }: { data: ActivityTableProps[] }) => { - return ( -
-
-
- - - - - - - - - - - - - {data.map((row, index) => { - return ; - })} - -
EventUserSourceTime
-
-
- ); -}; - -export default ActivityTable; diff --git a/frontend/ee/api/secrets/GetProjectLogs.ts b/frontend/ee/api/secrets/GetProjectLogs.ts new file mode 100644 index 000000000..d5324355c --- /dev/null +++ b/frontend/ee/api/secrets/GetProjectLogs.ts @@ -0,0 +1,43 @@ +import SecurityClient from '~/utilities/SecurityClient'; + + +interface workspaceProps { + workspaceId: string; + offset: number; + limit: number; + filters: object; +} + +/** + * This function fetches the activity logs for a certain project + * @param {object} obj + * @param {string} obj.workspaceId - workspace id for which we are trying to get project log + * @param {object} obj.offset - teh starting point of logs that we want to pull + * @param {object} obj.limit - how many logs will we output + * @param {object} obj.filters + * @returns + */ +const getProjectLogs = async ({ workspaceId, offset, limit, filters }: workspaceProps) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/logs?' + + new URLSearchParams({ + offset: String(offset), + limit: String(limit), + filters: JSON.stringify(filters) + }), + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).logs; + } else { + console.log('Failed to get project logs'); + } + }); +}; + +export default getProjectLogs; diff --git a/frontend/ee/api/secrets/GetSecretVersions.ts b/frontend/ee/api/secrets/GetSecretVersions.ts index 8185234e6..7b3840399 100644 --- a/frontend/ee/api/secrets/GetSecretVersions.ts +++ b/frontend/ee/api/secrets/GetSecretVersions.ts @@ -17,7 +17,7 @@ interface secretVersionProps { */ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => { return SecurityClient.fetchCall( - '/api/v1/secret/' + secretId + '/secret-versions?'+ + '/api/v1/secret/' + secretId + '/secret-versions?' + new URLSearchParams({ offset: String(offset), limit: String(limit) @@ -32,7 +32,7 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps if (res && res.status == 200) { return await res.json(); } else { - console.log('Failed to get project secrets'); + console.log('Failed to get secret version history'); } }); }; diff --git a/frontend/ee/components/ActivitySideBar.tsx b/frontend/ee/components/ActivitySideBar.tsx new file mode 100644 index 000000000..d96a3f6ab --- /dev/null +++ b/frontend/ee/components/ActivitySideBar.tsx @@ -0,0 +1,80 @@ +import { useTranslation } from "next-i18next"; +import { faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import patienceDiff from 'ee/utilities/findTextDifferences'; + +import DashboardInputField from '../../components/dashboard/DashboardInputField'; + + +const secretChanges = [{ + "oldSecret": "secret1", + "newSecret": "ecret2" +}, { + "oldSecret": "secret1", + "newSecret": "sercet2" +}, { + "oldSecret": "localhosta:8080", + "newSecret": "aaaalocalhoats:3000" +}] + + +interface SideBarProps { + toggleSidebar: (value: string[]) => void; + sidebarData: string[]; + currentEvent: string; +} + +/** + * @param {object} obj + * @param {function} obj.toggleSidebar - function that opens or closes the sidebar + * @param {string[]} obj.secretIds - data of payload + * @param {string} obj.currentEvent - the event name for which a sidebar is being displayed + * @returns the sidebar with the payload of user activity logs + */ +const ActivitySideBar = ({ + toggleSidebar, + sidebarData, + currentEvent +}: SideBarProps) => { + const { t } = useTranslation(); + + return
+
+
+

{t("activity:event." + currentEvent)}

+
toggleSidebar([])}> + +
+
+
+ {currentEvent == 'readSecrets' && sidebarData.map((item, id) => + <> +
Key {id}
+ {}} + type="varName" + position={1} + value={"a" + item} + isDuplicate={false} + blurred={false} + /> + + )} + {currentEvent == 'updateSecrets' && sidebarData.map((item, id) => + secretChanges.map(secretChange => + <> +
Secret Name {id}
+
+
- {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split(''), false).lines.map((character, id) => character.aIndex != -1 && {character.line})}
+
+ {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split('')).lines.map((character, id) => character.bIndex != -1 && {character.line})}
+
+ + ))} +
+
+ +
+}; + +export default ActivitySideBar; diff --git a/frontend/ee/components/ActivityTable.tsx b/frontend/ee/components/ActivityTable.tsx new file mode 100644 index 000000000..a25920202 --- /dev/null +++ b/frontend/ee/components/ActivityTable.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import { useTranslation } from "next-i18next"; +import { + faAngleDown, + faAngleRight, + faUpRightFromSquare, + faX +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import timeSince from 'ee/utilities/timeSince'; + +import guidGenerator from '../../components/utilities/randomId'; + + +interface PayloadProps { + name: string; + secretVersions: string[]; +} + +interface logData { + _id: string; + channel: string; + createdAt: string; + ipAddress: string; + user: string; + payload: PayloadProps[]; +} + + +/** + * + * @param obj + * @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened + * @returns + */ +const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData, toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => { + const [payloadOpened, setPayloadOpened] = useState(false); + const { t } = useTranslation(); + + return ( + <> + + setPayloadOpened(!payloadOpened)} + className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" + > + + + + {row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")} + + + {row.user} + + + {row.channel} + + + {timeSince(new Date(row.createdAt))} + + + {payloadOpened && + + + Timestamp + {row.createdAt} + } + {payloadOpened && + row.payload?.map((action, index) => + + + {t("activity:event." + action.name)} + { + toggleSidebar(action.secretVersions); + setCurrentEvent(action.name); + }}> + {action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")} + + + )} + {payloadOpened && + + + IP Address + {row.ipAddress} + } + + ); +}; + +/** + * This is the table for activity logs (one of the tabs) + * @param {object} obj + * @param {logData} obj.data - data for user activity logs + * @param {function} obj.toggleSidebar - function that opens or closes the sidebar + * @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened + * @returns + */ +const ActivityTable = ({ data, toggleSidebar, setCurrentEvent }: { data: logData[], toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => { + return ( +
+
+
+ + + + + + + + + + + + + {data?.map((row, index) => { + return ; + })} + +
EventUserSourceTime
+
+
+ ); +}; + +export default ActivityTable; diff --git a/frontend/ee/utilities/findTextDifferences.ts b/frontend/ee/utilities/findTextDifferences.ts new file mode 100644 index 000000000..ffd847a77 --- /dev/null +++ b/frontend/ee/utilities/findTextDifferences.ts @@ -0,0 +1,346 @@ +/** + * + * @param textOld - old secret + * @param textNew - new (updated) secret + * @param diffPlusFlag - a flag for whether we want to detect moving segments + * - doesn't work in some examples (e.g., when we have a full reverse ordering of the text) + * @returns + */ +function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) { + + /** + * findUnique finds all unique values in arr[lo..hi], inclusive. This + * function is used in preparation for determining the longest common + * subsequence. Specifically, it first reduces the array range in question + * to unique values. + * @param chars - an array of characters + * @param lo + * @param hi + * @returns - an ordered Map, with the arr[i] value as the Map key and the + * array index i as the Map value. + */ + function findUnique(chars: string[], lo: number, hi: number) { + const characterMap = new Map(); + + for (let i=lo; i<=hi; i++) { + const character = chars[i]; + + if (characterMap.has(character)) { + characterMap.get(character).count++; + characterMap.get(character).index = i; + } else { + characterMap.set(character, { count: 1, index: i }); + } + } + + characterMap.forEach((val, key, map) => { + if (val.count !== 1) { + map.delete(key); + } else { + map.set(key, val.index); + } + }); + + return characterMap; + } + + /** + * @param aArray + * @param aLo + * @param aHi + * @param bArray + * @param bLo + * @param bHi + * @returns an ordered Map, with the Map key as the common line between aArray + * and bArray, with the Map value as an object containing the array indexes of + * the matching unique lines. + * + */ + function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) { + const ma = findUnique(aArray, aLo, aHi); + const mb = findUnique(bArray, bLo, bHi); + + ma.forEach((val, key, map) => { + if (mb.has(key)) { + map.set(key, { + indexA: val, + indexB: mb.get(key) + }); + } else { + map.delete(key); + } + }); + + return ma; + } + + /** + * longestCommonSubsequence takes an ordered Map from the function uniqueCommon + * and determines the Longest Common Subsequence (LCS). + * @param abMap + * @returns an ordered array of objects containing the array indexes of the + * matching lines for a LCS. + */ + function longestCommonSubsequence(abMap: Map) { + const ja: any = []; + + // First, walk the list creating the jagged array. + abMap.forEach((val, key, map) => { + let i = 0; + + while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) { + i++; + } + + if (!ja[i]) { + ja[i] = []; + } + + if (0 < i) { + val.prev = ja[i-1][ja[i - 1].length - 1]; + } + ja[i].push(val); + }); + + // Now, pull out the longest common subsequence. + let lcs: any[] = []; + + if (0 < ja.length) { + const n = ja.length - 1; + lcs = [ja[n][ja[n].length - 1]]; + + while (lcs[lcs.length - 1].prev) { + lcs.push(lcs[lcs.length - 1].prev); + } + } + + return lcs.reverse(); + } + + // "result" is the array used to accumulate the textOld that are deleted, the + // lines that are shared between textOld and textNew, and the textNew that were + // inserted. + + const result: any[] = []; + let deleted = 0; + let inserted = 0; + + // aMove and bMove will contain the lines that don't match, and will be returned + // for possible searching of lines that moved. + + const aMove: any[] = []; + const aMoveIndex: any[] = []; + const bMove: any[] = []; + const bMoveIndex: any[] = []; + + /** + * addToResult simply pushes the latest value onto the "result" array. This + * array captures the diff of the line, aIndex, and bIndex from the textOld + * and textNew array. + * @param aIndex + * @param bIndex + */ + function addToResult(aIndex: number, bIndex: number) { + if (bIndex < 0) { + aMove.push(textOld[aIndex]); + aMoveIndex.push(result.length); + deleted++; + } else if (aIndex < 0) { + bMove.push(textNew[bIndex]); + bMoveIndex.push(result.length); + inserted++; + } + + result.push({ + line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex], + aIndex: aIndex, + bIndex: bIndex, + }); + } + + /** + * addSubMatch handles the lines between a pair of entries in the LCS. Thus, + * this function might recursively call recurseLCS to further match the lines + * between textOld and textNew. + * @param aLo + * @param aHi + * @param bLo + * @param bHi + */ + function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) { + // Match any lines at the beginning of textOld and textNew. + while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) { + addToResult(aLo++, bLo++); + } + + // Match any lines at the end of textOld and textNew, but don't place them + // in the "result" array just yet, as the lines between these matches at + // the beginning and the end need to be analyzed first. + + const aHiTemp = aHi; + while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) { + aHi--; + bHi--; + } + + // Now, check to determine with the remaining lines in the subsequence + // whether there are any unique common lines between textOld and textNew. + // + // If not, add the subsequence to the result (all textOld having been + // deleted, and all textNew having been inserted). + // + // If there are unique common lines between textOld and textNew, then let's + // recursively perform the patience diff on the subsequence. + + const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi); + + if (uniqueCommonMap.size === 0) { + while (aLo <= aHi) { + addToResult(aLo++, -1); + } + + while (bLo <= bHi) { + addToResult(-1, bLo++); + } + } else { + recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap); + } + + // Finally, let's add the matches at the end to the result. + while (aHi < aHiTemp) { + addToResult(++aHi, ++bHi); + } + } + + /** + * recurseLCS finds the longest common subsequence (LCS) between the arrays + * textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence + * recursively performs another LCS search (via addSubMatch), until there are + * none found, at which point the subsequence is dumped to the result. + * @param aLo + * @param aHi + * @param bLo + * @param bHi + * @param uniqueCommonMap + */ + function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) { + const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi)); + + if (x.length === 0) { + addSubMatch(aLo, aHi, bLo, bHi); + } else { + if (aLo < x[0].indexA || bLo < x[0].indexB) { + addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1); + } + + let i; + for (i = 0; i < x.length - 1; i++) { + addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1); + } + + if (x[i].indexA <= aHi || x[i].indexB <= bHi) { + addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi); + } + } + } + + recurseLCS(0, textOld.length - 1, 0, textNew.length - 1); + + if (diffPlusFlag) { + return { + lines: result, + lineCountDeleted: deleted, + lineCountInserted: inserted, + lineCountMoved: 0, + aMove: aMove, + aMoveIndex: aMoveIndex, + bMove: bMove, + bMoveIndex: bMoveIndex, + }; + } + + return { + lines: result, + lineCountDeleted: deleted, + lineCountInserted: inserted, + lineCountMoved: 0, + }; +} + +/** + * use: patienceDiffPlus( textOld[], textNew[] ) + * + * where: + * textOld[] contains the original text lines. + * textNew[] contains the new text lines. + * + * returns an object with the following properties: + * lines[] with properties of: + * line containing the line of text from textOld or textNew. + * aIndex referencing the index in aLine[]. + * bIndex referencing the index in textNew[]. + * (Note: The line is text from either textOld or textNew, with aIndex and bIndex + * referencing the original index. If aIndex === -1 then the line is new from textNew, + * and if bIndex === -1 then the line is old from textOld.) + * moved is true if the line was moved from elsewhere in textOld[] or textNew[]. + * lineCountDeleted is the number of lines from textOld[] not appearing in textNew[]. + * lineCountInserted is the number of lines from textNew[] not appearing in textOld[]. + * lineCountMoved is the number of lines that moved. + */ + +function patienceDiffPlus(textOld: string[], textNew: string[]) { + + const difference = patienceDiff(textOld, textNew, true); + + let aMoveNext = difference.aMove; + let aMoveIndexNext = difference.aMoveIndex; + let bMoveNext = difference.bMove; + let bMoveIndexNext = difference.bMoveIndex; + + delete difference.aMove; + delete difference.aMoveIndex; + delete difference.bMove; + delete difference.bMoveIndex; + + let lastLineCountMoved; + + do { + const aMove = aMoveNext; + const aMoveIndex = aMoveIndexNext; + const bMove = bMoveNext; + const bMoveIndex = bMoveIndexNext; + + aMoveNext = []; + aMoveIndexNext = []; + bMoveNext = []; + bMoveIndexNext = []; + + const subDiff = patienceDiff(aMove!, bMove!); + + lastLineCountMoved = difference.lineCountMoved; + + subDiff.lines.forEach((v, i) => { + + if (0 <= v.aIndex && 0 <= v.bIndex) { + + difference.lines[aMoveIndex![v.aIndex]].moved = true; + difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex]; + difference.lines[bMoveIndex![v.bIndex]].moved = true; + difference.lineCountInserted--; + difference.lineCountDeleted--; + difference.lineCountMoved++; + } else if (v.bIndex < 0) { + aMoveNext!.push(aMove![v.aIndex]); + aMoveIndexNext!.push(aMoveIndex![v.aIndex]); + } else { + bMoveNext!.push(bMove![v.bIndex]); + bMoveIndexNext!.push(bMoveIndex![v.bIndex]); + } + }); + } while (0 < difference.lineCountMoved - lastLineCountMoved); + + return difference; + +} + +export default patienceDiff; diff --git a/frontend/ee/utilities/timeSince.ts b/frontend/ee/utilities/timeSince.ts new file mode 100644 index 000000000..b79249967 --- /dev/null +++ b/frontend/ee/utilities/timeSince.ts @@ -0,0 +1,35 @@ +/** + * Time since a certain date + * @param {Date} date - the timestamp got which we want to understand how long ago it happened + * @returns {String} text - how much time has passed since a certain timestamp + */ +function timeSince(date: Date) { + const seconds = Math.floor( + ((new Date() as any) - (date as any)) / 1000 + ) as number; + + let interval = seconds / 31536000; + + if (interval > 1) { + return Math.floor(interval) + ' years ago'; + } + interval = seconds / 2592000; + if (interval > 1) { + return Math.floor(interval) + ' months ago'; + } + interval = seconds / 86400; + if (interval > 1) { + return Math.floor(interval) + ' days ago'; + } + interval = seconds / 3600; + if (interval > 1) { + return Math.floor(interval) + ' hours ago'; + } + interval = seconds / 60; + if (interval > 1) { + return Math.floor(interval) + ' minutes ago'; + } + return Math.floor(seconds) + ' seconds ago'; +} + +export default timeSince; diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index 9b51d7f74..f384ae69f 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -1,137 +1,90 @@ import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; +import { useTranslation } from "next-i18next"; +import ActivitySideBar from 'ee/components/ActivitySideBar'; +import Button from '~/components/basic/buttons/Button'; import EventFilter from '~/components/basic/EventFilter'; -import ActivityTable from '~/components/basic/table/ActivityTable'; import NavHeader from '~/components/navigation/NavHeader'; -import onboardingCheck from '~/components/utilities/checks/OnboardingCheck'; +import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps'; -const data = [ - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - }, - { - eventName: 'Secrets Pulled', - user: 'matsiiako@gmail.com', - source: 'CLI', - time: new Date() - }, - { - eventName: 'Secrets Pushed', - user: 'matsiiako@gmail.com', - source: 'Web', - time: new Date() - } -]; +import getProjectLogs from '../../ee/api/secrets/GetProjectLogs'; +import ActivityTable from '../../ee/components/ActivityTable'; + + +interface logData { + _id: string; + channel: string; + createdAt: string; + ipAddress: string; + user: { + email: string; + }; + actions: { + name: string; + payload: { + secretVersions: string[]; + } + }[] +} + +interface PayloadProps { + name: string; + secretVersions: string[]; +} + +interface logDataPoint { + _id: string; + channel: string; + createdAt: string; + ipAddress: string; + user: string; + payload: PayloadProps[]; +} /** - * This tab is called Home because in the future it will include some company news, - * updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction - * for the new users + * This is the tab that includes all of the user activity logs */ export default function Activity() { const router = useRouter(); - const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); - const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); - const [hasUserStarred, setHasUserStarred] = useState(false); - const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); - const [usersInOrg, setUsersInOrg] = useState(false); const [eventChosen, setEventChosen] = useState(''); + const [logsData, setLogsData] = useState([]); + const [currentOffset, setCurrentOffset] = useState(0); + const currentLimit = 10; + const [sidebarData, toggleSidebar] = useState([]) + const [currentEvent, setCurrentEvent] = useState(""); + const { t } = useTranslation(); useEffect(() => { - onboardingCheck({ - setHasUserClickedIntro, - setHasUserClickedSlack, - setHasUserPushedSecrets, - setHasUserStarred, - setUsersInOrg - }); - }, []); + const getLogData = async () => { + const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, filters: {} }) + setLogsData(logsData.concat(tempLogsData.map((log: logData) => { + return { + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log.user.email, + payload: log.actions.map(action => { + return { + name: action.name, + secretVersions: action.payload.secretVersions + } + }) + } + }))) + } + getLogData(); + }, [currentLimit, currentOffset]); + + const loadMoreLogs = () => { + setCurrentOffset(currentOffset + currentLimit); + } return (
+ {sidebarData.length > 0 && }

Activity Logs

@@ -140,22 +93,32 @@ export default function Activity() { Event history limited to the last 12 months.

- {/* Licence Required
-
*/} +
- eventChosen != '' ? event.eventName == eventChosen : event - )} + data={logsData!.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .filter((log) => + eventChosen != '' ? log.payload?.map(action => t("activity:event." + action.name)).includes(eventChosen) : true + ) + } + toggleSidebar={toggleSidebar} + setCurrentEvent={setCurrentEvent} /> +
+
+
+
); } Activity.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(["activity"]); \ No newline at end of file diff --git a/frontend/public/locales/en/activity.json b/frontend/public/locales/en/activity.json new file mode 100644 index 000000000..b84d570ae --- /dev/null +++ b/frontend/public/locales/en/activity.json @@ -0,0 +1,7 @@ +{ + "event": { + "readSecrets": "Secrets Viewed", + "updateSecrets": "Secrets Updated", + "addSecrets": "Secrets Added" + } +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 6f3d038cf..81e97e6ed 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1238,6 +1238,7 @@ module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", + "./ee/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { @@ -1382,12 +1383,12 @@ module.exports = { "0%": { transform: "scale(0.2)", opacity: 0, - transform: "translateY(120%)", + // transform: "translateY(120%)", }, "100%": { transform: "scale(1)", opacity: 1, - transform: "translateY(100%)", + // transform: "translateY(100%)", }, }, popright: { @@ -1410,12 +1411,12 @@ module.exports = { "0%": { transform: "scale(0.2)", opacity: 0, - transform: "translateY(80%)", + // transform: "translateY(80%)", }, "100%": { transform: "scale(1)", opacity: 1, - transform: "translateY(100%)", + // transform: "translateY(100%)", }, }, }, From a8f0c391bc4d38c7eddfe7e486a8c936ed5a71d0 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 2 Jan 2023 14:18:49 +0700 Subject: [PATCH 29/91] Finish v1 audit logs, secret versioning, version all unversioned secrets --- backend/src/app.ts | 4 +- .../src/controllers/v2/workspaceController.ts | 2 - .../src/ee/controllers/v1/actionController.ts | 27 ++- .../ee/controllers/v1/workspaceController.ts | 17 +- backend/src/ee/helpers/action.ts | 112 ++++++++++ backend/src/ee/helpers/log.ts | 1 + backend/src/ee/helpers/secret.ts | 29 ++- backend/src/ee/models/action.ts | 10 +- backend/src/ee/models/log.ts | 20 +- backend/src/ee/routes/v1/workspace.ts | 1 - backend/src/ee/services/EELogService.ts | 36 +++- backend/src/ee/services/EESecretService.ts | 25 ++- backend/src/helpers/database.ts | 78 +++++++ backend/src/helpers/secret.ts | 199 ++++-------------- backend/src/index.ts | 4 +- backend/src/models/user.ts | 3 +- backend/src/services/DatabaseService.ts | 16 ++ backend/src/services/database.ts | 10 - backend/src/services/index.ts | 2 + 19 files changed, 401 insertions(+), 195 deletions(-) create mode 100644 backend/src/ee/helpers/action.ts create mode 100644 backend/src/helpers/database.ts create mode 100644 backend/src/services/DatabaseService.ts delete mode 100644 backend/src/services/database.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 8320a7d76..9fba18c67 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -13,7 +13,8 @@ import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, - secret as eeSecretRouter + secret as eeSecretRouter, + action as eeActionRouter } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -69,6 +70,7 @@ if (NODE_ENV === 'production') { // (EE) routes app.use('/api/v1/secret', eeSecretRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); +app.use('/api/v1/action', eeActionRouter); // v1 routes app.use('/api/v1/signup', v1SignupRouter); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index cfe1f239e..0aafcd525 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -369,7 +369,6 @@ export const getWorkspaceServiceTokens = async ( */ 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; @@ -400,7 +399,6 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { keys }); - if (postHogClient) { postHogClient.capture({ event: 'secrets pushed', diff --git a/backend/src/ee/controllers/v1/actionController.ts b/backend/src/ee/controllers/v1/actionController.ts index 20b470037..b136b0fa4 100644 --- a/backend/src/ee/controllers/v1/actionController.ts +++ b/backend/src/ee/controllers/v1/actionController.ts @@ -1,18 +1,29 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; -import { Action } from '../../models'; +import { Action, SecretVersion } from '../../models'; +import { ActionNotFoundError } from '../../../utils/errors'; -export const getAction = (req: Request, res: Response) => { +export const getAction = async (req: Request, res: Response) => { let action; - // try { - // const { actionId } = req.params; + try { + const { actionId } = req.params; - // action = await Action.findById(actionId); + action = await Action + .findById(actionId) + .populate([ + 'payload.secretVersions.oldSecretVersion', + 'payload.secretVersions.newSecretVersion' + ]); - - // } catch (err) { + if (!action) throw ActionNotFoundError({ + message: 'Failed to find action' + }); - // } + } catch (err) { + throw ActionNotFoundError({ + message: 'Failed to find action' + }); + } return res.status(200).send({ action diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index cff7f4a98..1df50b18b 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -41,14 +41,23 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { let logs try { const { workspaceId } = req.params; + const { userId, actionNames } = req.query; const offset: number = parseInt(req.query.offset as string); const limit: number = parseInt(req.query.limit as string); - const filters: any = req.query.filters || {}; - filters.workspace = workspaceId; - - logs = await Log.find(filters) + logs = await Log.find({ + workspace: workspaceId, + ...( userId ? { user: userId } : {}), + ...( + actionNames + ? { + actionNames: { + $in: actionNames + } + } : {} + ) + }) .skip(offset) .limit(limit) .populate('actions') diff --git a/backend/src/ee/helpers/action.ts b/backend/src/ee/helpers/action.ts new file mode 100644 index 000000000..2971e3f96 --- /dev/null +++ b/backend/src/ee/helpers/action.ts @@ -0,0 +1,112 @@ +import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; +import { Secret } from '../../models'; +import { SecretVersion, Action } from '../models'; +import { ACTION_UPDATE_SECRETS } from '../../variables'; + +/** + * Create an (audit) action for secrets including + * add, delete, update, and read actions. + * @param {Object} obj + * @param {String} obj.name - name of action + * @param {ObjectId[]} obj.secretIds - ids of relevant secrets + * @returns {Action} action - new action + */ +const createActionSecretHelper = async ({ + name, + userId, + workspaceId, + secretIds +}: { + name: string; + userId: string; + workspaceId: string; + secretIds: Types.ObjectId[]; +}) => { + + let action; + let latestSecretVersions; + try { + if (name === ACTION_UPDATE_SECRETS) { + // case: action is updating secrets + // -> add old and new secret versions + + // TODO: make query more efficient + latestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds, + }, + }, + }, + { + $sort: { version: -1 }, + }, + { + $group: { + _id: "$secret", + versions: { $push: "$$ROOT" }, + }, + }, + { + $project: { + _id: 0, + secret: "$_id", + versions: { $slice: ["$versions", 2] }, + }, + } + ])) + .map((s) => ({ + oldSecretVersion: s.versions[0]._id, + newSecretVersion: s.versions[1]._id + })); + + + } else { + // case: action is adding, deleting, or reading secrets + // -> add new secret versions + latestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds + } + } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' }, + versionId: { $max: '$_id' } // secret version id + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => ({ + newSecretVersion: s.versionId + })); + } + + action = await new Action({ + name, + user: userId, + workspace: workspaceId, + payload: { + secretVersions: latestSecretVersions + } + }).save(); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to create action'); + } + + return action; +} + +export { createActionSecretHelper }; \ No newline at end of file diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts index a075cc91f..c357c9818 100644 --- a/backend/src/ee/helpers/log.ts +++ b/backend/src/ee/helpers/log.ts @@ -22,6 +22,7 @@ const createLogHelper = async ({ log = await new Log({ user: userId, workspace: workspaceId, + actionNames: actions.map((a) => a.name), actions, channel, ipAddress diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index a688a108f..8cd59d8d6 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -1,3 +1,4 @@ +import { Types } from 'mongoose'; import * as Sentry from '@sentry/node'; import { Secret @@ -59,16 +60,40 @@ const addSecretVersionsHelper = async ({ }: { secretVersions: ISecretVersion[] }) => { + let newSecretVersions; try { - await SecretVersion.insertMany(secretVersions); + newSecretVersions = await SecretVersion.insertMany(secretVersions); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); throw new Error('Failed to add secret versions'); } + + return newSecretVersions; +} + +const markDeletedSecretVersionsHelper = async ({ + secretIds +}: { + secretIds: Types.ObjectId[]; +}) => { + try { + await SecretVersion.updateMany({ + secret: { $in: secretIds } + }, { + isDeleted: true + }, { + new: true + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to mark secret versions as deleted'); + } } export { takeSecretSnapshotHelper, - addSecretVersionsHelper + addSecretVersionsHelper, + markDeletedSecretVersionsHelper } \ No newline at end of file diff --git a/backend/src/ee/models/action.ts b/backend/src/ee/models/action.ts index 8ffdfe4bb..3d48aa04d 100644 --- a/backend/src/ee/models/action.ts +++ b/backend/src/ee/models/action.ts @@ -26,8 +26,14 @@ const actionSchema = new Schema( }, payload: { secretVersions: [{ - type: Schema.Types.ObjectId, - ref: 'SecretVersion' + oldSecretVersion: { + type: Schema.Types.ObjectId, + ref: 'SecretVersion' + }, + newSecretVersion: { + type: Schema.Types.ObjectId, + ref: 'SecretVersion' + } }] } }, { diff --git a/backend/src/ee/models/log.ts b/backend/src/ee/models/log.ts index abfadb223..1fdd52710 100644 --- a/backend/src/ee/models/log.ts +++ b/backend/src/ee/models/log.ts @@ -1,9 +1,16 @@ import { Schema, model, Types } from 'mongoose'; +import { + ACTION_ADD_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_READ_SECRETS, + ACTION_DELETE_SECRETS +} from '../../variables'; export interface ILog { _id: Types.ObjectId; user?: Types.ObjectId; workspace?: Types.ObjectId; + actionNames: string[]; actions: Types.ObjectId[]; channel: string; ipAddress?: string; @@ -19,9 +26,20 @@ const logSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Workspace' }, + actionNames: { + type: [String], + enum: [ + ACTION_ADD_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_READ_SECRETS, + ACTION_DELETE_SECRETS + ], + required: true + }, actions: [{ type: Schema.Types.ObjectId, - ref: 'Action' + ref: 'Action', + required: true }], channel: { type: String, diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 5deb7dd85..e9e6938bf 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -33,7 +33,6 @@ router.get( param('workspaceId').exists().trim(), query('offset').exists().isInt(), query('limit').exists().isInt(), - query('filters').exists(), validateRequest, workspaceController.getWorkspaceLogs ); diff --git a/backend/src/ee/services/EELogService.ts b/backend/src/ee/services/EELogService.ts index 7e6ca4acf..c1b2da6fb 100644 --- a/backend/src/ee/services/EELogService.ts +++ b/backend/src/ee/services/EELogService.ts @@ -1,10 +1,15 @@ +import { Types } from 'mongoose'; import { + Log, Action, IAction } from '../models'; import { createLogHelper } from '../helpers/log'; +import { + createActionSecretHelper +} from '../helpers/action'; import EELicenseService from './EELicenseService'; /** @@ -19,6 +24,7 @@ class EELogService { * @param {Action} obj.actions - actions to include in log * @param {String} obj.channel - channel (web/cli/auto) associated with the log * @param {String} obj.ipAddress - ip address associated with the log + * @returns {Log} log - new audit log */ static async createLog({ userId, @@ -33,7 +39,7 @@ class EELogService { channel: string; ipAddress: string; }) { - if (!EELicenseService.isLicenseValid) return; + if (!EELicenseService.isLicenseValid) return null; return await createLogHelper({ userId, workspaceId, @@ -42,6 +48,34 @@ class EELogService { ipAddress }) } + + /** + * Create an (audit) action for secrets including + * add, delete, update, and read actions. + * @param {Object} obj + * @param {String} obj.name - name of action + * @param {ObjectId[]} obj.secretIds - secret ids + * @returns {Action} action - new action + */ + static async createActionSecret({ + name, + userId, + workspaceId, + secretIds + }: { + name: string; + userId: string; + workspaceId: string; + secretIds: Types.ObjectId[]; + }) { + if (!EELicenseService.isLicenseValid) return null; + return await createActionSecretHelper({ + name, + userId, + workspaceId, + secretIds + }); + } } export default EELogService; \ No newline at end of file diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts index 643f763f1..64aa81af0 100644 --- a/backend/src/ee/services/EESecretService.ts +++ b/backend/src/ee/services/EESecretService.ts @@ -1,7 +1,9 @@ +import { Types } from 'mongoose'; import { ISecretVersion } from '../models'; import { takeSecretSnapshotHelper, - addSecretVersionsHelper + addSecretVersionsHelper, + markDeletedSecretVersionsHelper } from '../helpers/secret'; import EELicenseService from './EELicenseService'; @@ -28,7 +30,7 @@ class EESecretService { } /** - * Adds secret versions [secretVersions] to the SecretVersion collection. + * Add secret versions [secretVersions] to the SecretVersion collection. * @param {Object} obj * @param {SecretVersion} obj.secretVersions */ @@ -38,10 +40,27 @@ class EESecretService { secretVersions: ISecretVersion[]; }) { if (!EELicenseService.isLicenseValid) return; - await addSecretVersionsHelper({ + return await addSecretVersionsHelper({ secretVersions }); } + + /** + * Mark secret versions associated with secrets with ids [secretIds] + * as deleted. + * @param {Object} obj + * @param {ObjectId[]} obj.secretIds - secret ids + */ + static async markDeletedSecretVersions({ + secretIds + }: { + secretIds: Types.ObjectId[]; + }) { + if (!EELicenseService.isLicenseValid) return; + await markDeletedSecretVersionsHelper({ + secretIds + }); + } } export default EESecretService; \ No newline at end of file diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts new file mode 100644 index 000000000..7a96e2ab5 --- /dev/null +++ b/backend/src/helpers/database.ts @@ -0,0 +1,78 @@ +import mongoose from 'mongoose'; +import { ISecret, Secret } from '../models'; +import { EESecretService } from '../ee/services'; +import { getLogger } from '../utils/logger'; + +/** + * Initialize database connection + * @param {Object} obj + * @param {String} obj.mongoURL - mongo connection string + * @returns + */ +const initDatabaseHelper = async ({ + mongoURL +}: { + mongoURL: string; +}) => { + try { + await mongoose.connect(mongoURL); + getLogger("database").info("Database connection established"); + + await prepareDatabase(); + } catch (err) { + getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`); + } + + return mongoose.connection; +} + +/** + * Prepare database by: + * - Setting unversioned secrets to version 1 + * - Initializing secret versions for unversioned secrets + */ +const prepareDatabase = async () => { + try { + // set previously unversioned secrets in Secret to version 1 + await Secret.updateMany( + { version: { $exists: false } }, + { $set: { version: 1 } } + ); + + // initialize secret versions for unversioned secrets + const unversionedSecrets: ISecret[] = await Secret.aggregate([ + { + $lookup: { + from: 'secretversions', + localField: '_id', + foreignField: 'secret', + as: 'versions', + }, + }, + { + $match: { + versions: { $size: 0 }, + }, + }, + ]); + + if (unversionedSecrets.length > 0) { + await EESecretService.addSecretVersions({ + secretVersions: unversionedSecrets.map((s, idx) => ({ + ...s, + secret: s._id, + version: s.version ? s.version : 1, + isDeleted: false, + workspace: s.workspace, + environment: s.environment + })) + }); + } + } catch (err) { + getLogger('database').error('Failed to prepare database'); + } +} + +export { + initDatabaseHelper +} \ No newline at end of file diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 93cbb648a..61fc25174 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -285,6 +285,9 @@ const v1PushSecrets = async ({ } }; +// TODO: optimize this route. +// TODO: ensure that it's possible to query for and filter logs + /** * Push secrets for user with id [userId] to workspace * with id [workspaceId] with environment [environment]. Follow steps: @@ -341,42 +344,19 @@ const v1PushSecrets = async ({ await Secret.deleteMany({ _id: { $in: toDelete } }); + + await EESecretService.markDeletedSecretVersions({ + secretIds: toDelete + }); - await SecretVersion.updateMany({ - secret: { $in: toDelete } - }, { - isDeleted: true - }, { - new: true + const deleteAction = await EELogService.createActionSecret({ + name: ACTION_DELETE_SECRETS, + userId, + workspaceId, + secretIds: toDelete }); - // add audit log for deleted secrets - const deletedLatestSecretVersions = (await SecretVersion.aggregate([ - { - $match: { secret: { $in: toDelete } } - }, - { - $group: { - _id: '$secret', - version: { $max: '$version' } - } - }, - { - $sort: { version: -1 } - } - ]) - .exec()) - .map((s) => s._id); - - const deleteAction = await new Action({ - name: ACTION_DELETE_SECRETS, - user: new Types.ObjectId(userId), - workspace: new Types.ObjectId(workspaceId), - payload: { - secretVersions: deletedLatestSecretVersions - } - }).save(); - actions.push(deleteAction); + deleteAction && actions.push(deleteAction); } const toUpdate = oldSecrets @@ -459,10 +439,6 @@ const v1PushSecrets = async ({ secretValueIV, secretValueTag, secretValueHash, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentHash, } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; return ({ @@ -481,34 +457,14 @@ const v1PushSecrets = async ({ }) }); - // add audit log for updated secrets - const updatedLatestSecretVersions = (await SecretVersion.aggregate([ - { - $match: { secret: { $in: toUpdate.map((u) => u._id) } } - }, - { - $group: { - _id: '$secret', - version: { $max: '$version' } - } - }, - { - $sort: { version: -1 } - } - ]) - .exec()) - .map((s) => s._id); - - const updateAction = await new Action({ + const updateAction = await EELogService.createActionSecret({ name: ACTION_UPDATE_SECRETS, - user: new Types.ObjectId(userId), - workspace: new Types.ObjectId(workspaceId), - payload: { - secretVersions: updatedLatestSecretVersions - } - }).save(); + userId, + workspaceId, + secretIds: toUpdate.map((u) => u._id) + }); - actions.push(updateAction); + updateAction && actions.push(updateAction); } // handle adding new secrets @@ -517,45 +473,14 @@ const v1PushSecrets = async ({ if (toAdd.length > 0) { // add secrets const newSecrets = await Secret.insertMany( - toAdd.map(({ - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentHash, - }, idx) => { - const obj: any = { - version: 1, - workspace: workspaceId, - type: toAdd[idx].type, - environment, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentHash - }; - - if (toAdd[idx].type === 'personal') { - obj['user' as keyof typeof obj] = userId; - } - - return obj; - }) + toAdd.map((s, idx) => ({ + ...s, + version: 1, + workspace: workspaceId, + type: toAdd[idx].type, + environment, + ...( toAdd[idx].type === 'personal' ? { user: userId } : {}) + })) ); // (EE) add secret versions for new secrets @@ -584,35 +509,14 @@ const v1PushSecrets = async ({ secretValueHash })) }); - - // add audit log for new secrets - const newLatestSecretVersions = (await SecretVersion.aggregate([ - { - $match: { secret: { $in: newSecrets.map((n) => n._id) } } - }, - { - $group: { - _id: '$secret', - version: { $max: '$version' } - } - }, - { - $sort: { version: -1 } - } - ]) - .exec()) - .map((s) => s._id); - const addAction = await new Action({ + const addAction = await EELogService.createActionSecret({ name: ACTION_ADD_SECRETS, - user: new Types.ObjectId(userId), - workspace: new Types.ObjectId(workspaceId), - payload: { - secretVersions: newLatestSecretVersions - } - }).save(); - - actions.push(addAction); + userId, + workspaceId, + secretIds: newSecrets.map((n) => n._id) + }); + addAction && actions.push(addAction); } // (EE) take a secret snapshot @@ -620,6 +524,7 @@ const v1PushSecrets = async ({ workspaceId }) + // (EE) create (audit) log if (actions.length > 0) { await EELogService.createLog({ userId, @@ -637,7 +542,7 @@ const v1PushSecrets = async ({ }; /** - * Pull secrets for user with id [userId] for workspace + * Get secrets for user with id [userId] for workspace * with id [workspaceId] with environment [environment] * @param {Object} obj * @param {String} obj.userId -id of user to pull secrets for @@ -704,7 +609,7 @@ const pullSecrets = async ({ channel: string; ipAddress: string; }): Promise => { - let secrets: any; // TODO: FIX any + let secrets: any; try { secrets = await getSecrets({ @@ -712,35 +617,15 @@ const pullSecrets = async ({ workspaceId, environment }) - - // add audit log for new secrets - const readLatestSecretVersions = (await SecretVersion.aggregate([ - { - $match: { secret: { $in: secrets.map((n: any) => n._id) } } - }, - { - $group: { - _id: '$secret', - version: { $max: '$version' } - } - }, - { - $sort: { version: -1 } - } - ]) - .exec()) - .map((s) => s._id); - const readAction = await new Action({ + const readAction = await EELogService.createActionSecret({ name: ACTION_READ_SECRETS, - user: new Types.ObjectId(userId), - workspace: new Types.ObjectId(workspaceId), - payload: { - secretVersions: readLatestSecretVersions - } - }).save(); + userId, + workspaceId, + secretIds: secrets.map((n: any) => n._id) + }); - await EELogService.createLog({ + readAction && await EELogService.createLog({ userId, workspaceId, actions: [readAction], diff --git a/backend/src/index.ts b/backend/src/index.ts index d182c2655..bc07ec3b9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,12 +4,12 @@ dotenv.config(); import * as Sentry from '@sentry/node'; import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config'; import { server } from './app'; -import { initDatabase } from './services/database'; +import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; import { initSmtp } from './services/smtp'; import { setTransporter } from './helpers/nodemailer'; -initDatabase(MONGO_URL); +DatabaseService.initDatabase(MONGO_URL); setUpHealthEndpoint(server); diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 3b0aa1862..7ea988c9d 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -52,7 +52,8 @@ const userSchema = new Schema( }, refreshVersion: { type: Number, - default: 0 + default: 0, + select: false } }, { diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts new file mode 100644 index 000000000..2e8dc839f --- /dev/null +++ b/backend/src/services/DatabaseService.ts @@ -0,0 +1,16 @@ +import mongoose from 'mongoose'; +import { getLogger } from '../utils/logger'; +import { initDatabaseHelper } from '../helpers/database'; + +/** + * Class to handle database actions + */ +class DatabaseService { + static async initDatabase(MONGO_URL: string) { + return await initDatabaseHelper({ + mongoURL: MONGO_URL + }); + } +} + +export default DatabaseService; \ No newline at end of file diff --git a/backend/src/services/database.ts b/backend/src/services/database.ts deleted file mode 100644 index 85f39c1b2..000000000 --- a/backend/src/services/database.ts +++ /dev/null @@ -1,10 +0,0 @@ -import mongoose from 'mongoose'; -import { getLogger } from '../utils/logger'; - -export const initDatabase = (MONGO_URL: string) => { - mongoose - .connect(MONGO_URL) - .then(() => getLogger("database").info("Database connection established")) - .catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`)); - return mongoose.connection; -}; diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index 531033f30..c53829922 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,9 +1,11 @@ +import DatabaseService from './DatabaseService'; import postHogClient from './PostHogClient'; import BotService from './BotService'; import EventService from './EventService'; import IntegrationService from './IntegrationService'; export { + DatabaseService, postHogClient, BotService, EventService, From 1c2a43ceea987155dd1386dd516be99ecb813840 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 2 Jan 2023 15:24:28 +0700 Subject: [PATCH 30/91] Clean unecessary imports --- backend/src/ee/helpers/secret.ts | 2 +- backend/src/helpers/secret.ts | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 0c4172256..2f726a9a5 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -147,7 +147,7 @@ const initSecretVersioningHelper = async () => { } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to ensure secrets are versioned'); + throw new Error('Failed to ensure that secrets are versioned'); } } diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 61fc25174..f71f094e8 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,5 +1,4 @@ import * as Sentry from '@sentry/node'; -import { Types } from 'mongoose'; import { Secret, ISecret, @@ -10,13 +9,8 @@ import { } from '../ee/services'; import { SecretVersion, - Action, IAction } from '../ee/models'; -import { - takeSecretSnapshotHelper -} from '../ee/helpers/secret'; -import { decryptSymmetric } from '../utils/crypto'; import { SECRET_SHARED, SECRET_PERSONAL, @@ -62,8 +56,6 @@ interface Update { [index: string]: any; } -type DecryptSecretType = 'text' | 'object' | 'expanded'; - /** * Push secrets for user with id [userId] to workspace * with id [workspaceId] with environment [environment]. Follow steps: @@ -285,9 +277,6 @@ const v1PushSecrets = async ({ } }; -// TODO: optimize this route. -// TODO: ensure that it's possible to query for and filter logs - /** * Push secrets for user with id [userId] to workspace * with id [workspaceId] with environment [environment]. Follow steps: From 4af839040e42b995b240768e6768bf7a3cff274b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 2 Jan 2023 15:43:21 +0700 Subject: [PATCH 31/91] Patch actionNames on getWorkspacelogs --- backend/src/ee/controllers/v1/workspaceController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 1df50b18b..838a3253d 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -41,10 +41,11 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { let logs try { const { workspaceId } = req.params; - const { userId, actionNames } = req.query; const offset: number = parseInt(req.query.offset as string); const limit: number = parseInt(req.query.limit as string); + const userId: string = req.query.userId as string; + const actionNames: string = req.query.actionNames as string; logs = await Log.find({ workspace: workspaceId, @@ -53,7 +54,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { actionNames ? { actionNames: { - $in: actionNames + $in: actionNames.split(',') } } : {} ) From e99ee94a7b5bfcb27c9eae9a8466bfb270449f46 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 2 Jan 2023 22:43:00 +0700 Subject: [PATCH 32/91] Modify service token format --- .../v1/serviceTokenDataController.ts | 25 +++++++--------- backend/src/helpers/auth.ts | 30 ++++++++++++++----- backend/src/middleware/requireAuth.ts | 3 +- .../src/middleware/requireWorkspaceAuth.ts | 6 ++-- backend/src/models/serviceTokenData .ts | 9 ++---- backend/src/routes/v1/serviceTokenData.ts | 4 +-- backend/src/routes/v2/workspace.ts | 2 -- 7 files changed, 41 insertions(+), 38 deletions(-) diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v1/serviceTokenDataController.ts index 3a4b94a83..d8f4d4eea 100644 --- a/backend/src/controllers/v1/serviceTokenDataController.ts +++ b/backend/src/controllers/v1/serviceTokenDataController.ts @@ -15,7 +15,7 @@ import { * @param res * @returns */ -export const getServiceTokenData = async (req: Request, res: Response) => ({ +export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).send({ serviceTokenData: req.serviceTokenData }); @@ -38,35 +38,32 @@ export const createServiceTokenData = async (req: Request, res: Response) => { tag, expiresIn } = req.body; + + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); - // 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); - // create service token data - serviceTokenData = new ServiceTokenData({ + serviceTokenData = await new ServiceTokenData({ name, workspace: workspaceId, environment, user: req.user._id, expiresAt, - prefix: serviceToken.substring(0, 9), - serviceTokenHash, + secretHash, encryptedKey, iv, tag - }) + }).save(); - await serviceTokenData.save(); - // return service token data without sensitive data serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); + if (!serviceTokenData) throw new Error('Failed to find service token data'); + + serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index c3b212916..ad63d41b4 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -15,7 +15,8 @@ import { import { AccountNotFoundError, ServiceTokenDataNotFoundError, - UnauthorizedRequestError + UnauthorizedRequestError, + BadRequestError } from '../utils/errors'; /** @@ -101,15 +102,30 @@ const getAuthSTDPayload = async ({ }) => { let serviceTokenData; try { - const serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + + // TODO: optimize double query + serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER, 'secretHash expiresAt'); + + if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + // case: service token expired + await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate expired service token' + }); + } + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + + const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); + if (!isMatch) throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); serviceTokenData = await ServiceTokenData - .findOne({ - serviceTokenHash - }) + .findById(TOKEN_IDENTIFIER) .select('+encryptedKey +iv +tag'); - - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); } catch (err) { throw UnauthorizedRequestError({ diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index b91d2cd09..8253cb64e 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -6,8 +6,7 @@ import { getAuthUserPayload, getAuthSTDPayload } from '../helpers/auth'; -import { JWT_AUTH_SECRET } from '../config'; -import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { BadRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 2a9110c2c..68edec6a4 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -40,10 +40,10 @@ const requireWorkspaceAuth = ({ if ( req.serviceTokenData && req.serviceTokenData.workspace !== workspaceId - && req.serviceTokenData.environment !== req.body.environment - ) - // case: st auth + && req.serviceTokenData.environment !== req.query.environment + ) { next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) + } return next(); } catch (err) { diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts index 1faecfaff..8e8ae5eab 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData .ts @@ -7,8 +7,7 @@ export interface IServiceTokenData { environment: string; // TODO: adapt to upcoming environment id user: Types.ObjectId; expiresAt: Date; - prefix: string; - serviceTokenHash: string; + secretHash: string; encryptedKey: string; iv: string; tag: string; @@ -37,11 +36,7 @@ const serviceTokenDataSchema = new Schema( expiresAt: { type: Date }, - prefix: { - type: String, - required: true - }, - serviceTokenHash: { + secretHash: { type: String, unique: true, required: true, diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v1/serviceTokenData.ts index 4223172d3..1d7700615 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v1/serviceTokenData.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; const router = express.Router(); import { requireAuth, @@ -20,8 +20,6 @@ router.get( requireAuth({ acceptedAuthModes: ['serviceToken'] }), - param('serviceTokenDataId').exists().trim(), - validateRequest, serviceTokenDataController.getServiceTokenData ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index df26749f3..b2c91bd1b 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -4,11 +4,9 @@ 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( From a07d4e6dd15625294434f50197a0cffe0c9a515a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 11:23:48 -0500 Subject: [PATCH 33/91] update types name for secrets v2 api --- backend/src/routes/v2/secret.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 69b70c37e..8c87db882 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -120,9 +120,12 @@ router.delete( } }) - const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) - if (bulkModificationInfoError) { - throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkDeleteError) { + if (bulkDeleteError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) + } + throw InternalServerError() } res.status(200).send() @@ -135,7 +138,7 @@ router.delete( router.patch( '/batch-modify/workspace/:workspaceId/environment/:environmentName', requireAuth, - body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), requireWorkspaceAuth({ @@ -145,7 +148,6 @@ router.patch( validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; - const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { throw InternalServerError({ message: "Unable to fetch secrets you own" }) @@ -154,6 +156,7 @@ router.patch( const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); const updateOperationsToPerform: any = [] + secretsModificationsRequested.forEach(userModifiedSecret => { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { const sanitizedSecret: SanitizedSecretModify = { @@ -180,7 +183,11 @@ router.patch( const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) if (bulkModificationInfoError) { - throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + if (bulkModificationInfoError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) + } + + throw InternalServerError() } return res.status(200).send() From ccb1c3141398262539d938d121c68d1245d3e5cd Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 11:24:41 -0500 Subject: [PATCH 34/91] add set command for crud cli --- cli/packages/cmd/secrets.go | 97 ++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index a926a4293..5b98a9f22 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -35,7 +35,6 @@ var secretsCmd = &cobra.Command{ } visualize.PrintAllSecretDetails(secrets) - }, } @@ -94,7 +93,19 @@ var secretsSetCmd = &cobra.Command{ // decrypt workspace key plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - secretsToUpload := []models.Secret{} + + // pull current secrets + secrets, err := util.GetAllEnvironmentVariables("", "dev") + if err != nil { + log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Debug(err) + } + + secretsToCreate := []models.Secret{} + secretsToModify := []models.Secret{} + + secretByKey := getSecretsByKeys(secrets) + for _, arg := range args { splitKeyValueFromArg := strings.SplitN(arg, "=", 2) if len(splitKeyValueFromArg) < 2 { @@ -104,46 +115,78 @@ var secretsSetCmd = &cobra.Command{ key := splitKeyValueFromArg[0] value := splitKeyValueFromArg[1] + fmt.Println("key", key, "value", value) + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) if err != nil { log.Errorf("unable to encrypt your secrets [err=%v]", err) } - hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) - + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) encryptedValue, err := util.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) if err != nil { log.Errorf("unable to encrypt your secrets [err=%v]", err) } - hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) - - fullEncryptedSecret := models.Secret{ - SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), - SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), - SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), - SecretKeyHash: hashedKey, - SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), - SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), - SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), - SecretValueHash: hashedValue, - Type: "shared", + if value, ok := secretByKey[key]; ok { + // case: secret exists in project so it needs to be modified + encryptedSecretDetails := models.Secret{ + ID: value.ID, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + } + secretsToModify = append(secretsToModify, encryptedSecretDetails) + } else { + // case: secret doesn't exist in project so it needs to be created + encryptedSecretDetails := models.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: "shared", + } + secretsToCreate = append(secretsToCreate, encryptedSecretDetails) } - secretsToUpload = append(secretsToUpload, fullEncryptedSecret) } - batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", - Secrets: secretsToUpload, - } - err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) - if err != nil { - log.Errorf("Unable to complete your request because %v", err) - return + if len(secretsToCreate) > 0 { + fmt.Println("create") + batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToCreate, + } + + err = http.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + if err != nil { + log.Errorf("Unable to process new secret creations because %v", err) + return + } } - log.Infof("secret name(s) [%v] have been created", strings.Join(args, ", ")) + if len(secretsToModify) > 0 { + fmt.Println("modify") + batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{ + WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + EnvironmentName: "dev", + Secrets: secretsToModify, + } + + err = http.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) + if err != nil { + log.Errorf("Unable to process the modifications to your secrets because %v", err) + return + } + } + + log.Infof("secret name(s) [%v] have been set", strings.Join(args, ", ")) }, } From 408eb482f1361d0335d449366d7a64494c8a69da Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 11:26:27 -0500 Subject: [PATCH 35/91] remove --ignore-scripts for backend temporary --- .github/workflows/check-be-pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-be-pull-request.yml b/.github/workflows/check-be-pull-request.yml index 8022a25bc..3cb010e8c 100644 --- a/.github/workflows/check-be-pull-request.yml +++ b/.github/workflows/check-be-pull-request.yml @@ -24,7 +24,7 @@ jobs: cache: "npm" cache-dependency-path: backend/package-lock.json - name: 📦 Install dependencies - run: npm ci --only-production --ignore-scripts + run: npm ci --only-production working-directory: backend - name: 🧪 Run tests run: npm run test:ci From 03b7d3a5ce4cc8e54a588e6aa99d25f1129afb33 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Mon, 2 Jan 2023 09:57:02 -0800 Subject: [PATCH 36/91] Wired frontend for logs --- .../ee/controllers/v1/workspaceController.ts | 1 + frontend/components/basic/EventFilter.tsx | 5 +-- frontend/ee/api/secrets/GetProjectLogs.ts | 41 +++++++++++++++---- frontend/pages/activity/[id].tsx | 35 ++++++++++++---- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 838a3253d..4fe040122 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -59,6 +59,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { } : {} ) }) + .sort({ createdAt: -1 }) .skip(offset) .limit(limit) .populate('actions') diff --git a/frontend/components/basic/EventFilter.tsx b/frontend/components/basic/EventFilter.tsx index d4fe36193..fc0c4e1e1 100644 --- a/frontend/components/basic/EventFilter.tsx +++ b/frontend/components/basic/EventFilter.tsx @@ -48,7 +48,7 @@ export default function EventFilter({
{selected != '' ? ( -

{selected}

+

{t("activity:event." + selected)}

) : (

Select an event

)} @@ -76,7 +76,7 @@ export default function EventFilter({ className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${ selected == t("activity:event." + event.name) && 'bg-mineshaft-700' }`} - value={t("activity:event." + event.name)} + value={event.name} > {({ selected }) => ( <> @@ -90,7 +90,6 @@ export default function EventFilter({ )} - {/* {event.name} */} ); })} diff --git a/frontend/ee/api/secrets/GetProjectLogs.ts b/frontend/ee/api/secrets/GetProjectLogs.ts index d5324355c..277205f2f 100644 --- a/frontend/ee/api/secrets/GetProjectLogs.ts +++ b/frontend/ee/api/secrets/GetProjectLogs.ts @@ -5,7 +5,8 @@ interface workspaceProps { workspaceId: string; offset: number; limit: number; - filters: object; + userId: string; + actionNames: string; } /** @@ -14,17 +15,41 @@ interface workspaceProps { * @param {string} obj.workspaceId - workspace id for which we are trying to get project log * @param {object} obj.offset - teh starting point of logs that we want to pull * @param {object} obj.limit - how many logs will we output - * @param {object} obj.filters + * @param {object} obj.userId - optional userId filter - will only query logs for that user + * @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions * @returns */ -const getProjectLogs = async ({ workspaceId, offset, limit, filters }: workspaceProps) => { +const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => { + let payload; + if (userId != "" && actionNames != '') { + payload = { + offset: String(offset), + limit: String(limit), + userId: JSON.stringify(userId), + actionNames: actionNames + } + } else if (userId != "") { + payload = { + offset: String(offset), + limit: String(limit), + userId: JSON.stringify(userId) + } + } else if (actionNames != "") { + payload = { + offset: String(offset), + limit: String(limit), + actionNames: actionNames + } + } else { + payload = { + offset: String(offset), + limit: String(limit) + } + } + return SecurityClient.fetchCall( '/api/v1/workspace/' + workspaceId + '/logs?' + - new URLSearchParams({ - offset: String(offset), - limit: String(limit), - filters: JSON.stringify(filters) - }), + new URLSearchParams(payload), { method: 'GET', headers: { diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index f384ae69f..ed5a6def0 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -55,9 +55,34 @@ export default function Activity() { const [currentEvent, setCurrentEvent] = useState(""); const { t } = useTranslation(); + // this use effect updates the data in case of a new filter being added + useEffect(() => { + setCurrentOffset(0); + const getLogData = async () => { + const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen }) + setLogsData(tempLogsData.map((log: logData) => { + return { + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log.user.email, + payload: log.actions.map(action => { + return { + name: action.name, + secretVersions: action.payload.secretVersions + } + }) + } + })) + } + getLogData(); + }, [eventChosen]); + + // this use effect adds more data in case 'View More' button is clicked useEffect(() => { const getLogData = async () => { - const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, filters: {} }) + const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen }) setLogsData(logsData.concat(tempLogsData.map((log: logData) => { return { _id: log._id, @@ -97,16 +122,10 @@ export default function Activity() {
b.createdAt.localeCompare(a.createdAt)) - .filter((log) => - eventChosen != '' ? log.payload?.map(action => t("activity:event." + action.name)).includes(eventChosen) : true - ) - } + data={logsData} toggleSidebar={toggleSidebar} setCurrentEvent={setCurrentEvent} /> From ae5320e4fa8728d831931c14fc68572c51246d65 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Mon, 2 Jan 2023 14:20:39 -0800 Subject: [PATCH 37/91] Finished activity logs V1 --- frontend/components/basic/EventFilter.tsx | 5 + frontend/ee/api/secrets/GetActionData.ts | 32 ++++ frontend/ee/components/ActivitySideBar.tsx | 207 ++++++++++++++++----- frontend/ee/components/ActivityTable.tsx | 43 ++--- frontend/pages/activity/[id].tsx | 12 +- 5 files changed, 220 insertions(+), 79 deletions(-) create mode 100644 frontend/ee/api/secrets/GetActionData.ts diff --git a/frontend/components/basic/EventFilter.tsx b/frontend/components/basic/EventFilter.tsx index fc0c4e1e1..c9b31fd43 100644 --- a/frontend/components/basic/EventFilter.tsx +++ b/frontend/components/basic/EventFilter.tsx @@ -6,6 +6,7 @@ import { faEye, faPlus, faShuffle, + faTrash, faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -28,6 +29,10 @@ const eventOptions = [ { name: 'updateSecrets', icon: faShuffle + }, + { + name: 'deleteSecrets', + icon: faTrash } ]; diff --git a/frontend/ee/api/secrets/GetActionData.ts b/frontend/ee/api/secrets/GetActionData.ts new file mode 100644 index 000000000..122870d69 --- /dev/null +++ b/frontend/ee/api/secrets/GetActionData.ts @@ -0,0 +1,32 @@ +import SecurityClient from '~/utilities/SecurityClient'; + + +interface workspaceProps { + actionId: string; +} + +/** + * This function fetches the data for a certain action performed by a user + * @param {object} obj + * @param {string} obj.actionId - id of an action for which we are trying to get data + * @returns + */ +const getActionData = async ({ actionId }: workspaceProps) => { + return SecurityClient.fetchCall( + '/api/v1/action/' + actionId, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + console.log(188, res) + if (res && res.status == 200) { + return (await res.json()).action; + } else { + console.log('Failed to get the info about an action'); + } + }); +}; + +export default getActionData; diff --git a/frontend/ee/components/ActivitySideBar.tsx b/frontend/ee/components/ActivitySideBar.tsx index d96a3f6ab..e0a634d45 100644 --- a/frontend/ee/components/ActivitySideBar.tsx +++ b/frontend/ee/components/ActivitySideBar.tsx @@ -1,79 +1,184 @@ +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import { faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import getActionData from "ee/api/secrets/GetActionData"; import patienceDiff from 'ee/utilities/findTextDifferences'; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; + import DashboardInputField from '../../components/dashboard/DashboardInputField'; -const secretChanges = [{ - "oldSecret": "secret1", - "newSecret": "ecret2" -}, { - "oldSecret": "secret1", - "newSecret": "sercet2" -}, { - "oldSecret": "localhosta:8080", - "newSecret": "aaaalocalhoats:3000" -}] +const { + decryptAssymmetric, + decryptSymmetric +} = require('../../components/utilities/cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); interface SideBarProps { - toggleSidebar: (value: string[]) => void; - sidebarData: string[]; - currentEvent: string; + toggleSidebar: (value: string) => void; + currentAction: string; +} + +interface SecretProps { + secret: string; + secretKeyCiphertext: string; + secretKeyHash: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueHash: string; + secretValueIV: string; + secretValueTag: string; +} + +interface DecryptedSecretProps { + newSecretVersion: { + key: string; + value: string; + } + oldSecretVersion: { + key: string; + value: string; + } +} + +interface ActionProps { + name: string; } /** * @param {object} obj * @param {function} obj.toggleSidebar - function that opens or closes the sidebar - * @param {string[]} obj.secretIds - data of payload - * @param {string} obj.currentEvent - the event name for which a sidebar is being displayed + * @param {string} obj.currentAction - the action id for which a sidebar is being displayed * @returns the sidebar with the payload of user activity logs */ const ActivitySideBar = ({ toggleSidebar, - sidebarData, - currentEvent + currentAction }: SideBarProps) => { const { t } = useTranslation(); + const router = useRouter(); + const [actionData, setActionData] = useState(); + const [actionMetaData, setActionMetaData] = useState(); + const [isLoading, setIsLoading] = useState(false); - return
-
-
-

{t("activity:event." + currentEvent)}

-
toggleSidebar([])}> - + useEffect(() => { + const getLogData = async () => { + setIsLoading(true); + const tempActionData = await getActionData({ actionId: currentAction }); + const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + // #TODO: make this a separate function and reuse across the app + let decryptedLatestKey: string; + if (latestKey) { + // assymmetrically decrypt symmetric key with local private key + decryptedLatestKey = decryptAssymmetric({ + ciphertext: latestKey.latestKey.encryptedKey, + nonce: latestKey.latestKey.nonce, + publicKey: latestKey.latestKey.sender.publicKey, + privateKey: String(PRIVATE_KEY) + }); + } + + const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: { + newSecretVersion?: SecretProps; + oldSecretVersion?: SecretProps; + }) => { + return { + newSecretVersion: { + key: decryptSymmetric({ + ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext, + iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV, + tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag, + key: decryptedLatestKey + }), + value: decryptSymmetric({ + ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext, + iv: encryptedSecretVersion.newSecretVersion!.secretValueIV, + tag: encryptedSecretVersion.newSecretVersion!.secretValueTag, + key: decryptedLatestKey + }) + }, + oldSecretVersion: { + key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext + ? decryptSymmetric({ + ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext, + iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV, + tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag, + key: decryptedLatestKey + }): undefined, + value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext + ? decryptSymmetric({ + ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext, + iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV, + tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag, + key: decryptedLatestKey + }): undefined + } + } + }) + + setActionData(decryptedSecretVersions); + setActionMetaData({name: tempActionData.name}); + setIsLoading(false); + } + getLogData(); + }, [currentAction]); + + return
+ {isLoading ? ( +
+ infisical loading indicator +
+ ) : ( +
+
+

{t("activity:event." + actionMetaData?.name)}

+
toggleSidebar("")}> + +
+
+
+ {(actionMetaData?.name == 'readSecrets' + || actionMetaData?.name == 'addSecrets' + || actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) => +
+
{item.newSecretVersion.key}
+ {}} + type="value" + position={1} + value={item.newSecretVersion.value} + isDuplicate={false} + blurred={false} + /> +
+ )} + {actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) => + <> +
{item.newSecretVersion.key}
+
+
- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && {character.line})}
+
+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && {character.line})}
+
+ + )}
-
- {currentEvent == 'readSecrets' && sidebarData.map((item, id) => - <> -
Key {id}
- {}} - type="varName" - position={1} - value={"a" + item} - isDuplicate={false} - blurred={false} - /> - - )} - {currentEvent == 'updateSecrets' && sidebarData.map((item, id) => - secretChanges.map(secretChange => - <> -
Secret Name {id}
-
-
- {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split(''), false).lines.map((character, id) => character.aIndex != -1 && {character.line})}
-
+ {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split('')).lines.map((character, id) => character.bIndex != -1 && {character.line})}
-
- - ))} -
-
- + )}
}; diff --git a/frontend/ee/components/ActivityTable.tsx b/frontend/ee/components/ActivityTable.tsx index a25920202..607b25c5d 100644 --- a/frontend/ee/components/ActivityTable.tsx +++ b/frontend/ee/components/ActivityTable.tsx @@ -4,8 +4,7 @@ import { useTranslation } from "next-i18next"; import { faAngleDown, faAngleRight, - faUpRightFromSquare, - faX + faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import timeSince from 'ee/utilities/timeSince'; @@ -14,6 +13,7 @@ import guidGenerator from '../../components/utilities/randomId'; interface PayloadProps { + _id: string; name: string; secretVersions: string[]; } @@ -29,25 +29,26 @@ interface logData { /** - * + * This is a single row of the activity table * @param obj - * @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened + * @param {logData} obj.row - data for a certain event + * @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event * @returns */ -const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData, toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => { +const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => { const [payloadOpened, setPayloadOpened] = useState(false); const { t } = useTranslation(); return ( <> - + setPayloadOpened(!payloadOpened)} className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" > @@ -66,26 +67,23 @@ const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData {payloadOpened && - + Timestamp {row.createdAt} } {payloadOpened && row.payload?.map((action, index) => - + {t("activity:event." + action.name)} - { - toggleSidebar(action.secretVersions); - setCurrentEvent(action.name); - }}> + toggleSidebar(action._id)}> {action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")} )} {payloadOpened && - + IP Address {row.ipAddress} @@ -99,28 +97,27 @@ const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData * @param {object} obj * @param {logData} obj.data - data for user activity logs * @param {function} obj.toggleSidebar - function that opens or closes the sidebar - * @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened * @returns */ -const ActivityTable = ({ data, toggleSidebar, setCurrentEvent }: { data: logData[], toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => { +const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => { return (
-
+
- + - - - - + + + + {data?.map((row, index) => { - return ; + return ; })}
EventUserSourceTimeEVENTUSERSOURCETIME
diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index ed5a6def0..974607d0a 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -21,6 +21,7 @@ interface logData { email: string; }; actions: { + _id: string; name: string; payload: { secretVersions: string[]; @@ -29,6 +30,7 @@ interface logData { } interface PayloadProps { + _id: string; name: string; secretVersions: string[]; } @@ -51,8 +53,7 @@ export default function Activity() { const [logsData, setLogsData] = useState([]); const [currentOffset, setCurrentOffset] = useState(0); const currentLimit = 10; - const [sidebarData, toggleSidebar] = useState([]) - const [currentEvent, setCurrentEvent] = useState(""); + const [currentSidebarAction, toggleSidebar] = useState() const { t } = useTranslation(); // this use effect updates the data in case of a new filter being added @@ -69,6 +70,7 @@ export default function Activity() { user: log.user.email, payload: log.actions.map(action => { return { + _id: action._id, name: action.name, secretVersions: action.payload.secretVersions } @@ -92,6 +94,7 @@ export default function Activity() { user: log.user.email, payload: log.actions.map(action => { return { + _id: action._id, name: action.name, secretVersions: action.payload.secretVersions } @@ -109,13 +112,13 @@ export default function Activity() { return (
- {sidebarData.length > 0 && } + {currentSidebarAction && }

Activity Logs

- Event history limited to the last 12 months. + Event history for this Infisical project.

@@ -127,7 +130,6 @@ export default function Activity() {
From 0ff8194cf8ec636711a49a5ec8d81d0a357379f2 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 3 Jan 2023 09:19:07 +0700 Subject: [PATCH 38/91] Modify getWorkspaceLogs to accept sortBy query param --- backend/src/ee/controllers/v1/workspaceController.ts | 9 ++++++++- backend/src/ee/routes/v1/workspace.ts | 3 +++ frontend/ee/api/secrets/GetProjectLogs.ts | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 4fe040122..25a8c8d76 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -37,6 +37,12 @@ import { }); } +/** + * Return (audit) logs for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ export const getWorkspaceLogs = async (req: Request, res: Response) => { let logs try { @@ -44,6 +50,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { const offset: number = parseInt(req.query.offset as string); const limit: number = parseInt(req.query.limit as string); + const sortBy: string = req.query.sortBy as string; const userId: string = req.query.userId as string; const actionNames: string = req.query.actionNames as string; @@ -59,7 +66,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { } : {} ) }) - .sort({ createdAt: -1 }) + .sort({ createdAt: sortBy === 'recent' ? -1 : 1 }) .skip(offset) .limit(limit) .populate('actions') diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index e9e6938bf..5ae190ccd 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -33,6 +33,9 @@ router.get( param('workspaceId').exists().trim(), query('offset').exists().isInt(), query('limit').exists().isInt(), + query('sortBy'), + query('userId'), + query('actionNames'), validateRequest, workspaceController.getWorkspaceLogs ); diff --git a/frontend/ee/api/secrets/GetProjectLogs.ts b/frontend/ee/api/secrets/GetProjectLogs.ts index 277205f2f..e127be815 100644 --- a/frontend/ee/api/secrets/GetProjectLogs.ts +++ b/frontend/ee/api/secrets/GetProjectLogs.ts @@ -25,6 +25,7 @@ const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames payload = { offset: String(offset), limit: String(limit), + sortBy: 'recent', userId: JSON.stringify(userId), actionNames: actionNames } @@ -32,18 +33,21 @@ const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames payload = { offset: String(offset), limit: String(limit), + sortBy: 'recent', userId: JSON.stringify(userId) } } else if (actionNames != "") { payload = { offset: String(offset), limit: String(limit), + sortBy: 'recent', actionNames: actionNames } } else { payload = { offset: String(offset), - limit: String(limit) + limit: String(limit), + sortBy: 'recent' } } From e9601307efdbea93f8fdae15a69e4b800d1859c3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 3 Jan 2023 09:33:00 +0700 Subject: [PATCH 39/91] Move service token data routes and controllers to v2 --- backend/src/app.ts | 6 +++--- backend/src/controllers/v1/index.ts | 4 +--- backend/src/controllers/v2/index.ts | 4 +++- .../controllers/{v1 => v2}/serviceTokenDataController.ts | 0 backend/src/ee/routes/v1/workspace.ts | 1 - backend/src/routes/v1/index.ts | 4 +--- backend/src/routes/v2/index.ts | 4 +++- backend/src/routes/{v1 => v2}/serviceTokenData.ts | 2 +- 8 files changed, 12 insertions(+), 13 deletions(-) rename backend/src/controllers/{v1 => v2}/serviceTokenDataController.ts (100%) rename backend/src/routes/{v1 => v2}/serviceTokenData.ts (95%) diff --git a/backend/src/app.ts b/backend/src/app.ts index f37fd7560..ca6428447 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -29,7 +29,6 @@ import { userAction as v1UserActionRouter, secret as v1SecretRouter, serviceToken as v1ServiceTokenRouter, - serviceTokenData as v1ServiceTokenDataRouter, password as v1PasswordRouter, stripe as v1StripeRouter, integration as v1IntegrationRouter, @@ -37,7 +36,8 @@ import { } from './routes/v1'; import { secret as v2SecretRouter, - workspace as v2WorkspaceRouter + workspace as v2WorkspaceRouter, + serviceTokenData as v2ServiceTokenDataRouter, } from './routes/v2'; import { getLogger } from './utils/logger'; @@ -85,7 +85,6 @@ 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); @@ -94,6 +93,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); +app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); //* 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/v1/index.ts b/backend/src/controllers/v1/index.ts index 56c6071c5..1da61835f 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -14,7 +14,6 @@ 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, @@ -32,6 +31,5 @@ export { stripeController, userActionController, userController, - workspaceController, - serviceTokenDataController + workspaceController }; diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index dc6977c91..d4729c15c 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,5 +1,7 @@ import * as workspaceController from './workspaceController'; +import * as serviceTokenDataController from './serviceTokenDataController'; export { - workspaceController + workspaceController, + serviceTokenDataController } diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts similarity index 100% rename from backend/src/controllers/v1/serviceTokenDataController.ts rename to backend/src/controllers/v2/serviceTokenDataController.ts diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index d127599e1..bc3480280 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -25,5 +25,4 @@ router.get( workspaceController.getWorkspaceSecretSnapshots ); - export default router; \ No newline at end of file diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 89b02ebc9..2dfe58baa 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -15,7 +15,6 @@ import password from './password'; import stripe from './stripe'; import integration from './integration'; import integrationAuth from './integrationAuth'; -import serviceTokenData from './serviceTokenData'; export { signup, @@ -34,6 +33,5 @@ export { password, stripe, integration, - integrationAuth, - serviceTokenData + integrationAuth }; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 6e6758753..acf115a92 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,7 +1,9 @@ import secret from './secret'; import workspace from './workspace'; +import serviceTokenData from './serviceTokenData'; export { secret, - workspace + workspace, + serviceTokenData } diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts similarity index 95% rename from backend/src/routes/v1/serviceTokenData.ts rename to backend/src/routes/v2/serviceTokenData.ts index 1d7700615..578d4e38e 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -13,7 +13,7 @@ import { COMPLETED, GRANTED } from '../../variables'; -import { serviceTokenDataController } from '../../controllers/v1'; +import { serviceTokenDataController } from '../../controllers/v2'; router.get( '/', From 6845e9129ae71812411f191bff0e9f95fc5488b4 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Mon, 2 Jan 2023 18:33:24 -0800 Subject: [PATCH 40/91] Updated icon for activity logs --- frontend/components/basic/Layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/components/basic/Layout.tsx b/frontend/components/basic/Layout.tsx index a53de0b41..8bdabc97b 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import { faBookOpen, + faFileLines, faGear, faKey, faMobile, @@ -158,7 +159,7 @@ export default function Layout({ children }: LayoutProps) { { href: '/activity/' + workspaceMapping[workspaceSelected as any], title: 'Activity Logs', - emoji: + emoji: }, { href: "/settings/project/" + workspaceMapping[workspaceSelected as any], From d0949b2e196c8d0764d3f331b559d0b05284574c Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Mon, 2 Jan 2023 18:53:56 -0800 Subject: [PATCH 41/91] Fixed the sorting buf with version history --- .../src/ee/controllers/v1/secretController.ts | 1 + frontend/ee/components/SecretVersionList.tsx | 67 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index b2d66ab33..a2d68ca96 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -18,6 +18,7 @@ import { SecretVersion } from '../../models'; secretVersions = await SecretVersion.find({ secret: secretId }) + .sort({ createdAt: -1 }) .skip(offset) .limit(limit); diff --git a/frontend/ee/components/SecretVersionList.tsx b/frontend/ee/components/SecretVersionList.tsx index 6cf7bd9b2..3eb40005f 100644 --- a/frontend/ee/components/SecretVersionList.tsx +++ b/frontend/ee/components/SecretVersionList.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import Image from 'next/image'; import { useRouter } from 'next/router'; import { useTranslation } from "next-i18next"; import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons'; @@ -22,15 +23,18 @@ interface EncrypetedSecretVersionListProps { /** + * @param {string} secretId - the id of a secret for which are querying version history * @returns a list of versions for a specific secret */ const SecretVersionList = ({ secretId }: { secretId: string; }) => { const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); - const [secretVersions, setSecretVersions] = useState([{createdAt: "123", value: "124"}]); + const [secretVersions, setSecretVersions] = useState([]); useEffect(() => { const getSecretVersionHistory = async () => { + setIsLoading(true); try { const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10}); const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) @@ -61,43 +65,54 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => { }) setSecretVersions(decryptedSecretVersions); + setIsLoading(false); } catch (error) { console.log(error) } }; getSecretVersionHistory(); - }, []); + }, [secretId]); return
-

{t("dashboard:sidebar.version-history")}

-
-
- {secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) - .map((version: DecryptedSecretVersionListProps, index: number) => -
-
-
-
-
-
-
- {(new Date(version.createdAt)).toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - })} +

{t("dashboard:sidebar.version-history")}

+
+ {isLoading ? ( +
+ infisical loading indicator +
+ ) : ( +
+ {secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .map((version: DecryptedSecretVersionListProps, index: number) => +
+
+
+
+
+
+
+ {(new Date(version.createdAt)).toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} +
+

Value:{version.value}

+
-

Value:{version.value}

- {/*

Updated by:{version.user}

*/} -
+ )}
)}
-
}; export default SecretVersionList; From daf8a73529f1d0f965d68bed29d70fb1ab900b82 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 2 Jan 2023 22:41:15 -0500 Subject: [PATCH 42/91] add dynmaic workspace and user creds for secrets cmd --- cli/packages/cmd/secrets.go | 125 ++++++++++++++++++++++++++++------- cli/packages/util/common.go | 2 + cli/packages/util/secrets.go | 14 ++++ 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 5b98a9f22..8960b6603 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "fmt" "strings" + "unicode" "crypto/sha256" @@ -27,7 +28,14 @@ var secretsCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + + secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets = util.SubstituteSecrets(secrets) if err != nil { log.Debugln(err) @@ -56,6 +64,41 @@ var secretsSetCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + secretType, err := cmd.Flags().GetString("type") + if err != nil { + log.Errorln("Unable to parse the secret type flag") + log.Debugln(err) + return + } + + if !util.IsSecretTypeValid(secretType) { + log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType) + return + } + + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + + if !util.IsSecretEnvironmentValid(environmentName) { + log.Errorln("You have entered a invalid environment name. Environment names can only be prod, dev, test or staging") + return + } + + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + } + + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + log.Error(err) + return + } + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() if err != nil { log.Error(err) @@ -77,7 +120,7 @@ var secretsSetCmd = &cobra.Command{ SetHeader("Accept", "application/json") request := models.GetEncryptedWorkspaceKeyRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", + WorkspaceId: workspaceFile.WorkspaceId, } workspaceKeyResponse, err := http.CallGetEncryptedWorkspaceKey(httpClient, request) @@ -95,9 +138,9 @@ var secretsSetCmd = &cobra.Command{ plainTextEncryptionKey := util.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) // pull current secrets - secrets, err := util.GetAllEnvironmentVariables("", "dev") + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { - log.Error("Unable to retrieve secrets. Run with -d to see full logs") + log.Error("unable to retrieve secrets. Run with -d to see full logs") log.Debug(err) } @@ -108,15 +151,19 @@ var secretsSetCmd = &cobra.Command{ for _, arg := range args { splitKeyValueFromArg := strings.SplitN(arg, "=", 2) - if len(splitKeyValueFromArg) < 2 { - splitKeyValueFromArg[1] = "" + if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { + log.Error("ensure that each secret has a none empty key and value. Modify the input and try again") + return + } + + if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { + log.Error("keys of secrets cannot start with a number. Modify the key name(s) and try again") + return } key := splitKeyValueFromArg[0] value := splitKeyValueFromArg[1] - fmt.Println("key", key, "value", value) - hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) encryptedKey, err := util.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) if err != nil { @@ -129,16 +176,21 @@ var secretsSetCmd = &cobra.Command{ log.Errorf("unable to encrypt your secrets [err=%v]", err) } - if value, ok := secretByKey[key]; ok { + if existingSecret, ok := secretByKey[key]; ok { // case: secret exists in project so it needs to be modified encryptedSecretDetails := models.Secret{ - ID: value.ID, + ID: existingSecret.ID, SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, } - secretsToModify = append(secretsToModify, encryptedSecretDetails) + + // Only add to modifications if the value is different + if existingSecret.Value != value { + secretsToModify = append(secretsToModify, encryptedSecretDetails) + } + } else { // case: secret doesn't exist in project so it needs to be created encryptedSecretDetails := models.Secret{ @@ -150,17 +202,16 @@ var secretsSetCmd = &cobra.Command{ SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, - Type: "shared", + Type: secretType, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) } } if len(secretsToCreate) > 0 { - fmt.Println("create") batchCreateRequest := models.BatchCreateSecretsByWorkspaceAndEnvRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", + WorkspaceId: workspaceFile.WorkspaceId, + EnvironmentName: environmentName, Secrets: secretsToCreate, } @@ -172,10 +223,9 @@ var secretsSetCmd = &cobra.Command{ } if len(secretsToModify) > 0 { - fmt.Println("modify") batchModifyRequest := models.BatchModifySecretsByWorkspaceAndEnvRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", + WorkspaceId: workspaceFile.WorkspaceId, + EnvironmentName: environmentName, Secrets: secretsToModify, } @@ -186,7 +236,7 @@ var secretsSetCmd = &cobra.Command{ } } - log.Infof("secret name(s) [%v] have been set", strings.Join(args, ", ")) + log.Infoln("secrets have been successfully set") }, } @@ -198,6 +248,13 @@ var secretsDeleteCmd = &cobra.Command{ PreRun: toggleDebug, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() if err != nil { log.Error(err) @@ -214,7 +271,18 @@ var secretsDeleteCmd = &cobra.Command{ return } - secrets, err := util.GetAllEnvironmentVariables("", "dev") + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + } + + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + log.Error(err) + return + } + + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { log.Error("Unable to retrieve secrets. Run with -d to see full logs") log.Debug(err) @@ -233,13 +301,13 @@ var secretsDeleteCmd = &cobra.Command{ } if len(invalidSecretNamesThatDoNotExist) != 0 { - log.Errorf("secret name(s) [%v] does not exist in your project. Please remove and re-run the command", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) + log.Errorf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) return } request := models.BatchDeleteSecretsBySecretIdsRequest{ - WorkspaceId: "63b0c1dbf2a30bdfddcfe1ac", - EnvironmentName: "dev", + WorkspaceId: workspaceFile.WorkspaceId, + EnvironmentName: environmentName, SecretIds: validSecretIdsToDelete, } @@ -260,13 +328,22 @@ var secretsDeleteCmd = &cobra.Command{ func init() { secretsCmd.AddCommand(secretsGetCmd) + secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets") secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsDeleteCmd) + secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on") rootCmd.AddCommand(secretsCmd) } func getSecretsByNames(cmd *cobra.Command, args []string) { - secrets, err := util.GetAllEnvironmentVariables("", "dev") + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + log.Errorln("Unable to parse the environment name flag") + log.Debugln(err) + return + } + + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { log.Error("Unable to retrieve secrets. Run with -d to see full logs") log.Debug(err) diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index f3ee274b3..44f14a12b 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -10,6 +10,8 @@ const ( CONFIG_FOLDER_NAME = ".infisical" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" + SECRET_TYPE_PERSONAL = "personal" + SECRET_TYPE_SHARED = "shared" ) var INFISICAL_URL = "https://app.infisical.com/api" diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 8d9719698..66d2b56e3 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -388,3 +388,17 @@ func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []m return secretsToReturn } + +func IsSecretEnvironmentValid(env string) bool { + if env == "prod" || env == "dev" || env == "test" || env == "staging" { + return true + } + return false +} + +func IsSecretTypeValid(s string) bool { + if s == "personal" || s == "shared" { + return true + } + return false +} From 679db32de95634ffdde3d440725d608c03c1eef6 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 3 Jan 2023 10:49:58 +0700 Subject: [PATCH 43/91] Begin docs for secret versioning, snapshots, and audit logs --- docs/getting-started/dashboard/audit-logs.mdx | 9 +++++++++ docs/getting-started/dashboard/integrations.mdx | 7 +++---- docs/getting-started/dashboard/pit-recovery.mdx | 5 +++++ docs/getting-started/dashboard/versioning.mdx | 5 +++++ docs/getting-started/features.mdx | 14 +++++++------- docs/mint.json | 3 +++ 6 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 docs/getting-started/dashboard/audit-logs.mdx create mode 100644 docs/getting-started/dashboard/pit-recovery.mdx create mode 100644 docs/getting-started/dashboard/versioning.mdx diff --git a/docs/getting-started/dashboard/audit-logs.mdx b/docs/getting-started/dashboard/audit-logs.mdx new file mode 100644 index 000000000..bb31423b4 --- /dev/null +++ b/docs/getting-started/dashboard/audit-logs.mdx @@ -0,0 +1,9 @@ +--- +title: "Activity Logs" +--- + +Activity logs record all actions going through Infisical including CRUD operations applied to environment variables. They help answer questions like: + +- Who added or updated environment variables recently? +- Did Bob read environment variables last week (if at all)? +- What IP address was used for that action? diff --git a/docs/getting-started/dashboard/integrations.mdx b/docs/getting-started/dashboard/integrations.mdx index de25fa861..ce2904e38 100644 --- a/docs/getting-started/dashboard/integrations.mdx +++ b/docs/getting-started/dashboard/integrations.mdx @@ -4,11 +4,10 @@ title: "Integrations" Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production. -We're still early with integrations, but expect more soon. +We're still early with integrations, but expect more soon. - - View all available integrations and their guide + + View all available integrations and their guides ![integrations](../../images/project-integrations.png) - diff --git a/docs/getting-started/dashboard/pit-recovery.mdx b/docs/getting-started/dashboard/pit-recovery.mdx new file mode 100644 index 000000000..534cc2718 --- /dev/null +++ b/docs/getting-started/dashboard/pit-recovery.mdx @@ -0,0 +1,5 @@ +--- +title: "Point-in-Time Recovery" +--- + +Point-in-time (PIT) recovery allows environment variables to be rolled back to any point in time. It's powered by snapshots that get captured after mutations to environment variables. diff --git a/docs/getting-started/dashboard/versioning.mdx b/docs/getting-started/dashboard/versioning.mdx new file mode 100644 index 000000000..3a6ba2e2c --- /dev/null +++ b/docs/getting-started/dashboard/versioning.mdx @@ -0,0 +1,5 @@ +--- +title: "Secret Versioning" +--- + +Secret versioning allows an individual environment variable to be rolled back without touching other project environment variables. diff --git a/docs/getting-started/features.mdx b/docs/getting-started/features.mdx index c520dd905..0205f0a46 100644 --- a/docs/getting-started/features.mdx +++ b/docs/getting-started/features.mdx @@ -4,14 +4,14 @@ title: "Features" This is a non-exhaustive list of features that Infisical offers: -## Web UI +## Platform -The Web UI is used to manage teams and environment variables. - -- Provision access to organizations and projects. -- Add/delete/update, scope, search, sort, hide-unhide environment variables. -- Separate environment variables by environment. -- Import environment variables via drag-and-drop, export them as a .env file. +- Provision members access to organizations and projects. +- Manage secrets by adding, deleting, updating them across environments; search, sort, hide/un-hide, export/import them. +- Sync secrets to platforms via integrations to platforms like GitHub, Vercel, and Netlify. +- Rollback secrets to any point in time. +- Rollback each secrets to any version. +- Track actions through activity logs. ## CLI diff --git a/docs/mint.json b/docs/mint.json index e94b70a6b..bba08787c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -80,6 +80,9 @@ "getting-started/dashboard/organization", "getting-started/dashboard/project", "getting-started/dashboard/integrations", + "getting-started/dashboard/pit-recovery", + "getting-started/dashboard/versioning", + "getting-started/dashboard/audit-logs", "getting-started/dashboard/token" ] }, From fe0c46652355c5ce3b8ff5cabefe310f9f104e4d Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Mon, 2 Jan 2023 20:17:16 -0800 Subject: [PATCH 44/91] Moved the delete button to the sidebar --- .../dashboard/DashboardInputField.tsx | 10 +- frontend/components/dashboard/KeyPair.tsx | 20 +-- frontend/components/dashboard/SideBar.tsx | 159 ++++++++++-------- frontend/pages/dashboard/[id].tsx | 15 +- 4 files changed, 106 insertions(+), 98 deletions(-) diff --git a/frontend/components/dashboard/DashboardInputField.tsx b/frontend/components/dashboard/DashboardInputField.tsx index 29761bf07..a667c39d0 100644 --- a/frontend/components/dashboard/DashboardInputField.tsx +++ b/frontend/components/dashboard/DashboardInputField.tsx @@ -53,7 +53,7 @@ const DashboardInputField = ({ return (
@@ -85,7 +85,7 @@ const DashboardInputField = ({ return (
{override == true &&
Override enabled
} - {value.split(REGEX).map((word, id) => { + {value?.split(REGEX).map((word, id) => { if (word.match(REGEX) !== null) { return ( @@ -139,7 +139,7 @@ const DashboardInputField = ({ })}
{blurred && ( -
+
{value.split('').map(() => ( void; modifyKey: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void; isBlurred: boolean; @@ -28,7 +27,6 @@ interface KeyPairProps { * This component represent a single row for an environemnt variable on the dashboard * @param {object} obj * @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private) - * @param {function} obj.deleteRow - a function to delete a certain keyPair * @param {function} obj.modifyKey - modify the key of a certain environment variable * @param {function} obj.modifyValue - modify the value of a certain environment variable * @param {boolean} obj.isBlurred - if the blurring setting is turned on @@ -39,7 +37,6 @@ interface KeyPairProps { */ const KeyPair = ({ keyPair, - deleteRow, modifyKey, modifyValue, isBlurred, @@ -57,7 +54,7 @@ const KeyPair = ({
}
-
+
-
-
+
+
-
toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"> +
toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
-
-
-
); diff --git a/frontend/components/dashboard/SideBar.tsx b/frontend/components/dashboard/SideBar.tsx index 0a9fca6a1..41ac9c144 100644 --- a/frontend/components/dashboard/SideBar.tsx +++ b/frontend/components/dashboard/SideBar.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import Image from 'next/image'; import { useTranslation } from "next-i18next"; import { faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -40,6 +41,7 @@ interface SideBarProps { savePush: () => void; sharedToHide: string[]; setSharedToHide: (values: string[]) => void; + deleteRow: any; } /** @@ -54,6 +56,7 @@ interface SideBarProps { * @param {function} obj.savePush - save changes andp ush secrets * @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden. * @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually + * @param {function} obj.deleteRow - a function to delete a certain keyPair * @returns the sidebar with 'secret's settings' */ const SideBar = ({ @@ -67,93 +70,97 @@ const SideBar = ({ buttonReady, savePush, sharedToHide, - setSharedToHide + setSharedToHide, + deleteRow }: SideBarProps) => { + const [isLoading, setIsLoading] = useState(false); const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal")); const { t } = useTranslation(); return
-
-
-

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

-
toggleSidebar("None")}> - + {isLoading ? ( +
+ infisical loading indicator +
+ ) : ( +
+
+

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

+
toggleSidebar("None")}> + +
-
-
-

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

- -
- {data.filter(secret => secret.type == "shared")[0]?.value - ?
-

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

- secret.type == "shared")[0]?.pos} - value={data.filter(secret => secret.type == "shared")[0]?.value} - isDuplicate={false} - blurred={true} - /> -
- secret.type == "shared")[0]?.pos} /> -
-
- :
- {t("common:note")}: - {t("dashboard:sidebar.personal-explanation")} -
} -
- {data.filter(secret => secret.type == "shared")[0]?.value && -
-

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

- +

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

+ -
} -
+
+ {data.filter(secret => secret.type == "shared")[0]?.value + ?
+

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

secret.type == "personal")[0]?.pos : data[0]?.pos} - value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value} + position={data.filter(secret => secret.type == "shared")[0]?.pos} + value={data.filter(secret => secret.type == "shared")[0]?.value} isDuplicate={false} - blurred={true} + blurred={true} /> -
- secret.type == "personal")[0]?.pos : data[0]?.pos} /> +
+ secret.type == "shared")[0]?.pos} />
+ :
+ {t("common:note")}: + {t("dashboard:sidebar.personal-explanation")} +
} +
+ {data.filter(secret => secret.type == "shared")[0]?.value && +
+

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

+ +
} +
+ secret.type == "personal")[0]?.pos : data[0]?.pos} + value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value} + isDuplicate={false} + blurred={true} + /> +
+ secret.type == "personal")[0]?.pos : data[0]?.pos} /> +
+
+
+ + secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
- {/*
-

Group

- {}} - data={["Group1"]} - isFull={true} - /> -
*/} - - secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} /> -
+ )}
}; diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index 60fcc7a00..d61ff4afb 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -241,9 +241,14 @@ export default function Dashboard() { sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); }; - const deleteRow = (id: string) => { + const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { setButtonReady(true); - setData(data!.filter((row: SecretDataProps) => row.id !== id)); + toggleSidebar("None"); + createNotification({ + text: `${secretName} has been deleted. Remember to save changes.`, + type: 'error' + }); + setData(data!.filter((row: SecretDataProps) => !ids.includes(row.id))); }; /** @@ -395,8 +400,8 @@ export default function Dashboard() { alink.click(); }; - const deleteCertainRow = (id: string) => { - deleteRow(id); + const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { + deleteRow({ids, secretName}); }; /** @@ -438,6 +443,7 @@ export default function Dashboard() { savePush={savePush} sharedToHide={sharedToHide} setSharedToHide={setSharedToHide} + deleteRow={deleteCertainRow} />}
@@ -585,7 +591,6 @@ export default function Dashboard() { Date: Mon, 2 Jan 2023 20:41:20 -0800 Subject: [PATCH 45/91] Moved project id from dashboard to settings --- frontend/pages/dashboard/[id].tsx | 45 ------------------- frontend/pages/settings/project/[id].js | 59 ++++++++++++++++++++----- 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index d61ff4afb..24c0a850c 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -86,7 +86,6 @@ export default function Dashboard() { const [isNew, setIsNew] = useState(false); const [searchKeys, setSearchKeys] = useState(''); const [errorDragAndDrop, setErrorDragAndDrop] = useState(false); - const [projectIdCopied, setProjectIdCopied] = useState(false); const [sortMethod, setSortMethod] = useState('alphabetical'); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false); @@ -404,23 +403,6 @@ export default function Dashboard() { deleteRow({ids, secretName}); }; - /** - * This function copies the project id to the clipboard - */ - function copyToClipboard() { - const copyText = document.getElementById('myInput') as HTMLInputElement; - - if (copyText) { - copyText.select(); - copyText.setSelectionRange(0, 99999); // For mobile devices - - navigator.clipboard.writeText(copyText.value); - - setProjectIdCopied(true); - setTimeout(() => setProjectIdCopied(false), 2000); - } - } - return data ? (
@@ -470,33 +452,6 @@ export default function Dashboard() { )}
-
-

{`${t( - "common:project-id" - )}:`}

- -
- - - {t("common:click-to-copy")} - -
-
{(data?.length !== 0 || buttonReady) && (
+ + {t("common:click-to-copy")} + +
From c7c5a947d25410142c359da53058063042933783 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 3 Jan 2023 15:53:06 +0700 Subject: [PATCH 46/91] Modify secret snapshots to point to secret versions --- backend/src/app.ts | 2 + backend/src/ee/controllers/v1/index.ts | 2 + .../v1/secretSnapshotController.ts | 27 ++++++ backend/src/ee/helpers/secret.ts | 44 ++++++---- backend/src/ee/middleware/index.ts | 7 ++ .../middleware/requireSecretSnapshotAuth.ts | 50 +++++++++++ backend/src/ee/models/secretSnapshot.ts | 86 ++----------------- backend/src/ee/models/secretVersion.ts | 41 +++++++++ backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/routes/v1/secret.ts | 2 +- backend/src/ee/routes/v1/secretSnapshot.ts | 26 ++++++ backend/src/helpers/secret.ts | 78 ++++++----------- backend/src/types/express/index.d.ts | 1 + backend/src/utils/errors.ts | 10 +++ 14 files changed, 229 insertions(+), 149 deletions(-) create mode 100644 backend/src/ee/controllers/v1/secretSnapshotController.ts create mode 100644 backend/src/ee/middleware/index.ts create mode 100644 backend/src/ee/middleware/requireSecretSnapshotAuth.ts create mode 100644 backend/src/ee/routes/v1/secretSnapshot.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 9fba18c67..4b02e0311 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,6 +14,7 @@ import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, secret as eeSecretRouter, + secretSnapshot as eeSecretSnapshotRouter, action as eeActionRouter } from './ee/routes/v1'; import { @@ -69,6 +70,7 @@ if (NODE_ENV === 'production') { // (EE) routes app.use('/api/v1/secret', eeSecretRouter); +app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter); app.use('/api/v1/workspace', eeWorkspaceRouter); app.use('/api/v1/action', eeActionRouter); diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index 2a082de70..dd88f1178 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -1,11 +1,13 @@ import * as stripeController from './stripeController'; import * as secretController from './secretController'; +import * as secretSnapshotController from './secretSnapshotController'; import * as workspaceController from './workspaceController'; import * as actionController from './actionController'; export { stripeController, secretController, + secretSnapshotController, workspaceController, actionController } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts new file mode 100644 index 000000000..40e1a74a6 --- /dev/null +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -0,0 +1,27 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { SecretSnapshot } from '../../models'; + +export const getSecretSnapshot = async (req: Request, res: Response) => { + let secretSnapshot; + try { + const { secretSnapshotId } = req.params; + + secretSnapshot = await SecretSnapshot + .findById(secretSnapshotId) + .populate('secretVersions'); + + if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get secret snapshot' + }); + } + + return res.status(200).send({ + secretSnapshot + }); +} \ No newline at end of file diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 2f726a9a5..529c9a980 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -23,34 +23,44 @@ import { }: { workspaceId: string; }) => { + let secretSnapshot; try { - const secrets = await Secret.find({ + const secretIds = (await Secret.find({ workspace: workspaceId - }); + }, '_id')).map((s) => s._id); + const latestSecretVersions = (await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds + } + } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' }, + versionId: { $max: '$_id' } // secret version id + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()) + .map((s) => s.versionId); + const latestSecretSnapshot = await SecretSnapshot.findOne({ workspace: workspaceId }).sort({ version: -1 }); - if (!latestSecretSnapshot) { - // case: no snapshots exist for workspace -> create first snapshot - await new SecretSnapshot({ - workspace: workspaceId, - version: 1, - secrets - }).save(); - - return; - } - - // case: snapshots exist for workspace secretSnapshot = await new SecretSnapshot({ workspace: workspaceId, - version: latestSecretSnapshot.version + 1, - secrets + version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, + secretVersions: latestSecretVersions }).save(); - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/ee/middleware/index.ts b/backend/src/ee/middleware/index.ts new file mode 100644 index 000000000..ff9267965 --- /dev/null +++ b/backend/src/ee/middleware/index.ts @@ -0,0 +1,7 @@ +import requireLicenseAuth from './requireLicenseAuth'; +import requireSecretSnapshotAuth from './requireSecretSnapshotAuth'; + +export { + requireLicenseAuth, + requireSecretSnapshotAuth +} \ No newline at end of file diff --git a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts new file mode 100644 index 000000000..71d7c5215 --- /dev/null +++ b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts @@ -0,0 +1,50 @@ +import { Request, Response, NextFunction } from 'express'; +import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors'; +import { SecretSnapshot } from '../models'; +import { + validateMembership +} from '../../helpers/membership'; + +/** + * Validate if user on request has proper membership for secret snapshot + * @param {Object} obj + * @param {String[]} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.acceptedStatuses - accepted workspace statuses + * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing + */ +const requireSecretSnapshotAuth = ({ + acceptedRoles, + acceptedStatuses +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const { secretSnapshotId } = req.params; + + const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId); + + if (!secretSnapshot) { + return next(SecretSnapshotNotFoundError({ + message: 'Failed to find secret snapshot' + })); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: secretSnapshot.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.secretSnapshot = secretSnapshot as any; + + next(); + } catch (err) { + return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' })); + } + } +} + +export default requireSecretSnapshotAuth; \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts index 69633a92e..c646f353a 100644 --- a/backend/src/ee/models/secretSnapshot.ts +++ b/backend/src/ee/models/secretSnapshot.ts @@ -1,31 +1,9 @@ import { Schema, model, Types } from 'mongoose'; -import { - SECRET_SHARED, - SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD -} from '../../variables'; export interface ISecretSnapshot { workspace: Types.ObjectId; version: number; - secrets: { - version: number; - workspace: Types.ObjectId; - type: string; - user: Types.ObjectId; - environment: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretKeyHash: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHash: string; - }[] + secretVersions: Types.ObjectId[]; } const secretSnapshotSchema = new Schema( @@ -39,64 +17,10 @@ const secretSnapshotSchema = new Schema( type: Number, required: true }, - secrets: [{ - version: { - type: Number, - default: 1, - required: true - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - type: { - type: String, - enum: [SECRET_SHARED, SECRET_PERSONAL], - required: true - }, - user: { - // user associated with the personal secret - type: Schema.Types.ObjectId, - ref: 'User' - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], - required: true - }, - secretKeyCiphertext: { - type: String, - required: true - }, - secretKeyIV: { - type: String, // symmetric - required: true - }, - secretKeyTag: { - type: String, // symmetric - required: true - }, - secretKeyHash: { - type: String, - required: true - }, - secretValueCiphertext: { - type: String, - required: true - }, - secretValueIV: { - type: String, // symmetric - required: true - }, - secretValueTag: { - type: String, // symmetric - required: true - }, - secretValueHash: { - type: String, - required: true - } + secretVersions: [{ + type: Schema.Types.ObjectId, + ref: 'SecretVersion', + required: true }] }, { diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index a93a037f6..0197c3a25 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -1,9 +1,30 @@ import { Schema, model, Types } from 'mongoose'; +import { + SECRET_SHARED, + SECRET_PERSONAL, + ENV_DEV, + ENV_TESTING, + ENV_STAGING, + ENV_PROD +} from '../../variables'; + +/** + * TODO: + * 1. Modify SecretVersion to also contain XX + * - type + * - user + * - environment + * 2. Modify SecretSnapshot to point to arrays of SecretVersion + */ export interface ISecretVersion { _id?: Types.ObjectId; secret: Types.ObjectId; version: number; + workspace: Types.ObjectId; // new + type: string; // new + user: Types.ObjectId; // new + environment: string; // new isDeleted: boolean; secretKeyCiphertext: string; secretKeyIV: string; @@ -27,6 +48,26 @@ const secretVersionSchema = new Schema( default: 1, required: true }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: 'User' + }, + environment: { + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], + required: true + }, isDeleted: { type: Boolean, default: false, diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 02fc80939..612715111 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,9 +1,11 @@ import secret from './secret'; +import secretSnapshot from './secretSnapshot'; import workspace from './workspace'; import action from './action'; export { secret, + secretSnapshot, workspace, action } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 7217c96e2..cb6897994 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -5,7 +5,7 @@ import { requireSecretAuth, validateRequest } from '../../../middleware'; -import { body, query, param } from 'express-validator'; +import { query, param } from 'express-validator'; import { secretController } from '../../controllers/v1'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts new file mode 100644 index 000000000..04f6c9d59 --- /dev/null +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -0,0 +1,26 @@ +import express from 'express'; +const router = express.Router(); +import { + requireSecretSnapshotAuth +} from '../../middleware'; +import { + requireAuth, + validateRequest +} from '../../../middleware'; +import { param } from 'express-validator'; +import { ADMIN, MEMBER, GRANTED } from '../../../variables'; +import { secretSnapshotController } from '../../controllers/v1'; + +router.get( + '/:secretSnapshotId', + requireAuth, + requireSecretSnapshotAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('secretSnapshotId').exists().trim(), + validateRequest, + secretSnapshotController.getSecretSnapshot +); + +export default router; \ No newline at end of file diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index f71f094e8..0d64da3b1 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Secret, ISecret, @@ -8,7 +9,6 @@ import { EELogService } from '../ee/services'; import { - SecretVersion, IAction } from '../ee/models'; import { @@ -104,11 +104,9 @@ const v1PushSecrets = async ({ await Secret.deleteMany({ _id: { $in: toDelete } }); - - await SecretVersion.updateMany({ - secret: { $in: toDelete } - }, { - isDeleted: true + + await EESecretService.markDeletedSecretVersions({ + secretIds: toDelete }); } @@ -191,6 +189,10 @@ const v1PushSecrets = async ({ return ({ secret: _id, version: version ? version + 1 : 1, + workspace: new Types.ObjectId(workspaceId), + type: newSecret.type, + user: new Types.ObjectId(userId), + environment, isDeleted: false, secretKeyCiphertext: newSecret.ciphertextKey, secretKeyIV: newSecret.ivKey, @@ -242,6 +244,11 @@ const v1PushSecrets = async ({ EESecretService.addSecretVersions({ secretVersions: newSecrets.map(({ _id, + version, + workspace, + type, + user, + environment, secretKeyCiphertext, secretKeyIV, secretKeyTag, @@ -252,7 +259,11 @@ const v1PushSecrets = async ({ secretValueHash }) => ({ secret: _id, - version: 1, + version, + workspace, + type, + user, + environment, isDeleted: false, secretKeyCiphertext, secretKeyIV, @@ -419,29 +430,14 @@ const v1PushSecrets = async ({ // (EE) add secret versions for updated secrets await EESecretService.addSecretVersions({ secretVersions: toUpdate.map((s) => { - const { - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; - return ({ + ...newSecretsObj[`${s.type}-${s.secretKeyHash}`], secret: s._id, version: s.version ? s.version + 1 : 1, - isDeleted: false, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash + workspace: new Types.ObjectId(workspaceId), + user: s.user, + environment: s.environment, + isDeleted: false }) }) }); @@ -474,31 +470,13 @@ const v1PushSecrets = async ({ // (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 + secretVersions: newSecrets.map((s) => ({ + ...s, + secret: s._id, + isDeleted: false })) }); - + const addAction = await EELogService.createActionSecret({ name: ACTION_ADD_SECRETS, userId, diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 68961a946..33b10e2bc 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -13,6 +13,7 @@ declare global { integrationAuth: any; bot: any; secret: any; + secretSnapshot: any; serviceToken: any; accessToken: any; query?: any; diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index ba5611465..3c6a385e5 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -123,6 +123,16 @@ export const SecretNotFoundError = (error?: Partial) => new stack: error?.stack }); +//* ----->[SECRET SNAPSHOT ERRORS]<----- +export const SecretSnapshotNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'secret_snapshot_not_found_error', + message: error?.message ?? 'The requested secret snapshot was not found', + context: error?.context, + stack: error?.stack +}); + //* ----->[ACTION ERRORS]<----- export const ActionNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, From fb394de4285557ff8f9dc69cc504aeaa4b960e00 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 3 Jan 2023 16:02:05 +0700 Subject: [PATCH 47/91] Remove unecessary imports --- backend/src/controllers/v2/workspaceController.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 95c03bbc4..637801fa2 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,29 +1,17 @@ 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'; +import { ENV_SET } from '../../variables'; interface V2PushSecret { type: string; // personal or shared From 7e71e3ca570a02649a291c033a5ef99766fffdcb Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 16:09:47 -0500 Subject: [PATCH 48/91] v1 crud secrets complete --- cli/packages/cmd/secrets.go | 76 +++++++++++++++++++++++++------ cli/packages/http/api.go | 20 ++++++++ cli/packages/models/api.go | 5 ++ cli/packages/visualize/secrets.go | 2 +- 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 8960b6603..d76890ea6 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -35,6 +35,12 @@ var secretsCmd = &cobra.Command{ return } + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return + } + secrets, err := util.GetAllEnvironmentVariables("", environmentName) secrets = util.SubstituteSecrets(secrets) if err != nil { @@ -57,24 +63,24 @@ var secretsGetCmd = &cobra.Command{ } var secretsSetCmd = &cobra.Command{ - Example: `secrets set ..."`, - Short: "Used update retrieve secrets by name", + Example: `secrets set ..."`, + Short: "Used set secrets", Use: "set [secrets]", DisableFlagsInUseLine: true, PreRun: toggleDebug, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - secretType, err := cmd.Flags().GetString("type") - if err != nil { - log.Errorln("Unable to parse the secret type flag") - log.Debugln(err) - return - } + // secretType, err := cmd.Flags().GetString("type") + // if err != nil { + // log.Errorln("Unable to parse the secret type flag") + // log.Debugln(err) + // return + // } - if !util.IsSecretTypeValid(secretType) { - log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType) - return - } + // if !util.IsSecretTypeValid(secretType) { + // log.Errorf("secret type can only be `personal` or `shared`. You have entered [%v]", secretType) + // return + // } environmentName, err := cmd.Flags().GetString("env") if err != nil { @@ -91,6 +97,7 @@ var secretsSetCmd = &cobra.Command{ workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() if !workspaceFileExists { log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return } workspaceFile, err := util.GetWorkSpaceFromFile() @@ -144,8 +151,15 @@ var secretsSetCmd = &cobra.Command{ log.Debug(err) } + type SecretSetOperation struct { + SecretKey string + SecretValue string + SecretOperation string + } + secretsToCreate := []models.Secret{} secretsToModify := []models.Secret{} + secretOperations := []SecretSetOperation{} secretByKey := getSecretsByKeys(secrets) @@ -161,7 +175,8 @@ var secretsSetCmd = &cobra.Command{ return } - key := splitKeyValueFromArg[0] + // Key and value from argument + key := strings.ToUpper(splitKeyValueFromArg[0]) value := splitKeyValueFromArg[1] hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) @@ -189,6 +204,18 @@ var secretsSetCmd = &cobra.Command{ // Only add to modifications if the value is different if existingSecret.Value != value { secretsToModify = append(secretsToModify, encryptedSecretDetails) + secretOperations = append(secretOperations, SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE MODIFIED", + }) + } else { + // Current value is same as exisitng so no change + secretOperations = append(secretOperations, SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE UNCHANGED", + }) } } else { @@ -202,9 +229,14 @@ var secretsSetCmd = &cobra.Command{ SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, - Type: secretType, + Type: util.SECRET_TYPE_SHARED, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) + secretOperations = append(secretOperations, SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET CREATED", + }) } } @@ -236,7 +268,14 @@ var secretsSetCmd = &cobra.Command{ } } - log.Infoln("secrets have been successfully set") + // Print secret operations + headers := []string{"SECRET NAME", "SECRET VALUE", "STATUS"} + rows := [][]string{} + for _, secretOperation := range secretOperations { + rows = append(rows, []string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) + } + + visualize.Table(headers, rows) }, } @@ -274,6 +313,7 @@ var secretsDeleteCmd = &cobra.Command{ workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() if !workspaceFileExists { log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return } workspaceFile, err := util.GetWorkSpaceFromFile() @@ -343,6 +383,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { return } + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") + return + } + secrets, err := util.GetAllEnvironmentVariables("", environmentName) if err != nil { log.Error("Unable to retrieve secrets. Run with -d to see full logs") diff --git a/cli/packages/http/api.go b/cli/packages/http/api.go index 67f8042c2..01b8a8a40 100644 --- a/cli/packages/http/api.go +++ b/cli/packages/http/api.go @@ -80,3 +80,23 @@ func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request models.GetEn return result, nil } + +func CallGetEncryptedSecretsByWorkspaceIdAndEnv(httpClient resty.Client, request models.GetSecretsByWorkspaceIdAndEnvironmentRequest) (models.PullSecretsResponse, error) { + var pullSecretsRequestResponse models.PullSecretsResponse + response, err := httpClient. + R(). + SetQueryParam("environment", request.EnvironmentName). + SetQueryParam("channel", "cli"). + SetResult(&pullSecretsRequestResponse). + Get(fmt.Sprintf("%v/v1/secret/%v", util.INFISICAL_URL, request.WorkspaceId)) + + if err != nil { + return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unable to complete api request [err=%s]", err) + } + + if response.StatusCode() > 299 { + return models.PullSecretsResponse{}, fmt.Errorf("CallGetEncryptedSecretsByWorkspaceIdAndEnv: Unsuccessful response: [response=%s]", response) + } + + return pullSecretsRequestResponse, nil +} diff --git a/cli/packages/models/api.go b/cli/packages/models/api.go index 5566cdb8b..d17200b87 100644 --- a/cli/packages/models/api.go +++ b/cli/packages/models/api.go @@ -191,3 +191,8 @@ type GetEncryptedWorkspaceKeyResponse struct { UpdatedAt time.Time `json:"updatedAt"` } `json:"latestKey"` } + +type GetSecretsByWorkspaceIdAndEnvironmentRequest struct { + EnvironmentName string `json:"environmentName"` + WorkspaceId string `json:"workspaceId"` +} diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go index e732d06af..e9ac5d297 100644 --- a/cli/packages/visualize/secrets.go +++ b/cli/packages/visualize/secrets.go @@ -8,7 +8,7 @@ func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { rows = append(rows, []string{secret.Key, secret.Value, secret.Type}) } - headers := []string{"Secret name", "Secret vaule", "Secret type"} + headers := []string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"} Table(headers, rows) } From 59f5ad7710fe904b738f09acdaa993729a31c736 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 16:39:33 -0500 Subject: [PATCH 49/91] add expand flag to crud sli --- cli/packages/cmd/secrets.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index d76890ea6..5e6c4ca16 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -35,6 +35,13 @@ var secretsCmd = &cobra.Command{ return } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") + if err != nil { + log.Errorln("Unable to parse the substitute flag") + log.Debugln(err) + return + } + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() if !workspaceFileExists { log.Error("You have not yet connected to an Infisical Project. Please run [infisical init]") @@ -42,7 +49,11 @@ var secretsCmd = &cobra.Command{ } secrets, err := util.GetAllEnvironmentVariables("", environmentName) - secrets = util.SubstituteSecrets(secrets) + + if shouldExpandSecrets { + secrets = util.SubstituteSecrets(secrets) + } + if err != nil { log.Debugln(err) return @@ -333,7 +344,7 @@ var secretsDeleteCmd = &cobra.Command{ invalidSecretNamesThatDoNotExist := []string{} for _, secretKeyFromArg := range args { - if value, ok := secretByKey[secretKeyFromArg]; ok { + if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok { validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) } else { invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) @@ -368,10 +379,11 @@ var secretsDeleteCmd = &cobra.Command{ func init() { secretsCmd.AddCommand(secretsGetCmd) - secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets") + // secretsSetCmd.Flags().String("type", "shared", "Used to set the type for secrets") secretsCmd.AddCommand(secretsSetCmd) secretsCmd.AddCommand(secretsDeleteCmd) secretsCmd.PersistentFlags().String("env", "dev", "Used to define the environment name on which actions should be taken on") + secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") rootCmd.AddCommand(secretsCmd) } @@ -403,7 +415,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { } for _, secretKeyFromArg := range args { - if value, ok := secretsMap[secretKeyFromArg]; ok { + if value, ok := secretsMap[strings.ToUpper(secretKeyFromArg)]; ok { requestedSecrets = append(requestedSecrets, value) } else { requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{ From 3e945dd5522fcc2c35a6b0a5a32d165894e257e3 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 16:40:08 -0500 Subject: [PATCH 50/91] move v2 secret api to controller --- backend/src/controllers/v2/index.ts | 6 +- .../src/controllers/v2/secretController.ts | 138 +++++++++++++++++ backend/src/routes/v2/secret.ts | 144 ++---------------- 3 files changed, 153 insertions(+), 135 deletions(-) diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index dc6977c91..24ca015ac 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,5 +1,7 @@ import * as workspaceController from './workspaceController'; +import * as secretController from './secretController'; -export { - workspaceController +export { + workspaceController, + secretController } diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index e69de29bb..e29a8d19b 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -0,0 +1,138 @@ +import to from "await-to-js"; +import { Request, Response } from "express"; +import mongoose, { Types } from "mongoose"; +import Secret, { ISecret } from "../../models/secret"; +import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret/types"; +const { ValidationError } = mongoose.Error; +import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; +import { AnyBulkWriteOperation } from 'mongodb'; + +export const batchCreateSecrets = async (req: Request, res: Response) => { + const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] + + secretsToCreate.forEach(rawSecret => { + const safeUpdateFields: SanitizedSecretForCreate = { + secretKeyCiphertext: rawSecret.secretKeyCiphertext, + secretKeyIV: rawSecret.secretKeyIV, + secretKeyTag: rawSecret.secretKeyTag, + secretKeyHash: rawSecret.secretKeyHash, + secretValueCiphertext: rawSecret.secretValueCiphertext, + secretValueIV: rawSecret.secretValueIV, + secretValueTag: rawSecret.secretValueTag, + secretValueHash: rawSecret.secretValueHash, + secretCommentCiphertext: rawSecret.secretCommentCiphertext, + secretCommentIV: rawSecret.secretCommentIV, + secretCommentTag: rawSecret.secretCommentTag, + secretCommentHash: rawSecret.secretCommentHash, + workspace: new Types.ObjectId(workspaceId), + environment: environmentName, + type: rawSecret.type, + user: new Types.ObjectId(req.user._id) + } + + sanitizedSecretesToCreate.push(safeUpdateFields) + }) + + const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) + if (bulkCreateError) { + if (bulkCreateError instanceof ValidationError) { + throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) + } + + throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) + } + + res.status(200).send() +} + + +export const createSingleSecret = async (req: Request, res: Response) => { + try { + const secretFromDB = await Secret.findById(req.params.secretId) + return res.status(200).send(secretFromDB); + } catch (e) { + throw BadRequestError({ message: "Unable to find the requested secret" }) + } +} + +export const batchDeleteSecrets = async (req: Request, res: Response) => { + const { workspaceId, environmentName } = req.params + const secretIdsToDelete: string[] = req.body.secretIds + + const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanDeleteError) { + throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) + } + + const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + + secretIdsToDelete.forEach(secretIdToDelete => { + if (secretsUserCanDeleteSet.has(secretIdToDelete)) { + const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } + deleteOperationsToPerform.push(deleteOperation) + } else { + throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) + } + }) + + const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkDeleteError) { + if (bulkDeleteError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) + } + throw InternalServerError() + } + + res.status(200).send() +} + +export const batchModifySecrets = async (req: Request, res: Response) => { + const { workspaceId, environmentName } = req.params + const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; + const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanModifyError) { + throw InternalServerError({ message: "Unable to fetch secrets you own" }) + } + + const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); + const updateOperationsToPerform: any = [] + + + secretsModificationsRequested.forEach(userModifiedSecret => { + if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { + const sanitizedSecret: SanitizedSecretModify = { + secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, + secretKeyIV: userModifiedSecret.secretKeyIV, + secretKeyTag: userModifiedSecret.secretKeyTag, + secretKeyHash: userModifiedSecret.secretKeyHash, + secretValueCiphertext: userModifiedSecret.secretValueCiphertext, + secretValueIV: userModifiedSecret.secretValueIV, + secretValueTag: userModifiedSecret.secretValueTag, + secretValueHash: userModifiedSecret.secretValueHash, + secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, + secretCommentIV: userModifiedSecret.secretCommentIV, + secretCommentTag: userModifiedSecret.secretCommentTag, + secretCommentHash: userModifiedSecret.secretCommentHash, + } + + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } + updateOperationsToPerform.push(updateOperation) + } else { + throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) + if (bulkModificationInfoError) { + if (bulkModificationInfoError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) + } + + throw InternalServerError() + } + + return res.status(200).send() +} \ No newline at end of file diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 8c87db882..477e0039e 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,14 +1,9 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { ISecret, Secret } from '../../models'; -import { body, param, query, check } from 'express-validator'; -import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; +import { body, param } from 'express-validator'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { SanitizedSecretModify, CreateSecretRequestBody, SanitizedSecretForCreate, ModifySecretRequestBody } from '../../types/secret/types'; -import to from 'await-to-js'; -import mongoose, { Types } from 'mongoose'; -import { AnyBulkWriteOperation } from 'mongodb'; -const { ValidationError } = mongoose.Error; +import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; +import { secretController } from '../../controllers/v2'; const router = express.Router(); @@ -26,45 +21,7 @@ router.post( param('environmentName').exists().trim(), body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')), validateRequest, - async (req: Request, res: Response) => { - const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; - const { workspaceId, environmentName } = req.params - const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] - - secretsToCreate.forEach(rawSecret => { - const safeUpdateFields: SanitizedSecretForCreate = { - secretKeyCiphertext: rawSecret.secretKeyCiphertext, - secretKeyIV: rawSecret.secretKeyIV, - secretKeyTag: rawSecret.secretKeyTag, - secretKeyHash: rawSecret.secretKeyHash, - secretValueCiphertext: rawSecret.secretValueCiphertext, - secretValueIV: rawSecret.secretValueIV, - secretValueTag: rawSecret.secretValueTag, - secretValueHash: rawSecret.secretValueHash, - secretCommentCiphertext: rawSecret.secretCommentCiphertext, - secretCommentIV: rawSecret.secretCommentIV, - secretCommentTag: rawSecret.secretCommentTag, - secretCommentHash: rawSecret.secretCommentHash, - workspace: new Types.ObjectId(workspaceId), - environment: environmentName, - type: rawSecret.type, - user: new Types.ObjectId(req.user._id) - } - - sanitizedSecretesToCreate.push(safeUpdateFields) - }) - - const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) - if (bulkCreateError) { - if (bulkCreateError instanceof ValidationError) { - throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) - } - - throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) - } - - res.status(200).send() - } + secretController.batchCreateSecrets ); /** @@ -76,14 +33,8 @@ router.get( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.findById(req.params.secretId) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } + validateRequest, + secretController.createSingleSecret ); /** @@ -99,37 +50,9 @@ router.delete( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), - validateRequest, async (req: Request, res: Response) => { - const { workspaceId, environmentName } = req.params - const secretIdsToDelete: string[] = req.body.secretIds + validateRequest, + secretController.batchDeleteSecrets - const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) - if (secretIdsUserCanDeleteError) { - throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) - } - - const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); - const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] - - secretIdsToDelete.forEach(secretIdToDelete => { - if (secretsUserCanDeleteSet.has(secretIdToDelete)) { - const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } - deleteOperationsToPerform.push(deleteOperation) - } else { - throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) - } - }) - - const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) - if (bulkDeleteError) { - if (bulkDeleteError instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) - } - throw InternalServerError() - } - - res.status(200).send() - } ); /** @@ -145,53 +68,8 @@ router.patch( acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), - validateRequest, async (req: Request, res: Response) => { - const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; - const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) - if (secretIdsUserCanModifyError) { - throw InternalServerError({ message: "Unable to fetch secrets you own" }) - } - - const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); - const updateOperationsToPerform: any = [] - - - secretsModificationsRequested.forEach(userModifiedSecret => { - if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { - const sanitizedSecret: SanitizedSecretModify = { - secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, - secretKeyIV: userModifiedSecret.secretKeyIV, - secretKeyTag: userModifiedSecret.secretKeyTag, - secretKeyHash: userModifiedSecret.secretKeyHash, - secretValueCiphertext: userModifiedSecret.secretValueCiphertext, - secretValueIV: userModifiedSecret.secretValueIV, - secretValueTag: userModifiedSecret.secretValueTag, - secretValueHash: userModifiedSecret.secretValueHash, - secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, - secretCommentIV: userModifiedSecret.secretCommentIV, - secretCommentTag: userModifiedSecret.secretCommentTag, - secretCommentHash: userModifiedSecret.secretCommentHash, - } - - const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } - updateOperationsToPerform.push(updateOperation) - } else { - throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) - } - }) - - const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) - if (bulkModificationInfoError) { - if (bulkModificationInfoError instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) - } - - throw InternalServerError() - } - - return res.status(200).send() - } + validateRequest, + secretController.batchModifySecrets ); export default router; From 078c67f27c972621555036c697d5542ca769fce7 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 3 Jan 2023 17:43:40 -0500 Subject: [PATCH 51/91] Add crud cli docs --- docs/cli/commands/secrets.mdx | 93 +++++++++++++++++++++++++++++++++++ docs/mint.json | 1 + 2 files changed, 94 insertions(+) create mode 100644 docs/cli/commands/secrets.mdx diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx new file mode 100644 index 000000000..32ddb2255 --- /dev/null +++ b/docs/cli/commands/secrets.mdx @@ -0,0 +1,93 @@ +--- +title: "infisical secrets" +--- + +``` +infisical secrets +``` + +## Description +This command enables you to perform CRUD (create, read, update, delete) operations on secrets within your Infisical project. With it, you can view, create, update, and delete secrets in your environment. + +### Sub-commands + + Use this command to print out all of the secrets in your project + + ``` + $ infisical secrets + + ## Example + $ infisical secrets + ┌─────────────┬──────────────┬─────────────┐ + │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ + ├─────────────┼──────────────┼─────────────┤ + │ DOMAIN │ example.com │ shared │ + │ HASH │ jebhfbwe │ shared │ + └─────────────┴──────────────┴─────────────┘ + ``` + + ### flags + + Parse shell parameter expansions in your secrets + + Default value: `true` + + + + + + This command allows you selectively print the requested secrets by name + + ``` + $ infisical secrets get ... + + # Example + $ infisical secrets get DOMAIN + ┌─────────────┬──────────────┬─────────────┐ + │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ + ├─────────────┼──────────────┼─────────────┤ + │ DOMAIN │ example.com │ shared │ + └─────────────┴──────────────┴─────────────┘ + + ``` + + ### Flags + None + + + +This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. +If the secret key does not exist, a new secret will be created using both the key and value provided. + +``` +$ infisical secrets set ... + +## Example +$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe +┌────────────────┬───────────────┬────────────────────────┐ +│ SECRET NAME │ SECRET VALUE │ STATUS │ +├────────────────┼───────────────┼────────────────────────┤ +│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │ +│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │ +│ HASH │ jebhfbwe │ SECRET CREATED │ +└────────────────┴───────────────┴────────────────────────┘ +``` + + ### Flags + None + + + + This command allows you to delete secrets by their name(s). + + ``` + $ infisical secrets delete ... + + ## Example + $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH + secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project + ``` + + ### Flags + None + \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index e94b70a6b..9604e7beb 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -94,6 +94,7 @@ "cli/commands/login", "cli/commands/init", "cli/commands/run", + "cli/commands/secrets", "cli/commands/export", "cli/commands/vault" ] From 5967a5cdbab104943044e547c46ad145e5e79e2f Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 10:00:05 +0700 Subject: [PATCH 52/91] Add endpoint to return count of secret snapshots for a workspace --- .../ee/controllers/v1/workspaceController.ts | 27 ++++++++++++++++++- backend/src/ee/routes/v1/workspace.ts | 14 ++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 25a8c8d76..016baff67 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import e, { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; import { SecretSnapshot, @@ -37,6 +37,31 @@ import { }); } +/** + * Return count of secret snapshots for workspace with id [workspaceId] + * @param req + * @param res + */ +export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { + let count; + try { + const { workspaceId } = req.params; + count = await SecretSnapshot.countDocuments({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to count number of secret snapshots' + }); + } + + return res.status(200).send({ + count + }); +} + /** * Return (audit) logs for workspace with id [workspaceId] * @param req diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index bca79dd91..f95ea7c93 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -25,6 +25,20 @@ router.get( workspaceController.getWorkspaceSecretSnapshots ); +router.get( + '/:workspaceId/secret-snapshots/count', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceSecretSnapshotsCount +); + router.get( '/:workspaceId/logs', requireAuth, From 15db7920580fc2841c129ff8aeeeacf32e544eff Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 15:04:09 +0700 Subject: [PATCH 53/91] Patch requireAuth middleware in getting secret snapshot by id --- backend/src/ee/routes/v1/secretSnapshot.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index 04f6c9d59..4b23f03e5 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -13,7 +13,9 @@ import { secretSnapshotController } from '../../controllers/v1'; router.get( '/:secretSnapshotId', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireSecretSnapshotAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [GRANTED] From ff0b053d12a4edbd361b77bb8eddacdfdd7d3339 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 18:04:53 +0700 Subject: [PATCH 54/91] Begin API Key functionality --- .../controllers/v2/apiKeyDataController.ts | 54 +++++++++++++++++++ backend/src/controllers/v2/index.ts | 4 +- backend/src/models/apiKeyData.ts | 37 +++++++++++++ backend/src/models/index.ts | 7 ++- ...rviceTokenData .ts => serviceTokenData.ts} | 2 - backend/src/routes/v2/apiKeyData.ts | 21 ++++++++ backend/src/routes/v2/index.ts | 4 +- 7 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 backend/src/controllers/v2/apiKeyDataController.ts create mode 100644 backend/src/models/apiKeyData.ts rename backend/src/models/{serviceTokenData .ts => serviceTokenData.ts} (93%) create mode 100644 backend/src/routes/v2/apiKeyData.ts diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts new file mode 100644 index 000000000..e09504d49 --- /dev/null +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -0,0 +1,54 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import { + APIKeyData +} from '../../models'; +import { + SALT_ROUNDS +} from '../../config'; + +/** + * Create new API key for user with id [req.user._id] + * @param req + * @param res + */ +export const createAPIKey = async (req: Request, res: Response) => { + let apiKey, apiKeyData; + try { + const { name, expiresIn } = req.body; + + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + + apiKeyData = await new APIKeyData({ + name, + expiresAt, + user: req.user._id, + secretHash + }); + + // return api key data without sensitive data + apiKeyData = await APIKeyData.findById(apiKeyData._id); + + if (!apiKeyData) throw new Error('Failed to find API key data'); + + apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; + + } 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({ + apiKey, + apiKeyData + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index d4729c15c..e867f7692 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,7 +1,9 @@ import * as workspaceController from './workspaceController'; import * as serviceTokenDataController from './serviceTokenDataController'; +import * as apiKeyDataController from './apiKeyDataController'; export { workspaceController, - serviceTokenDataController + serviceTokenDataController, + apiKeyDataController } diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/apiKeyData.ts new file mode 100644 index 000000000..af73b5f69 --- /dev/null +++ b/backend/src/models/apiKeyData.ts @@ -0,0 +1,37 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface IAPIKeyData { + name: string; + user: Types.ObjectId; + expiresAt: Date; + secretHash: string; +} + +const apiKeyDataSchema = new Schema( + { + name: { + type: String, + required: true + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + expiresAt: { + type: Date + }, + secretHash: { + type: String, + required: true, + select: false + } + }, + { + timestamps: true + } +); + +const APIKeyData = model('APIKeyData', apiKeyDataSchema); + +export default APIKeyData; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 8e934d511..72ffca607 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,8 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; +import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; +import APIKeyData, { IAPIKeyData } from './apiKeyData'; export { BackupPrivateKey, @@ -50,5 +51,7 @@ export { Workspace, IWorkspace, ServiceTokenData, - IServiceTokenData + IServiceTokenData, + APIKeyData, + IAPIKeyData }; diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData.ts similarity index 93% rename from backend/src/models/serviceTokenData .ts rename to backend/src/models/serviceTokenData.ts index 8e8ae5eab..612d07bf6 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData.ts @@ -1,5 +1,4 @@ import { Schema, model, Types } from 'mongoose'; -import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; export interface IServiceTokenData { name: string; @@ -38,7 +37,6 @@ const serviceTokenDataSchema = new Schema( }, secretHash: { type: String, - unique: true, required: true, select: false }, diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts new file mode 100644 index 000000000..c6a6a1761 --- /dev/null +++ b/backend/src/routes/v2/apiKeyData.ts @@ -0,0 +1,21 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + validateRequest +} from '../../middleware'; +import { body } from 'express-validator'; +import { apiKeyDataController } from '../../controllers/v2'; + +router.post( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + body('name').exists().trim(), + body('expiresIn'), // measured in ms + validateRequest, + apiKeyDataController.createAPIKey +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index acf115a92..d0f3833ba 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,9 +1,11 @@ import secret from './secret'; import workspace from './workspace'; import serviceTokenData from './serviceTokenData'; +import apiKeyData from './apiKeyData'; export { secret, workspace, - serviceTokenData + serviceTokenData, + apiKeyData } From 58830eab79337f5daccaf7578a635dcfed8a7417 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 18:15:32 +0700 Subject: [PATCH 55/91] Move get service token data to v2 routes --- .../src/controllers/v1/workspaceController.ts | 27 ------------------- .../src/controllers/v2/workspaceController.ts | 27 +++++++++++++++++++ backend/src/routes/v1/workspace.ts | 14 ---------- backend/src/routes/v2/workspace.ts | 15 +++++++++++ 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index d34fecbd7..711401e0f 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -335,31 +335,4 @@ export const getWorkspaceServiceTokens = async ( return res.status(200).send({ serviceTokens }); -} - -export const getWorkspaceServiceTokenData = async ( - req: Request, - res: Response -) => { - let serviceTokenData; - try { - const { workspaceId } = req.query; - - serviceTokenData = await ServiceTokenData - .find({ - workspace: workspaceId - }) - .select('+encryptedKey +iv +tag'); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace service token data' - }); - } - - return res.status(200).send({ - serviceTokenData - }); } \ No newline at end of file diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 1b10ebccd..7c8fded0e 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -9,6 +9,7 @@ import { Key, IUser, ServiceToken, + ServiceTokenData } from '../../models'; import { createWorkspace as create, @@ -191,4 +192,30 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { return res.status(200).send({ key }); +} +export const getWorkspaceServiceTokenData = async ( + req: Request, + res: Response +) => { + let serviceTokenData; + try { + const { workspaceId } = req.query; + + serviceTokenData = await ServiceTokenData + .find({ + workspace: workspaceId + }) + .select('+encryptedKey +iv +tag'); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace service token data' + }); + } + + return res.status(200).send({ + serviceTokenData + }); } \ No newline at end of file diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 7a1f8c5b8..6e2fbdbd6 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -156,18 +156,4 @@ router.get( workspaceController.getWorkspaceServiceTokens ); -router.get( - '/:workspaceId/service-token-data', - requireAuth({ - acceptedAuthModes: ['jwt'] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspaceServiceTokenData -); - export default router; diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index b2c91bd1b..52bc8bb8f 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -57,4 +57,19 @@ router.get( workspaceController.getWorkspaceKey ); +router.get( + '/:workspaceId/service-token-data', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [GRANTED] + }), + param('workspaceId').exists().trim(), + validateRequest, + workspaceController.getWorkspaceServiceTokenData +); + + export default router; From c7fb9209c423d7189e6b93d28e9f46e5467f43cb Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 20:27:16 +0700 Subject: [PATCH 56/91] Complete v1 support for API key auth mode --- backend/src/app.ts | 2 + .../controllers/v2/apiKeyDataController.ts | 34 +++++++-- backend/src/helpers/auth.ts | 69 ++++++++++++++++--- backend/src/middleware/requireAuth.ts | 8 ++- backend/src/routes/v2/apiKeyData.ts | 10 ++- backend/src/routes/v2/workspace.ts | 1 - backend/src/types/express/index.d.ts | 1 + backend/src/utils/errors.ts | 10 +++ 8 files changed, 119 insertions(+), 16 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index ca6428447..461ede0c5 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -38,6 +38,7 @@ import { secret as v2SecretRouter, workspace as v2WorkspaceRouter, serviceTokenData as v2ServiceTokenDataRouter, + apiKeyData as v2APIKeyDataRouter, } from './routes/v2'; import { getLogger } from './utils/logger'; @@ -94,6 +95,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); +app.use('/api/v2/api-key-data', v2APIKeyDataRouter); //* 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/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index e09504d49..f3fc957f1 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -10,11 +10,36 @@ import { } from '../../config'; /** - * Create new API key for user with id [req.user._id] + * Return API key data for user with id [req.user_id] + * @param req + * @param res + * @returns + */ +export const getAPIKeyData = async (req: Request, res: Response) => { + let apiKeyData; + try { + apiKeyData = await APIKeyData.find({ + user: req.user._id + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get API key data' + }); + } + + return res.status(200).send({ + apiKeyData + }); +} + +/** + * Create new API key data for user with id [req.user._id] * @param req * @param res */ -export const createAPIKey = async (req: Request, res: Response) => { +export const createAPIKeyData = async (req: Request, res: Response) => { let apiKey, apiKeyData; try { const { name, expiresIn } = req.body; @@ -30,7 +55,7 @@ export const createAPIKey = async (req: Request, res: Response) => { expiresAt, user: req.user._id, secretHash - }); + }).save(); // return api key data without sensitive data apiKeyData = await APIKeyData.findById(apiKeyData._id); @@ -40,10 +65,11 @@ export const createAPIKey = async (req: Request, res: Response) => { apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; } catch (err) { + console.error(err); Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to create service token data' + message: 'Failed to API key data' }); } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index ad63d41b4..2b972c09d 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -3,22 +3,25 @@ import * as Sentry from '@sentry/node'; import bcrypt from 'bcrypt'; import { User, - ServiceTokenData + ServiceTokenData, + APIKeyData } from '../models'; import { JWT_AUTH_LIFETIME, JWT_AUTH_SECRET, JWT_REFRESH_LIFETIME, - JWT_REFRESH_SECRET, - SALT_ROUNDS + JWT_REFRESH_SECRET } from '../config'; import { AccountNotFoundError, ServiceTokenDataNotFoundError, - UnauthorizedRequestError, - BadRequestError + APIKeyDataNotFoundError, + UnauthorizedRequestError } from '../utils/errors'; +// TODO 1: check if API key works +// TODO 2: optimize middleware + /** * Validate that auth token value [authTokenValue] falls under one of * accepted auth modes [acceptedAuthModes]. @@ -40,6 +43,9 @@ const validateAuthMode = ({ case 'st': authMode = 'serviceToken'; break; + case 'ak': + authMode = 'apiKey'; + break; default: authMode = 'jwt'; break; @@ -106,9 +112,11 @@ const getAuthSTDPayload = async ({ // TODO: optimize double query serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER, 'secretHash expiresAt'); + .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt'); - if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + if (!serviceTokenData) { + throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { // case: service token expired await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); throw UnauthorizedRequestError({ @@ -116,8 +124,6 @@ const getAuthSTDPayload = async ({ }); } - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); - const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); if (!isMatch) throw UnauthorizedRequestError({ message: 'Failed to authenticate service token' @@ -136,6 +142,50 @@ const getAuthSTDPayload = async ({ return serviceTokenData; } +/** + * Return API key data payload corresponding to API key [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - API key value + * @returns {APIKeyData} apiKeyData - API key data + */ +const getAuthAPIKeyPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let user; + try { + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + + const apiKeyData = await APIKeyData + .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') + .populate('user', '+publicKey'); + + if (!apiKeyData) { + throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); + } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { + // case: API key expired + await APIKeyData.findByIdAndDelete(apiKeyData._id); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate expired API key' + }); + } + + const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); + if (!isMatch) throw UnauthorizedRequestError({ + message: 'Failed to authenticate API key' + }); + + user = apiKeyData.user; + } catch (err) { + throw UnauthorizedRequestError({ + message: 'Failed to authenticate API key' + }); + } + + return user; +} + /** * Return newly issued (JWT) auth and refresh tokens to user with id [userId] * @param {Object} obj @@ -229,6 +279,7 @@ export { validateAuthMode, getAuthUserPayload, getAuthSTDPayload, + getAuthAPIKeyPayload, createToken, issueTokens, clearTokens diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 8253cb64e..5d95883a5 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -4,7 +4,8 @@ import { User, ServiceTokenData } from '../models'; import { validateAuthMode, getAuthUserPayload, - getAuthSTDPayload + getAuthSTDPayload, + getAuthAPIKeyPayload } from '../helpers/auth'; import { BadRequestError } from '../utils/errors'; @@ -53,6 +54,11 @@ const requireAuth = ({ authTokenValue: AUTH_TOKEN_VALUE }); break; + case 'apiKey': + req.user = await getAuthAPIKeyPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); + break; default: req.user = await getAuthUserPayload({ authTokenValue: AUTH_TOKEN_VALUE diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts index c6a6a1761..9544f6b0a 100644 --- a/backend/src/routes/v2/apiKeyData.ts +++ b/backend/src/routes/v2/apiKeyData.ts @@ -7,6 +7,14 @@ import { import { body } from 'express-validator'; import { apiKeyDataController } from '../../controllers/v2'; +router.get( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + apiKeyDataController.getAPIKeyData +); + router.post( '/', requireAuth({ @@ -15,7 +23,7 @@ router.post( body('name').exists().trim(), body('expiresIn'), // measured in ms validateRequest, - apiKeyDataController.createAPIKey + apiKeyDataController.createAPIKeyData ); export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 52bc8bb8f..de3feb8bc 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -71,5 +71,4 @@ router.get( workspaceController.getWorkspaceServiceTokenData ); - export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 156800360..7b98d924f 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -16,6 +16,7 @@ declare global { serviceToken: any; accessToken: any; serviceTokenData: any; + apiKeyData: any; query?: any; } } diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index f7310d0be..9c8ac852b 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -143,4 +143,14 @@ export const ServiceTokenDataNotFoundError = (error?: Partial[API KEY DATA ERRORS]<----- +export const APIKeyDataNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'service_token_data_not_found_error', + message: error?.message ?? 'The requested service token data was not found', + context: error?.context, + stack: error?.stack +}) + //* ----->[MISC ERRORS]<----- From d3efe351f1cd90c9c613a036ba9b1536eea983c0 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 20:38:37 +0700 Subject: [PATCH 57/91] Add DELETE route to API keys --- .../controllers/v2/apiKeyDataController.ts | 26 +++++++++++++++++++ backend/src/routes/v2/apiKeyData.ts | 12 ++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index f3fc957f1..3aacde8af 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -77,4 +77,30 @@ export const createAPIKeyData = async (req: Request, res: Response) => { apiKey, apiKeyData }); +} + +/** + * Delete API key data with id [apiKeyDataId]. + * @param req + * @param res + * @returns + */ +export const deleteAPIKeyData = async (req: Request, res: Response) => { + let apiKeyData; + try { + const { apiKeyDataId } = req.params; + + apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete API key data' + }); + } + + return res.status(200).send({ + apiKeyData + }); } \ No newline at end of file diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts index 9544f6b0a..07bbcbc44 100644 --- a/backend/src/routes/v2/apiKeyData.ts +++ b/backend/src/routes/v2/apiKeyData.ts @@ -4,7 +4,7 @@ import { requireAuth, validateRequest } from '../../middleware'; -import { body } from 'express-validator'; +import { param, body } from 'express-validator'; import { apiKeyDataController } from '../../controllers/v2'; router.get( @@ -26,4 +26,14 @@ router.post( apiKeyDataController.createAPIKeyData ); +router.delete( + '/:apiKeyDataId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + param('apiKeyDataId').exists().trim(), + validateRequest, + apiKeyDataController.deleteAPIKeyData +); + export default router; \ No newline at end of file From 54676c630edea2dc0257edc355dfc2b56092b151 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 4 Jan 2023 21:14:25 +0700 Subject: [PATCH 58/91] Remove accept statuses --- backend/src/controllers/v1/keyController.ts | 4 --- .../controllers/v1/membershipController.ts | 8 +++--- .../src/controllers/v1/workspaceController.ts | 18 ++++++------- .../src/controllers/v2/workspaceController.ts | 3 +-- backend/src/ee/routes/v1/secret.ts | 7 +++-- backend/src/ee/routes/v1/workspace.ts | 5 ++-- backend/src/helpers/membership.ts | 20 +++----------- backend/src/helpers/rateLimiter.ts | 4 +-- backend/src/helpers/signup.ts | 5 ++-- backend/src/middleware/requireBotAuth.ts | 5 +--- .../src/middleware/requireIntegrationAuth.ts | 8 ++---- .../requireIntegrationAuthorizationAuth.ts | 6 +---- backend/src/middleware/requireSecretAuth.ts | 8 ++---- .../middleware/requireServiceTokenDataAuth.ts | 5 +--- .../src/middleware/requireWorkspaceAuth.ts | 6 +---- backend/src/models/membership.ts | 9 +------ backend/src/routes/v1/bot.ts | 8 +++--- backend/src/routes/v1/integration.ts | 8 +++--- backend/src/routes/v1/integrationAuth.ts | 7 ++--- backend/src/routes/v1/key.ts | 8 +++--- backend/src/routes/v1/secret.ts | 8 +++--- backend/src/routes/v1/serviceToken.ts | 3 +-- backend/src/routes/v1/workspace.ts | 27 +++++++------------ backend/src/routes/v2/serviceTokenData.ts | 6 +---- backend/src/routes/v2/workspace.ts | 14 ++++------ backend/src/variables/index.ts | 4 --- backend/src/variables/organization.ts | 8 +----- 27 files changed, 65 insertions(+), 157 deletions(-) diff --git a/backend/src/controllers/v1/keyController.ts b/backend/src/controllers/v1/keyController.ts index 332215894..ffcb16d9c 100644 --- a/backend/src/controllers/v1/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -2,7 +2,6 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; 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 @@ -26,9 +25,6 @@ export const uploadKey = async (req: Request, res: Response) => { throw new Error('Failed receiver membership validation for workspace'); } - receiverMembership.status = GRANTED; - await receiverMembership.save(); - await new Key({ encryptedKey: key.encryptedKey, nonce: key.nonce, diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 187e8127c..42cc29d9c 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -7,7 +7,7 @@ import { } from '../../helpers/membership'; import { sendMail } from '../../helpers/nodemailer'; import { SITE_URL } from '../../config'; -import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables'; +import { ADMIN, MEMBER, ACCEPTED } from '../../variables'; /** * Check that user is a member of workspace with id [workspaceId] @@ -175,8 +175,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { // already a member of the workspace const inviteeMembership = await Membership.findOne({ user: invitee._id, - workspace: workspaceId, - status: GRANTED + workspace: workspaceId }); if (inviteeMembership) @@ -205,8 +204,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { const m = await new Membership({ user: invitee._id, workspace: workspaceId, - role: MEMBER, - status: GRANTED + role: MEMBER }).save(); await sendMail({ diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 711401e0f..4c0e869c5 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -15,7 +15,7 @@ import { deleteWorkspace as deleteWork } from '../../helpers/workspace'; import { addMemberships } from '../../helpers/membership'; -import { ADMIN, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN } from '../../variables'; /** * Return public keys of members of workspace with id [workspaceId] @@ -33,13 +33,12 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { workspace: workspaceId }).populate<{ user: IUser }>('user', 'publicKey') ) - .filter((m) => m.status === COMPLETED || m.status === GRANTED) - .map((member) => { - return { - publicKey: member.user.publicKey, - userId: member.user._id - }; - }); + .map((member) => { + return { + publicKey: member.user.publicKey, + userId: member.user._id + }; + }); } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -169,8 +168,7 @@ export const createWorkspace = async (req: Request, res: Response) => { await addMemberships({ userIds: [req.user._id], workspaceId: workspace._id.toString(), - roles: [ADMIN], - statuses: [GRANTED] + roles: [ADMIN] }); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 7c8fded0e..fb8fd99e0 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -21,10 +21,9 @@ import { 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'; +import { ENV_SET } from '../../variables'; interface V2PushSecret { type: string; // personal or shared diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 200ce35aa..43cc8bafc 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -5,9 +5,9 @@ import { requireSecretAuth, validateRequest } from '../../../middleware'; -import { body, query, param } from 'express-validator'; +import { query, param } from 'express-validator'; import { secretController } from '../../controllers/v1'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables'; +import { ADMIN, MEMBER } from '../../../variables'; router.get( '/:secretId/secret-versions', @@ -15,8 +15,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('secretId').exists().trim(), query('offset').exists().isInt(), diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index bc3480280..6a7f11626 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -6,7 +6,7 @@ import { validateRequest } from '../../../middleware'; import { param, query } from 'express-validator'; -import { ADMIN, MEMBER, GRANTED } from '../../../variables'; +import { ADMIN, MEMBER } from '../../../variables'; import { workspaceController } from '../../controllers/v1'; router.get( @@ -15,8 +15,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), query('offset').exists().isInt(), diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index b237803f1..466cefdef 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -3,7 +3,7 @@ import { Membership, Key } from '../models'; /** * Validate that user with id [userId] is a member of workspace with id [workspaceId] - * and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses] + * and has at least one of the roles in [acceptedRoles] * @param {Object} obj * @param {String} obj.userId - id of user to validate * @param {String} obj.workspaceId - id of workspace @@ -12,12 +12,10 @@ const validateMembership = async ({ userId, workspaceId, acceptedRoles, - acceptedStatuses }: { userId: string; workspaceId: string; acceptedRoles: string[]; - acceptedStatuses: string[]; }) => { let membership; @@ -33,11 +31,6 @@ const validateMembership = async ({ if (!acceptedRoles.includes(membership.role)) { throw new Error('Failed to validate membership role'); } - - if (!acceptedStatuses.includes(membership.status)) { - throw new Error('Failed to validate membership status'); - } - } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -72,18 +65,15 @@ const findMembership = async (queryObj: any) => { * @param {String[]} obj.userIds - id of users. * @param {String} obj.workspaceId - id of workspace. * @param {String[]} obj.roles - roles of users. - * @param {String[]} obj.statuses - statuses of users. */ const addMemberships = async ({ userIds, workspaceId, - roles, - statuses + roles }: { userIds: string[]; workspaceId: string; roles: string[]; - statuses: string[]; }): Promise => { try { const operations = userIds.map((userId, idx) => { @@ -92,14 +82,12 @@ const addMemberships = async ({ filter: { user: userId, workspace: workspaceId, - role: roles[idx], - status: statuses[idx] + role: roles[idx] }, update: { user: userId, workspace: workspaceId, - role: roles[idx], - status: statuses[idx] + role: roles[idx] }, upsert: true } diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 6153369e3..6171559af 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -3,7 +3,7 @@ import rateLimit from 'express-rate-limit'; // 300 requests per 15 minutes const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 400, + max: 450, standardHeaders: true, legacyHeaders: false, skip: (request) => request.path === '/healthcheck' @@ -20,7 +20,7 @@ const signupLimiter = rateLimit({ // 10 requests per hour const loginLimiter = rateLimit({ windowMs: 60 * 60 * 1000, - max: 20, + max: 25, standardHeaders: true, legacyHeaders: false }); diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 8a201cb11..4621d1f50 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -5,7 +5,7 @@ import { createOrganization } from './organization'; import { addMembershipsOrg } from './membershipOrg'; import { createWorkspace } from './workspace'; import { addMemberships } from './membership'; -import { OWNER, ADMIN, ACCEPTED, GRANTED } from '../variables'; +import { OWNER, ADMIN, ACCEPTED } from '../variables'; import { sendMail } from '../helpers/nodemailer'; /** @@ -113,8 +113,7 @@ const initializeDefaultOrg = async ({ await addMemberships({ userIds: [user._id.toString()], workspaceId: workspace._id.toString(), - roles: [ADMIN], - statuses: [GRANTED] + roles: [ADMIN] }); } catch (err) { throw new Error('Failed to initialize default organization and workspace'); diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index 14c099393..435b06a59 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -7,11 +7,9 @@ type req = 'params' | 'body' | 'query'; const requireBotAuth = ({ acceptedRoles, - acceptedStatuses, location = 'params' }: { acceptedRoles: string[]; - acceptedStatuses: string[]; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -24,8 +22,7 @@ const requireBotAuth = ({ await validateMembership({ userId: req.user._id.toString(), workspaceId: bot.workspace.toString(), - acceptedRoles, - acceptedStatuses + acceptedRoles }); req.bot = bot; diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index 4389028ab..b185b922b 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -9,14 +9,11 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err * with the integration on request params. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.acceptedStatuses - accepted workspace statuses */ const requireIntegrationAuth = ({ - acceptedRoles, - acceptedStatuses + acceptedRoles }: { acceptedRoles: string[]; - acceptedStatuses: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { // integration authorization middleware @@ -35,8 +32,7 @@ const requireIntegrationAuth = ({ await validateMembership({ userId: req.user._id.toString(), workspaceId: integration.workspace.toString(), - acceptedRoles, - acceptedStatuses + acceptedRoles }); const integrationAuth = await IntegrationAuth.findOne({ diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index 278716e60..6c1f9066e 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -10,16 +10,13 @@ import { UnauthorizedRequestError } from '../utils/errors'; * with the integration authorization on request params. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.acceptedStatuses - accepted workspace statuses * @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request */ const requireIntegrationAuthorizationAuth = ({ acceptedRoles, - acceptedStatuses, attachAccessToken = true }: { acceptedRoles: string[]; - acceptedStatuses: string[]; attachAccessToken?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -38,8 +35,7 @@ const requireIntegrationAuthorizationAuth = ({ await validateMembership({ userId: req.user._id.toString(), workspaceId: integrationAuth.workspace.toString(), - acceptedRoles, - acceptedStatuses + acceptedRoles }); req.integrationAuth = integrationAuth; diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts index 8f6fc5305..c6a291200 100644 --- a/backend/src/middleware/requireSecretAuth.ts +++ b/backend/src/middleware/requireSecretAuth.ts @@ -9,15 +9,12 @@ import { * Validate if user on request has proper membership to modify secret. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted workspace roles - * @param {String[]} obj.acceptedStatuses - accepted workspace statuses * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireSecretAuth = ({ - acceptedRoles, - acceptedStatuses + acceptedRoles }: { acceptedRoles: string[]; - acceptedStatuses: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { try { @@ -34,8 +31,7 @@ const requireSecretAuth = ({ await validateMembership({ userId: req.user._id.toString(), workspaceId: secret.workspace.toString(), - acceptedRoles, - acceptedStatuses + acceptedRoles }); req.secret = secret as any; diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts index 48eaac3dc..2b4eb739a 100644 --- a/backend/src/middleware/requireServiceTokenDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -7,11 +7,9 @@ type req = 'params' | 'body' | 'query'; const requireServiceTokenDataAuth = ({ acceptedRoles, - acceptedStatuses, location = 'params' }: { acceptedRoles: string[]; - acceptedStatuses: string[]; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -30,8 +28,7 @@ const requireServiceTokenDataAuth = ({ await validateMembership({ userId: req.user._id.toString(), workspaceId: serviceTokenData.workspace.toString(), - acceptedRoles, - acceptedStatuses + acceptedRoles }); } diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 68edec6a4..9b710cc83 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -9,16 +9,13 @@ type req = 'params' | 'body' | 'query'; * on request params. * @param {Object} obj * @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 = ({ acceptedRoles, - acceptedStatuses, location = 'params' }: { acceptedRoles: string[]; - acceptedStatuses: string[]; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -30,8 +27,7 @@ const requireWorkspaceAuth = ({ const membership = await validateMembership({ userId: req.user._id.toString(), workspaceId, - acceptedRoles, - acceptedStatuses + acceptedRoles }); req.membership = membership; diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index a38c64651..abbce82c1 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -1,5 +1,5 @@ import { Schema, model, Types } from 'mongoose'; -import { ADMIN, MEMBER, INVITED, COMPLETED, GRANTED } from '../variables'; +import { ADMIN, MEMBER } from '../variables'; export interface IMembership { _id: Types.ObjectId; @@ -7,7 +7,6 @@ export interface IMembership { inviteEmail?: string; workspace: Types.ObjectId; role: 'admin' | 'member'; - status: 'invited' | 'completed' | 'granted'; } const membershipSchema = new Schema( @@ -28,12 +27,6 @@ const membershipSchema = new Schema( type: String, enum: [ADMIN, MEMBER], required: true - }, - status: { - // INVITED, COMPLETED, GRANTED - type: String, - enum: [INVITED, COMPLETED, GRANTED], - required: true } }, { diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index 0d631b8eb..1b98f48c5 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -8,7 +8,7 @@ import { validateRequest } from '../../middleware'; import { botController } from '../../controllers/v1'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; router.get( '/:workspaceId', @@ -16,8 +16,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim().notEmpty(), validateRequest, @@ -30,8 +29,7 @@ router.patch( acceptedAuthModes: ['jwt'] }), requireBotAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), body('isActive').isBoolean(), body('botKey'), diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 3715bef46..ea589162a 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -5,7 +5,7 @@ import { requireIntegrationAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { body, param } from 'express-validator'; import { integrationController } from '../../controllers/v1'; @@ -15,8 +15,7 @@ router.patch( acceptedAuthModes: ['jwt'] }), requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('integrationId').exists().trim(), body('app').exists().trim(), @@ -35,8 +34,7 @@ router.delete( acceptedAuthModes: ['jwt'] }), requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('integrationId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index c613ce62f..605c5023e 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -7,7 +7,7 @@ import { requireIntegrationAuthorizationAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { integrationAuthController } from '../../controllers/v1'; router.get( @@ -25,7 +25,6 @@ router.post( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED], location: 'body' }), body('workspaceId').exists().trim().notEmpty(), @@ -41,8 +40,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('integrationAuthId'), validateRequest, @@ -56,7 +54,6 @@ router.delete( }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED], attachAccessToken: false }), param('integrationAuthId'), diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index d8bc7c29e..b66bd1276 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -6,7 +6,7 @@ import { validateRequest } from '../../middleware'; import { body, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { keyController } from '../../controllers/v1'; router.post( @@ -15,8 +15,7 @@ router.post( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), body('key').exists(), @@ -30,8 +29,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId'), validateRequest, diff --git a/backend/src/routes/v1/secret.ts b/backend/src/routes/v1/secret.ts index c3f0626ce..cce105500 100644 --- a/backend/src/routes/v1/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -8,7 +8,7 @@ import { } from '../../middleware'; import { body, query, param } from 'express-validator'; import { secretController } from '../../controllers/v1'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; router.post( '/:workspaceId', @@ -16,8 +16,7 @@ router.post( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), body('secrets').exists(), body('keys').exists(), @@ -34,8 +33,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), query('environment').exists().trim(), query('channel'), diff --git a/backend/src/routes/v1/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts index edc00a939..18487ac3e 100644 --- a/backend/src/routes/v1/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -7,7 +7,7 @@ import { validateRequest } from '../../middleware'; import { body } from 'express-validator'; -import { ADMIN, MEMBER, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { serviceTokenController } from '../../controllers/v1'; // note: deprecate service-token routes in favor of service-token data routes/structure @@ -25,7 +25,6 @@ router.post( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED], location: 'body' }), body('name').exists().trim().notEmpty(), diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 6e2fbdbd6..801462662 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -6,7 +6,7 @@ import { requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { workspaceController, membershipController } from '../../controllers/v1'; router.get( @@ -15,8 +15,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, @@ -30,7 +29,6 @@ router.get( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] }), param('workspaceId').exists().trim(), validateRequest, @@ -51,8 +49,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, @@ -76,8 +73,7 @@ router.delete( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN] }), param('workspaceId').exists().trim(), validateRequest, @@ -90,8 +86,7 @@ router.post( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), body('name').exists().trim().notEmpty(), @@ -105,8 +100,7 @@ router.post( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), body('email').exists().trim().notEmpty(), @@ -120,8 +114,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, @@ -134,8 +127,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, @@ -148,8 +140,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 578d4e38e..254bf60f3 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -10,8 +10,6 @@ import { param, body } from 'express-validator'; import { ADMIN, MEMBER, - COMPLETED, - GRANTED } from '../../variables'; import { serviceTokenDataController } from '../../controllers/v2'; @@ -30,7 +28,6 @@ router.post( }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED], location: 'body' }), body('name').exists().trim(), @@ -50,8 +47,7 @@ router.delete( acceptedAuthModes: ['jwt'] }), requireServiceTokenDataAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED], + acceptedRoles: [ADMIN, MEMBER] }), param('serviceTokenDataId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 52bc8bb8f..fabb54a69 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -6,7 +6,7 @@ import { requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { workspaceController } from '../../controllers/v2'; router.post( @@ -15,8 +15,7 @@ router.post( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), body('secrets').exists(), body('keys').exists(), @@ -33,8 +32,7 @@ router.get( acceptedAuthModes: ['jwt', 'serviceToken'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), query('environment').exists().trim(), query('channel'), @@ -49,8 +47,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, @@ -63,8 +60,7 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().trim(), validateRequest, diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index e284d6d5c..dac4f0646 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -28,8 +28,6 @@ import { MEMBER, INVITED, ACCEPTED, - COMPLETED, - GRANTED } from './organization'; import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; @@ -42,8 +40,6 @@ export { MEMBER, INVITED, ACCEPTED, - COMPLETED, - GRANTED, SECRET_SHARED, SECRET_PERSONAL, ENV_DEV, diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index f91e1f5d3..1b74bd3ee 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -9,16 +9,10 @@ const INVITED = 'invited'; // -- organization const ACCEPTED = 'accepted'; -// -- workspace -const COMPLETED = 'completed'; -const GRANTED = 'granted'; - export { OWNER, ADMIN, MEMBER, INVITED, - ACCEPTED, - COMPLETED, - GRANTED + ACCEPTED } \ No newline at end of file From fe05732c46ae71974652bd6ece0e4dc7d389aea4 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 10:39:32 -0500 Subject: [PATCH 59/91] update host to prod host --- cli/packages/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 0d505e968..f09f08800 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -30,7 +30,7 @@ func Execute() { func init() { rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging") - rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "http://localhost:8080/api", "Point the CLI to your own backend") + rootCmd.PersistentFlags().StringVar(&util.INFISICAL_URL, "domain", "https://app.infisical.com/api", "Point the CLI to your own backend") // rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { // } } From 68a8471292a70e43362652801199eed827c8a10c Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 10:58:00 -0500 Subject: [PATCH 60/91] remove accepted roles from secrets v2 api --- backend/src/routes/v2/secret.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 477e0039e..40d35d200 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,7 +1,7 @@ import express from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { body, param } from 'express-validator'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; import { secretController } from '../../controllers/v2'; @@ -14,8 +14,7 @@ router.post( '/batch-create/workspace/:workspaceId/environment/:environmentName', requireAuth, requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), @@ -30,8 +29,7 @@ router.post( router.get( '/:secretId', requireAuth, param('secretId').exists().trim(), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), validateRequest, secretController.createSingleSecret @@ -47,8 +45,7 @@ router.delete( param('environmentName').exists().trim(), body('secretIds').exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), validateRequest, secretController.batchDeleteSecrets @@ -65,8 +62,7 @@ router.patch( param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] + acceptedRoles: [ADMIN, MEMBER] }), validateRequest, secretController.batchModifySecrets From 68c488b8ee9a66bc9e08131b2dbdf2e9ff8663ef Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 14:58:16 -0500 Subject: [PATCH 61/91] Add acceptedAuthModes for v2 secrets --- backend/src/routes/v2/secret.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 40d35d200..83108823f 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -12,7 +12,9 @@ const router = express.Router(); */ router.post( '/batch-create/workspace/:workspaceId/environment/:environmentName', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), @@ -27,7 +29,11 @@ router.post( * Get a single secret by secret id */ router.get( - '/:secretId', requireAuth, param('secretId').exists().trim(), + '/:secretId', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + param('secretId').exists().trim(), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), @@ -40,7 +46,9 @@ router.get( */ router.delete( '/batch/workspace/:workspaceId/environment/:environmentName', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), body('secretIds').exists().isArray().custom(array => array.length > 0), @@ -57,7 +65,9 @@ router.delete( */ router.patch( '/batch-modify/workspace/:workspaceId/environment/:environmentName', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), From fba40b5d4b96a2acea4f431a4fb2d3bf7524bfb0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 15:32:45 -0500 Subject: [PATCH 62/91] print requestError logs in backend when in dev mode --- backend/src/middleware/requestErrorHandler.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 36f1dce49..3c59c72c3 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -4,26 +4,33 @@ import * as Sentry from '@sentry/node'; import { InternalServerError } from "../utils/errors"; import { getLogger } from "../utils/logger"; import RequestError, { LogLevel } from "../utils/requestError"; +import { NODE_ENV } from "../config"; -export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => { - if(res.headersSent) return next(); +export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { + if (res.headersSent) return next(); + if (NODE_ENV !== "production" && error instanceof RequestError) { + /* eslint-disable no-console */ + console.log(error) + /* eslint-enable no-console */ + } + //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError - if(!(error instanceof RequestError)){ - error = InternalServerError({context: {exception: error.message}, stack: error.stack}) + if (!(error instanceof RequestError)) { + error = InternalServerError({ context: { exception: error.message }, stack: error.stack }) getLogger('backend-main').log((error).levelName.toLowerCase(), (error).message) } - + //* Set Sentry user identification if req.user is populated - if(req.user !== undefined && req.user !== null){ + if (req.user !== undefined && req.user !== null) { Sentry.setUser({ email: req.user.email }) } //* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL' //* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on - if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)){ + if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((error).level)) { Sentry.captureException(error) } - + res.status((error).statusCode).json((error).format(req)) next() } \ No newline at end of file From 880f4d25a923e41c33c05e73c7ceeec0cc2c1cf8 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 17:35:40 -0500 Subject: [PATCH 63/91] print all errors during backend dev --- backend/src/middleware/requestErrorHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 3c59c72c3..50044387e 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -9,7 +9,7 @@ import { NODE_ENV } from "../config"; export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => { if (res.headersSent) return next(); - if (NODE_ENV !== "production" && error instanceof RequestError) { + if (NODE_ENV !== "production") { /* eslint-disable no-console */ console.log(error) /* eslint-enable no-console */ From d75d9ec324956fdb4d9a1d9b3856f9fc44aec159 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 20:05:00 -0500 Subject: [PATCH 64/91] Add get call secrets route for service token and jwt --- backend/src/app.ts | 8 ++--- .../src/controllers/v2/secretController.ts | 30 +++++++++++++++++++ .../src/controllers/v2/workspaceController.ts | 16 +++++----- backend/src/routes/v2/secret.ts | 16 +++++----- backend/src/routes/v2/workspace.ts | 6 ++-- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 461ede0c5..140521ee3 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -94,13 +94,13 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); -app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); +app.use('/api/v2/service-token', v2ServiceTokenDataRouter); app.use('/api/v2/api-key-data', v2APIKeyDataRouter); //* Handle unrouted requests and respond with proper error message as well as status code -app.use((req, res, next)=>{ - if(res.headersSent) return next(); - next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`})) +app.use((req, res, next) => { + if (res.headersSent) return next(); + next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` })) }) //* Error Handling Middleware (must be after all routing logic) diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index e29a8d19b..472859de9 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -6,6 +6,7 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; +import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; export const batchCreateSecrets = async (req: Request, res: Response) => { const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; @@ -135,4 +136,33 @@ export const batchModifySecrets = async (req: Request, res: Response) => { } return res.status(200).send() +} + +export const fetchAllSecrets = async (req: Request, res: Response) => { + const { environment } = req.query; + const { workspaceId } = req.params; + + let userId: string | undefined = undefined // Used for choosing the personal secrets to fetch in + if (req.user) { + userId = req.user._id.toString(); + } + + if (req.serviceTokenData) { + userId = req.serviceTokenData.user._id + } + + const [retriveAllSecretsError, allSecrets] = await to(Secret.find( + { + workspace: workspaceId, + environment, + $or: [{ user: userId }, { user: { $exists: false } }], + type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + } + ).then()) + + if (retriveAllSecretsError instanceof ValidationError) { + throw RouteValidationError({ message: "Unable to get secrets, please try again", stack: retriveAllSecretsError.stack }) + } + + return res.json(allSecrets) } \ No newline at end of file diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index fb8fd99e0..54317690d 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -6,7 +6,7 @@ import { MembershipOrg, Integration, IntegrationAuth, - Key, + Key, IUser, ServiceToken, ServiceTokenData @@ -78,7 +78,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { workspaceId, keys }); - + if (postHogClient) { postHogClient.capture({ event: 'secrets pushed', @@ -125,7 +125,7 @@ export const pullSecrets = async (req: Request, res: Response) => { 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(); @@ -138,7 +138,7 @@ export const pullSecrets = async (req: Request, res: Response) => { workspaceId, environment }); - + if (channel !== 'cli') { secrets = reformatPullSecrets({ secrets }); } @@ -178,7 +178,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { 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 }); @@ -188,9 +188,7 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { }); } - return res.status(200).send({ - key - }); + return res.status(200).json(key); } export const getWorkspaceServiceTokenData = async ( req: Request, @@ -213,7 +211,7 @@ export const getWorkspaceServiceTokenData = async ( message: 'Failed to get workspace service token data' }); } - + return res.status(200).send({ serviceTokenData }); diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 83108823f..95ce3e6b0 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,9 +1,10 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { body, param } from 'express-validator'; +import { body, param, query } from 'express-validator'; import { ADMIN, MEMBER } from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret/types'; import { secretController } from '../../controllers/v2'; +import { fetchAllSecrets } from '../../controllers/v2/secretController'; const router = express.Router(); @@ -26,19 +27,20 @@ router.post( ); /** - * Get a single secret by secret id + * Get all secrets for a given environment and workspace id */ router.get( - '/:secretId', + '/workspace/:workspaceId', + param('workspaceId').exists().trim(), + query("environment").exists(), requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'serviceToken'] }), - param('secretId').exists().trim(), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), validateRequest, - secretController.createSingleSecret + fetchAllSecrets ); /** diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index eaa8e58a3..c90834d6d 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -42,15 +42,15 @@ router.get( ); router.get( - '/:workspaceId/key', + '/:workspaceId/encrypted-key', requireAuth({ acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] - }), + }), param('workspaceId').exists().trim(), - validateRequest, + validateRequest, workspaceController.getWorkspaceKey ); From 347b7201de0f82fa5e4cba411c43181c2492931a Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 4 Jan 2023 17:11:07 -0800 Subject: [PATCH 65/91] Finished secret snapshots --- .../ee/controllers/v1/workspaceController.ts | 1 + backend/src/ee/routes/v1/workspace.ts | 4 +- backend/src/helpers/secret.ts | 11 +- frontend/components/basic/Layout.tsx | 10 +- frontend/components/basic/buttons/Button.tsx | 2 +- .../context/Notifications/Notification.tsx | 4 +- .../Notifications/NotificationProvider.tsx | 2 +- frontend/components/dashboard/KeyPair.tsx | 15 +- .../api/secrets/GetProjectSercetShanpshots.ts | 39 +++ .../secrets/GetProjectSercetSnapshotsCount.ts | 31 +++ .../ee/api/secrets/GetSecretSnapshotData.ts | 31 +++ frontend/ee/components/PITRecoverySidebar.tsx | 158 +++++++++++ frontend/pages/dashboard/[id].tsx | 247 ++++++++++++++---- frontend/public/locales/en/activity.json | 3 +- 14 files changed, 485 insertions(+), 73 deletions(-) create mode 100644 frontend/ee/api/secrets/GetProjectSercetShanpshots.ts create mode 100644 frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts create mode 100644 frontend/ee/api/secrets/GetSecretSnapshotData.ts create mode 100644 frontend/ee/components/PITRecoverySidebar.tsx diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 016baff67..88c31b8e1 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -21,6 +21,7 @@ import { secretSnapshots = await SecretSnapshot.find({ workspace: workspaceId }) + .sort({ createdAt: -1 }) .skip(offset) .limit(limit); diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 48c054970..4b2e839eb 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -39,7 +39,9 @@ router.get( router.get( '/:workspaceId/logs', - requireAuth, + requireAuth({ + acceptedAuthModes: ['jwt'] + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] }), diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 0d64da3b1..920e8dc1d 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -470,11 +470,12 @@ const v1PushSecrets = async ({ // (EE) add secret versions for new secrets EESecretService.addSecretVersions({ - secretVersions: newSecrets.map((s) => ({ - ...s, - secret: s._id, - isDeleted: false - })) + secretVersions: newSecrets.map((secretDocument) => { + return { + ...secretDocument.toObject(), + secret: secretDocument._id, + isDeleted: false + }}) }); const addAction = await EELogService.createActionSecret({ diff --git a/frontend/components/basic/Layout.tsx b/frontend/components/basic/Layout.tsx index 8bdabc97b..8ac9ec92b 100644 --- a/frontend/components/basic/Layout.tsx +++ b/frontend/components/basic/Layout.tsx @@ -121,7 +121,7 @@ export default function Layout({ children }: LayoutProps) { } }); } - router.push("/dashboard/" + newWorkspaceId + "?Development"); + router.push("/dashboard/" + newWorkspaceId); setIsOpen(false); setNewWorkspaceName(""); } else { @@ -141,8 +141,7 @@ export default function Layout({ children }: LayoutProps) { { href: "/dashboard/" + - workspaceMapping[workspaceSelected as any] + - "?Development", + workspaceMapping[workspaceSelected as any], title: t("nav:menu.secrets"), emoji: , }, @@ -199,7 +198,7 @@ export default function Layout({ children }: LayoutProps) { .map((workspace: { _id: string }) => workspace._id) .includes(intendedWorkspaceId) ) { - router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); + router.push("/dashboard/" + userWorkspaces[0]._id); } else { setWorkspaceList( userWorkspaces.map((workspace: any) => workspace.name) @@ -242,8 +241,7 @@ export default function Layout({ children }: LayoutProps) { ) { router.push( "/dashboard/" + - workspaceMapping[workspaceSelected as any] + - "?Development" + workspaceMapping[workspaceSelected as any] ); localStorage.setItem( "projectData.id", diff --git a/frontend/components/basic/buttons/Button.tsx b/frontend/components/basic/buttons/Button.tsx index 9197ccf13..939d9b17d 100644 --- a/frontend/components/basic/buttons/Button.tsx +++ b/frontend/components/basic/buttons/Button.tsx @@ -115,7 +115,7 @@ export default function Button(props: ButtonProps): JSX.Element { )} diff --git a/frontend/components/context/Notifications/Notification.tsx b/frontend/components/context/Notifications/Notification.tsx index ad556a6f5..6635c795e 100644 --- a/frontend/components/context/Notifications/Notification.tsx +++ b/frontend/components/context/Notifications/Notification.tsx @@ -36,7 +36,7 @@ const Notification = ({ return (
{notification.type === 'error' && ( @@ -56,7 +56,7 @@ const Notification = ({ onClick={() => clearNotification(notification.text)} > diff --git a/frontend/components/context/Notifications/NotificationProvider.tsx b/frontend/components/context/Notifications/NotificationProvider.tsx index 05f9eee19..aa694a1d0 100644 --- a/frontend/components/context/Notifications/NotificationProvider.tsx +++ b/frontend/components/context/Notifications/NotificationProvider.tsx @@ -38,7 +38,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => { const createNotification = ({ text, type = 'success', - timeoutMs = 5000 + timeoutMs = 4000 }: Notification) => { const doesNotifExist = notifications.some((notif) => notif.text === text); diff --git a/frontend/components/dashboard/KeyPair.tsx b/frontend/components/dashboard/KeyPair.tsx index 52495829a..ba6cc6ee7 100644 --- a/frontend/components/dashboard/KeyPair.tsx +++ b/frontend/components/dashboard/KeyPair.tsx @@ -21,6 +21,7 @@ interface KeyPairProps { isDuplicate: boolean; toggleSidebar: (id: string) => void; sidebarSecretId: string; + isSnapshot: boolean; } /** @@ -33,6 +34,7 @@ interface KeyPairProps { * @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard * @param {function} obj.toggleSidebar - open/close/switch sidebar * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed + * @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar * @returns */ const KeyPair = ({ @@ -42,10 +44,11 @@ const KeyPair = ({ isBlurred, isDuplicate, toggleSidebar, - sidebarSecretId + sidebarSecretId, + isSnapshot }: KeyPairProps) => { return ( -
+
{keyPair.type == "personal" &&
@@ -65,7 +68,7 @@ const KeyPair = ({
-
+
-
toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"> + {!isSnapshot &&
toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"> -
+
}
); }; -export default React.memo(KeyPair); \ No newline at end of file +export default KeyPair; \ No newline at end of file diff --git a/frontend/ee/api/secrets/GetProjectSercetShanpshots.ts b/frontend/ee/api/secrets/GetProjectSercetShanpshots.ts new file mode 100644 index 000000000..21c2ac801 --- /dev/null +++ b/frontend/ee/api/secrets/GetProjectSercetShanpshots.ts @@ -0,0 +1,39 @@ +import SecurityClient from '~/utilities/SecurityClient'; + + +interface workspaceProps { + workspaceId: string; + offset: number; + limit: number; +} + +/** + * This function fetches the secret snapshots for a certain project + * @param {object} obj + * @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots + * @param {object} obj.offset - teh starting point of snapshots that we want to pull + * @param {object} obj.limit - how many snapshots will we output + * @returns + */ +const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/secret-snapshots?' + + new URLSearchParams({ + offset: String(offset), + limit: String(limit) + }), { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).secretSnapshots; + } else { + console.log('Failed to get project secret snapshots'); + } + }); +}; + +export default getProjectSecretShanpshots; diff --git a/frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts b/frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts new file mode 100644 index 000000000..19389026b --- /dev/null +++ b/frontend/ee/api/secrets/GetProjectSercetSnapshotsCount.ts @@ -0,0 +1,31 @@ +import SecurityClient from '~/utilities/SecurityClient'; + + +interface workspaceProps { + workspaceId: string; +} + +/** + * This function fetches the count of secret snapshots for a certain project + * @param {object} obj + * @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots + * @returns + */ +const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => { + return SecurityClient.fetchCall( + '/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).count; + } else { + console.log('Failed to get the count of project secret snapshots'); + } + }); +}; + +export default getProjectSercetSnapshotsCount; diff --git a/frontend/ee/api/secrets/GetSecretSnapshotData.ts b/frontend/ee/api/secrets/GetSecretSnapshotData.ts new file mode 100644 index 000000000..181fa85ce --- /dev/null +++ b/frontend/ee/api/secrets/GetSecretSnapshotData.ts @@ -0,0 +1,31 @@ +import SecurityClient from '~/utilities/SecurityClient'; + + +interface SnapshotProps { + secretSnapshotId: string; +} + +/** + * This function fetches the secrets for a certain secret snapshot + * @param {object} obj + * @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets + * @returns + */ +const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => { + return SecurityClient.fetchCall( + '/api/v1/secret-snapshot/' + secretSnapshotId, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + if (res && res.status == 200) { + return (await res.json()).secretSnapshot; + } else { + console.log('Failed to get the secrets of a certain snapshot'); + } + }); +}; + +export default getSecretSnapshotData; diff --git a/frontend/ee/components/PITRecoverySidebar.tsx b/frontend/ee/components/PITRecoverySidebar.tsx new file mode 100644 index 000000000..e763d695b --- /dev/null +++ b/frontend/ee/components/PITRecoverySidebar.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; +import { faX } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import getProjectSecretShanpshots from "ee/api/secrets/GetProjectSercetShanpshots"; +import getSecretSnapshotData from "ee/api/secrets/GetSecretSnapshotData"; +import timeSince from "ee/utilities/timeSince"; + +import Button from "~/components/basic/buttons/Button"; +import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto"; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; + + +interface SideBarProps { + toggleSidebar: (value: boolean) => void; + setSnapshotData: (value: any) => void; + chosenSnapshot: string; +} + +interface SnaphotProps { + _id: string; + createdAt: string; + secretVersions: string[]; +} + +interface EncrypetedSecretVersionListProps { + _id: string; + createdAt: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + environment: string; + type: "personal" | "shared"; +} + +/** + * @param {object} obj + * @param {function} obj.toggleSidebar - function that opens or closes the sidebar + * @param {function} obj.setSnapshotData - state manager for snapshot data + * @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected + * + * + * @returns the sidebar with the options for point-in-time recovery (commits) + */ +const PITRecoverySidebar = ({ + toggleSidebar, + setSnapshotData, + chosenSnapshot +}: SideBarProps) => { + const { t } = useTranslation(); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState([]); + const [currentOffset, setCurrentOffset] = useState(0); + const currentLimit = 15; + + const loadMoreSnapshots = () => { + setCurrentOffset(currentOffset + currentLimit); + } + + useEffect(() => { + const getLogData = async () => { + setIsLoading(true); + const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset }) + setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results)); + setIsLoading(false); + } + getLogData(); + }, [currentOffset]); + + const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => { + const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId }); + + const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + let decryptedLatestKey: string; + if (latestKey) { + // assymmetrically decrypt symmetric key with local private key + decryptedLatestKey = decryptAssymmetric({ + ciphertext: latestKey.latestKey.encryptedKey, + nonce: latestKey.latestKey.nonce, + publicKey: latestKey.latestKey.sender.publicKey, + privateKey: String(PRIVATE_KEY) + }); + } + + const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => { + return { + id: encryptedSecretVersion._id, + pos: pos, + type: encryptedSecretVersion.type, + environment: encryptedSecretVersion.environment, + key: decryptSymmetric({ + ciphertext: encryptedSecretVersion.secretKeyCiphertext, + iv: encryptedSecretVersion.secretKeyIV, + tag: encryptedSecretVersion.secretKeyTag, + key: decryptedLatestKey + }), + value: decryptSymmetric({ + ciphertext: encryptedSecretVersion.secretValueCiphertext, + iv: encryptedSecretVersion.secretValueIV, + tag: encryptedSecretVersion.secretValueTag, + key: decryptedLatestKey + }) + } + }) + + setSnapshotData({ id: secretSnapshotData._id, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions }) + } + + return
+ {isLoading ? ( +
+ infisical loading indicator +
+ ) : ( +
+
+

{t("Point-in-time Recovery")}

+
toggleSidebar(false)}> + +
+
+
+ {secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
+
+
{timeSince(new Date(snapshot.createdAt))}
+
{" - " + snapshot.secretVersions.length + " Secrets"}
+
+
exploreSnapshot({ snapshotId: snapshot._id })} + className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}> + {id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"} +
+
)} +
+
+
+
+
+
+ )} +
+}; + +export default PITRecoverySidebar; diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index 24c0a850c..d6e104f57 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -6,8 +6,9 @@ import { useTranslation } from "next-i18next"; import { faArrowDownAZ, faArrowDownZA, + faArrowLeft, faCheck, - faCopy, + faClockRotateLeft, faDownload, faEye, faEyeSlash, @@ -16,6 +17,8 @@ import { faPlus, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import getProjectSercetSnapshotsCount from 'ee/api/secrets/GetProjectSercetSnapshotsCount'; +import PITRecoverySidebar from 'ee/components/PITRecoverySidebar'; import Button from '~/components/basic/buttons/Button'; import ListBox from '~/components/basic/Listbox'; @@ -30,12 +33,13 @@ import pushKeys from '~/components/utilities/secrets/pushKeys'; import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps'; import guidGenerator from '~/utilities/randomId'; -import { envMapping } from '../../public/data/frequentConstants'; +import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants'; import getUser from '../api/user/getUser'; import checkUserAction from '../api/userActions/checkUserAction'; import registerUserAction from '../api/userActions/registerUserAction'; import getWorkspaces from '../api/workspace/getWorkspaces'; +const queryString = require("query-string"); interface SecretDataProps { type: 'personal' | 'shared'; @@ -46,6 +50,19 @@ interface SecretDataProps { comment: string; } +interface SnapshotProps { + id: string; + createdAt: string; + secretVersions: { + id: string; + pos: number; + type: "personal" | "shared"; + environment: string; + key: string; + value: string; + }[]; +} + /** * this function finds the teh duplicates in an array * @param arr - array of anything (e.g., with secret keys and types (personal/shared)) @@ -76,21 +93,20 @@ export default function Dashboard() { const [workspaceId, setWorkspaceId] = useState(''); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); - const [env, setEnv] = useState( - router.asPath.split('?').length == 1 - ? 'Development' - : Object.keys(envMapping).includes(router.asPath.split('?')[1]) - ? router.asPath.split('?')[1] - : 'Development' - ); + const [env, setEnv] = useState('Development'); + const [snapshotEnv, setSnapshotEnv] = useState('Development'); const [isNew, setIsNew] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [searchKeys, setSearchKeys] = useState(''); const [errorDragAndDrop, setErrorDragAndDrop] = useState(false); const [sortMethod, setSortMethod] = useState('alphabetical'); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false); const [sidebarSecretId, toggleSidebar] = useState("None"); + const [PITSidebarOpen, togglePITSidebar] = useState(false); const [sharedToHide, setSharedToHide] = useState([]); + const [snapshotData, setSnapshotData] = useState(); + const [numSnapshots, setNumSnapshots] = useState(); const { t } = useTranslation(); const { createNotification } = useNotificationContext(); @@ -141,17 +157,39 @@ export default function Dashboard() { useEffect(() => { (async () => { try { + console.log(1, 'reloaded') + const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) }) + setNumSnapshots(tempNumSnapshots); const userWorkspaces = await getWorkspaces(); const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); if ( - !listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0]) + !listWorkspaces.includes(router.asPath.split('/')[2]) ) { router.push('/dashboard/' + listWorkspaces[0]); } - if (env != router.asPath.split('?')[1]) { - router.push(router.asPath.split('?')[0] + '?' + env); - } + const user = await getUser(); + setIsNew( + (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3 + ? true + : false + ); + + const userAction = await checkUserAction({ + action: 'first_time_secrets_pushed' + }); + setHasUserEverPushed(userAction ? true : false); + } catch (error) { + console.log('Error', error); + setData(undefined); + } + })(); + }, []); + + useEffect(() => { + (async () => { + try { + setIsLoading(true); setBlurred(true); setWorkspaceId(String(router.query.id)); @@ -173,18 +211,7 @@ export default function Dashboard() { dataToSort?.map((item) => item.key).indexOf(item) ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id) ) - - const user = await getUser(); - setIsNew( - (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3 - ? true - : false - ); - - const userAction = await checkUserAction({ - action: 'first_time_secrets_pushed' - }); - setHasUserEverPushed(userAction ? true : false); + setIsLoading(false); } catch (error) { console.log('Error', error); setData(undefined); @@ -321,12 +348,21 @@ export default function Dashboard() { /** * Save the changes of environment variables and push them to the database */ - const savePush = async () => { - // Format the new object with environment variables - const obj = Object.assign( - {}, - ...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment] })) - ); + const savePush = async (dataToPush?: any[], envToPush?: string) => { + let obj; + // dataToPush is mostly used for rollbacks, otherwise we always take the current state data + if ((dataToPush ?? [])?.length > 0) { + obj = Object.assign( + {}, + ...dataToPush!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] })) + ); + } else { + // Format the new object with environment variables + obj = Object.assign( + {}, + ...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] })) + ); + } // Checking if any of the secret keys start with a number - if so, don't do anything const nameErrors = !Object.keys(obj) @@ -350,13 +386,17 @@ export default function Dashboard() { // Once "Save changed is clicked", disable that button setButtonReady(false); - pushKeys({ obj, workspaceId: String(router.query.id), env }); + console.log(envToPush ? envToPush : env, env, envToPush) + pushKeys({ obj, workspaceId: String(router.query.id), env: envToPush ? envToPush : env }); // If this user has never saved environment variables before, show them a prompt to read docs if (!hasUserEverPushed) { setCheckDocsPopUpVisible(true); await registerUserAction({ action: 'first_time_secrets_pushed' }); } + + // increasing the number of project commits + setNumSnapshots(numSnapshots ?? 0 + 1); }; const addData = (newData: SecretDataProps[]) => { @@ -427,6 +467,11 @@ export default function Dashboard() { setSharedToHide={setSharedToHide} deleteRow={deleteCertainRow} />} + {PITSidebarOpen && }
{checkDocsPopUpVisible && ( @@ -441,9 +486,22 @@ export default function Dashboard() { /> )}
+ {snapshotData && +
+
}
-

{t("dashboard:title")}

- {data?.length == 0 && ( +
+

{snapshotData ? "Secret Snapshot" : t("dashboard:title")}

+ {snapshotData && {new Date(snapshotData.createdAt).toLocaleString()}} +
+ {!snapshotData && data?.length == 0 && (
- {(data?.length !== 0 || buttonReady) && ( -
+
+
+ {(data?.length !== 0 || buttonReady) && !snapshotData && ( +
)} + {snapshotData &&
+
}
- {data?.length !== 0 && ( + {(!snapshotData || data?.length !== 0) && ( <> - + : }
-
+ {!snapshotData &&
-
+
} + {!snapshotData &&
+
}
-
+ {!snapshotData &&
+
} )}
- {data?.length !== 0 ? ( + {isLoading ? ( +
+ infisical loading indicator +
+ ) : ( + data?.length !== 0 ? (
- {data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => ( + {!snapshotData && data?.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase())) + .filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => ( item.key + item.type))?.includes(keyPair.key + keyPair.type)} toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId} + isSnapshot={false} + /> + ))} + {snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key)) + .filter(row => reverseEnvMapping[row.environment] == snapshotEnv) + .filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase())) + .filter(row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions + ?.map((item) => item.key) + .filter( + (item, index) => + index !== + snapshotData.secretVersions?.map((item) => item.key).indexOf(item) + ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id).includes(row.id) && row.type == 'shared')).map((keyPair) => ( + item.key + item.type))?.includes(keyPair.key + keyPair.type)} + toggleSidebar={toggleSidebar} + sidebarSecretId={sidebarSecretId} + isSnapshot={true} /> ))}
-
+ {!snapshotData &&
-
+
}
) : (
- {isKeyAvailable && ( + {isKeyAvailable && !snapshotData && ( ))}
- )} + ))}
diff --git a/frontend/public/locales/en/activity.json b/frontend/public/locales/en/activity.json index b84d570ae..5dff03d1e 100644 --- a/frontend/public/locales/en/activity.json +++ b/frontend/public/locales/en/activity.json @@ -2,6 +2,7 @@ "event": { "readSecrets": "Secrets Viewed", "updateSecrets": "Secrets Updated", - "addSecrets": "Secrets Added" + "addSecrets": "Secrets Added", + "deleteSecrets": "Secrets Deleted" } } From 5428766bf677f39778a3f381b5a6d919a544b645 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 4 Jan 2023 20:17:11 -0500 Subject: [PATCH 66/91] modify getServiceTokenData to return single json --- .../v2/serviceTokenDataController.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index d8f4d4eea..83c8fea94 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -15,9 +15,7 @@ import { * @param res * @returns */ -export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).send({ - serviceTokenData: req.serviceTokenData -}); +export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData); /** * Create new service token data for workspace with id [workspaceId] and @@ -29,9 +27,9 @@ export const getServiceTokenData = async (req: Request, res: Response) => res.st export const createServiceTokenData = async (req: Request, res: Response) => { let serviceToken, serviceTokenData; try { - const { + const { name, - workspaceId, + workspaceId, environment, encryptedKey, iv, @@ -41,10 +39,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const secret = crypto.randomBytes(16).toString('hex'); const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); - - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + serviceTokenData = await new ServiceTokenData({ name, workspace: workspaceId, @@ -56,12 +54,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => { iv, tag }).save(); - + // return service token data without sensitive data serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); - + if (!serviceTokenData) throw new Error('Failed to find service token data'); - + serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; } catch (err) { @@ -90,7 +88,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { const { serviceTokenDataId } = req.params; serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - + } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -98,7 +96,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { message: 'Failed to delete service token data' }); } - + return res.status(200).send({ serviceTokenData }); From 6c88c4dc3677ebac9ee084acaeb943f493e35931 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 4 Jan 2023 17:34:30 -0800 Subject: [PATCH 67/91] Updated the image for signup invites --- frontend/pages/signupinvite.js | 6 +- .../public/images/dragon-signupinvite.svg | 129 ++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 frontend/public/images/dragon-signupinvite.svg diff --git a/frontend/pages/signupinvite.js b/frontend/pages/signupinvite.js index 169c5c014..d32d6038d 100644 --- a/frontend/pages/signupinvite.js +++ b/frontend/pages/signupinvite.js @@ -141,16 +141,16 @@ export default function SignupInvite() { // Step 4 of the sign up process (download the emergency kit pdf) const stepConfirmEmail = (
-

+

Confirm your email

verify email -
+
@@ -176,7 +176,7 @@ const AddServiceTokenDialog = ({ "6 months", "12 months", ]} - width="full" + isFull={true} text={`${t("common:expired-in")}: `} />
@@ -211,7 +211,7 @@ const AddServiceTokenDialog = ({
-
+
- {t("common.click-to-copy")} + {t("common:click-to-copy")}
diff --git a/frontend/components/basic/table/ServiceTokenTable.js b/frontend/components/basic/table/ServiceTokenTable.tsx similarity index 62% rename from frontend/components/basic/table/ServiceTokenTable.js rename to frontend/components/basic/table/ServiceTokenTable.tsx index 6e5cc65ea..a79acd759 100644 --- a/frontend/components/basic/table/ServiceTokenTable.js +++ b/frontend/components/basic/table/ServiceTokenTable.tsx @@ -1,19 +1,37 @@ -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/router'; import { faX } from '@fortawesome/free-solid-svg-icons'; +import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; + +import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; import { reverseEnvMapping } from '../../../public/data/frequentConstants'; import guidGenerator from '../../utilities/randomId'; import Button from '../buttons/Button'; +interface TokenProps { + _id: string; + name: string; + environment: string; + expiresAt: string; +} + +interface ServiceTokensProps { + data: TokenProps[]; + workspaceName: string; + setServiceTokens: (value: TokenProps[]) => void; +} + /** - * This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. + * This is the component that we utilize for the service token table * #TODO: add the possibility of choosing and doing operations on multiple users. - * @param {*} props + * @param {object} obj + * @param {any[]} obj.data - current state of the service token table + * @param {string} obj.workspaceName - name of the current project + * @param {function} obj.setServiceTokens - updating the state of the service token table * @returns */ -const ServiceTokenTable = ({ data, workspaceName }) => { - const router = useRouter(); +const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => { + console.log(data) + const { createNotification } = useNotificationContext(); return (
@@ -30,7 +48,7 @@ const ServiceTokenTable = ({ data, workspaceName }) => { {data?.length > 0 ? ( - data.map((row, index) => { + data.map((row) => { return ( {