Compare commits

...

56 Commits

Author SHA1 Message Date
8fbca05052 remove extract_version 2023-10-17 23:13:04 +01:00
d99830067e bake posthog key in single docker img 2023-10-17 23:08:43 +01:00
cdc8ef95ab add platform version to UI 2023-10-17 23:08:43 +01:00
072e97b956 Fix azure samlConfig 2023-10-17 16:51:46 +01:00
4f26a7cad3 Revert audience change for azure saml 2023-10-17 15:50:31 +01:00
7bb6ff3d0c Merge branch 'main' of https://github.com/Infisical/infisical 2023-10-17 15:32:42 +01:00
ecccec8e35 Attempt fix azure samlConfig 2023-10-17 15:32:34 +01:00
7fd15a06e5 conditionally build standalone infisical 2023-10-17 14:32:56 +01:00
5d4a37004e Merge pull request #1089 from G3root/serve-frontend-from-backend
feat: serve frontend from backend
2023-10-17 14:15:41 +01:00
aa61fd091d remove redundant file from docker ignore 2023-10-17 14:11:16 +01:00
04ac54bcfa run cmd as non root user and update port to non privileged 2023-10-17 14:08:22 +01:00
38dbf1e738 Add missing / to samlConfig callbackURL 2023-10-17 14:03:37 +01:00
ddf9d7848c Update protocol in samlConfig 2023-10-17 12:57:05 +01:00
0b40de49ec remove redis error logs 2023-10-17 12:24:54 +01:00
b1d16cab39 remove promise.all from closeDatabaseHelper 2023-10-17 12:24:18 +01:00
fb7c7045e9 set telemetry post frontend build in standalone docker img 2023-10-17 12:22:08 +01:00
d570828e47 Update path and callbackURL for samlConfig 2023-10-17 12:13:54 +01:00
2a92b6c787 Merge pull request #1093 from akhilmhdh/feat/secret-update-id
feat: changed secret update to use id
2023-10-17 11:14:30 +01:00
ee54fdabe1 feat: changed secret update to use id 2023-10-17 15:38:30 +05:30
136308f299 revert: temp workaround: remove use of get static prop 2023-10-16 20:37:35 +05:30
ba41244877 chore: remove next.js 2023-10-16 20:35:47 +05:30
c4dcf334f0 fix: remove copy config 2023-10-16 20:35:29 +05:30
66bac3ef48 fix: static props not rendered 2023-10-16 20:35:16 +05:30
e5347719c3 temp workaround: remove use of get static prop 2023-10-16 15:03:55 +01:00
275416a08f Remove 1-on-1 pairing link from README 2023-10-16 10:23:24 +01:00
abe1f54aab remove nginx and pm2 2023-10-15 23:58:04 +01:00
13c1e2b349 fix: docker file 2023-10-15 12:09:45 +05:30
f5a9afec61 fix: pm2 config 2023-10-15 11:52:55 +05:30
d0a85c98b2 chore: add docker ignore and git ignore 2023-10-15 11:47:35 +05:30
e0669cae7c chore: update path 2023-10-15 11:47:06 +05:30
2c011b7d53 feat: add custom server 2023-10-14 13:20:27 +05:30
1b24a9b6e9 chore: add next to gitignore 2023-10-14 13:20:14 +05:30
00c173aead chore: add next for backend 2023-10-14 13:19:54 +05:30
2e15ad0625 fix: type 2023-10-14 12:54:26 +05:30
3f0b6dc6c1 Merge pull request #1087 from Infisical/self-hosted-sso-docs-clarification
Add FAQ to self-hosted SSO docs for it not working due to misconfigur…
2023-10-13 16:00:52 +01:00
f766a1eb29 Merge pull request #1078 from akhilmhdh/feat/secret-bug
feat: added support for recursive file creation
2023-10-13 15:57:50 +01:00
543c55b5a6 Add FAQ to self-hosted SSO docs for it not working due to misconfiguration 2023-10-13 15:56:10 +01:00
cdb1d38f99 Merge pull request #1086 from Infisical/del-org-stripe
Patch delete user, org, project session impl and account for organizationId in local storage
2023-10-13 12:46:38 +01:00
0a53b72cce Patch delete user, org, project session impl and account for orgId in localStorage 2023-10-13 11:22:40 +01:00
b921c376b2 Merge pull request #1071 from ragnarbull/improve-logging
Improve-logging
2023-10-12 10:08:39 -04:00
b1ec59eb67 polish error handling 2023-10-12 15:06:37 +01:00
4e6e12932a Merge pull request #1080 from Tchoupinax/main
feat(helm-chart): allow to provide affinity values for the pods
2023-10-12 06:03:56 -04:00
792c382743 update chart version 2023-10-12 11:03:21 +01:00
f5c8e537c9 generate documentation 2023-10-12 11:01:21 +01:00
4bf09a8efc Merge pull request #1079 from Salman2301/patch-1
docs: fix missing export format `yaml`
2023-10-12 05:02:06 -04:00
001265cf2a feat(helm-chart): allow to provide affinity values for the pods 2023-10-11 21:15:21 +02:00
a56a135396 Merge pull request #1067 from Infisical/delete-org
Delete user, organization, project capabilities feature/update
2023-10-11 19:26:19 +01:00
9838c29867 docs: fix missing export format yaml 2023-10-11 18:48:56 +05:30
4f5946b252 feat: added support for recursive file creation 2023-10-11 17:19:34 +05:30
dc23517133 Merge remote-tracking branch 'origin' into delete-org 2023-10-10 14:54:19 +01:00
f119c921d0 remove comment 2023-10-09 13:38:21 +00:00
b6ef55783e Fix: add stack trace errors in logging for prod 2023-10-09 13:37:16 +00:00
feade5d029 Clear react-query cache upon user logout, delete account 2023-10-09 09:00:31 +01:00
8f74d20e74 Fix redirect to unknown org in login case when user is not part of any orgs 2023-10-09 08:31:13 +01:00
0eb7896b59 Merge remote-tracking branch 'origin' into delete-org 2023-10-09 07:47:41 +01:00
9fcecc9c92 Finish preliminary delete user, organization, refactor delete project, create org page 2023-10-09 07:47:20 +01:00
106 changed files with 1995 additions and 576 deletions

View File

@ -1,2 +1,10 @@
backend/node_modules
frontend/node_modules
frontend/node_modules
backend/frontend-build
**/node_modules
**/.next
.dockerignore
.git
README.md
.dockerignore
**/Dockerfile

View File

@ -73,3 +73,6 @@ jobs:
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

6
.gitignore vendored
View File

@ -33,7 +33,7 @@ reports
junit.xml
# next.js
/.next/
.next/
/out/
# production
@ -59,4 +59,6 @@ yarn-error.log*
.infisical.json
# Editor specific
.vscode/*
.vscode/*
frontend-build

View File

@ -1,7 +1,12 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
FROM node:16-alpine AS frontend-dependencies
FROM node:16-alpine AS base
FROM base AS frontend-dependencies
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
@ -11,7 +16,7 @@ COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM node:16-alpine AS frontend-builder
FROM base AS frontend-builder
WORKDIR /app
# Copy dependencies
@ -27,18 +32,20 @@ ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
# Build
RUN npm run build
# Production image
FROM node:16-alpine AS frontend-runner
FROM base AS frontend-runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN adduser --system --uid 1001 non-root-user
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
ARG POSTHOG_API_KEY
@ -48,20 +55,22 @@ ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
USER nextjs
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM node:16-alpine AS backend-build
FROM base AS backend-build
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
WORKDIR /app
@ -69,10 +78,11 @@ COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm run build
# Production stage
FROM node:16-alpine AS backend-runner
FROM base AS backend-runner
WORKDIR /app
@ -81,27 +91,36 @@ RUN npm ci --only-production
COPY --from=backend-build /app .
RUN mkdir frontend-build
# Production stage
FROM node:16-alpine AS production
FROM base AS production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
WORKDIR /
# Install PM2
RUN npm install -g pm2
# Copy ecosystem.config.js
COPY ecosystem.config.js .
RUN apk add --no-cache nginx
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app/ /app/
COPY --from=frontend-runner /app ./backend/frontend-build
EXPOSE 80
ENV PORT 8080
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
WORKDIR /backend
ENV TELEMETRY_ENABLED true
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

View File

@ -133,7 +133,6 @@ Whether it's big or small, we love contributions. Check out our guide to see how
Not sure where to get started? You can:
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
- Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.

View File

@ -26,6 +26,7 @@
],
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
"unused-imports/no-unused-vars": [
"warn",
{

View File

@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
import { EventType } from "../../ee/models";
import { EventType, Role } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
@ -15,7 +15,6 @@ import {
getUserProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";

View File

@ -6,13 +6,11 @@ import {
Organization,
Workspace
} from "../../models";
import { createOrganization as create } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { ACCEPTED, ADMIN } from "../../variables";
import { getLicenseServerUrl, getSiteURL } from "../../config";
import { licenseServerKeyRequest } from "../../config/request";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/organization";
import { ACCEPTED } from "../../variables";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@ -34,36 +32,6 @@ export const getOrganizations = async (req: Request, res: Response) => {
});
};
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { organizationName }
} = await validateRequest(reqValidator.CreateOrgv1, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name: organizationName
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/**
* Return organization with id [organizationId]
* @param req

View File

@ -1,12 +1,15 @@
import { Request, Response } from "express";
import GitAppInstallationSession from "../../ee/models/gitAppInstallationSession";
import {
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks
} from "../../ee/models";
import crypto from "crypto";
import { Types } from "mongoose";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import GitAppOrganizationInstallation from "../../ee/models/gitAppOrganizationInstallation";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import GitRisks, {
import {
STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_NOT_REVOKED,
STATUS_RESOLVED_REVOKED

View File

@ -9,7 +9,6 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
generateFolderId,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
@ -132,9 +131,6 @@ export const createFolder = async (req: Request, res: Response) => {
// space has no folders initialized
if (!folders) {
if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
const id = generateFolderId();
const folder = new Folder({
workspace: workspaceId,
environment,
@ -142,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
id: "root",
name: "root",
version: 1,
children: [{ id, name: folderName, children: [], version: 1 }]
children: []
}
});
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: folder.nodes
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
@ -163,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: id,
folderId: child.id,
folderName,
folderPath: `root/${folderName}`
folderPath: directory
}
},
{
@ -173,26 +170,26 @@ export const createFolder = async (req: Request, res: Response) => {
}
);
return res.json({ folder: { id, name: folderName } });
return res.json({ folder: { id: child.id, name: folderName } });
}
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
if (!hasCreated) return res.json({ folder: child });
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
folderId: child.id
});
await EEAuditLogService.createAuditLog(
@ -201,7 +198,7 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
folderId: child.id,
folderName,
folderPath: directory
}
@ -211,7 +208,7 @@ export const createFolder = async (req: Request, res: Response) => {
}
);
return res.json({ folder });
return res.json({ folder: child });
};
/**

View File

@ -202,12 +202,12 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
);
// delete workspace
await deleteWork({
id: workspaceId
const workspace = await deleteWork({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
message: "Successfully deleted workspace"
workspace
});
};

View File

@ -1,11 +1,25 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models";
import {
Membership,
MembershipOrg,
ServiceAccount,
Workspace
} from "../../models";
import { Role } from "../../ee/models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
import Role from "../../ee/models/role";
import { BadRequestError } from "../../utils/errors";
import { CUSTOM } from "../../variables";
import {
createOrganization as create,
deleteOrganization,
updateSubscriptionOrgQuantity
} from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import {
ACCEPTED,
ADMIN,
CUSTOM
} from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
@ -332,3 +346,60 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response
serviceAccounts
});
};
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { name }
} = await validateRequest(reqValidator.CreateOrgv2, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/**
* Delete organization with id [organizationId]
* @param req
* @param res
*/
export const deleteOrganizationById = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.DeleteOrgv2, req);
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId),
role: ADMIN
});
if (!membershipOrg) throw UnauthorizedRequestError();
const organization = await deleteOrganization({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send({
organization
});
}

View File

@ -5,49 +5,9 @@ import bcrypt from "bcrypt";
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
import { getSaltRounds } from "../../config";
import { validateRequest } from "../../helpers/validation";
import { deleteUser } from "../../helpers/user";
import * as reqValidator from "../../validation";
/**
* Return the current user.
* @param req
* @param res
* @returns
*/
export const getMe = async (req: Request, res: Response) => {
/*
#swagger.summary = "Retrieve the current user on the request"
#swagger.description = "Retrieve the current user on the request"
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
$ref: "#/components/schemas/CurrentUser",
"description": "Current user on request"
}
}
}
}
}
}
*/
const user = await User.findById(req.user._id).select(
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
);
return res.status(200).send({
user
});
};
/**
* Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to
@ -296,3 +256,59 @@ export const deleteMySessions = async (req: Request, res: Response) => {
message: "Successfully revoked all sessions"
});
};
/**
* Return the current user.
* @param req
* @param res
* @returns
*/
export const getMe = async (req: Request, res: Response) => {
/*
#swagger.summary = "Retrieve the current user on the request"
#swagger.description = "Retrieve the current user on the request"
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
$ref: "#/components/schemas/CurrentUser",
"description": "Current user on request"
}
}
}
}
}
}
*/
const user = await User.findById(req.user._id).select(
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
);
return res.status(200).send({
user
});
};
/**
* Delete the current user.
* @param req
* @param res
*/
export const deleteMe = async (req: Request, res: Response) => {
const user = await deleteUser({
userId: req.user._id
});
return res.status(200).send({
user
});
}

View File

@ -476,7 +476,7 @@ export const getSecrets = async (req: Request, res: Response) => {
if (folderId && folderId !== "root") {
const folder = await Folder.findOne({ workspace: workspaceId, environment });
if (!folder) throw BadRequestError({ message: "Folder not found" });
if (!folder) return res.send({ secrets: [] });
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
@ -673,6 +673,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext,
secretValueTag,
secretValueIV,
secretId,
type,
environment,
secretPath,
@ -741,6 +742,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretId,
authData: req.authData,
newSecretName,
secretValueCiphertext,
@ -777,7 +779,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const {
body: { type, environment, secretPath, workspaceId },
body: { type, environment, secretPath, workspaceId, secretId },
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
@ -813,6 +815,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
const { secret } = await SecretService.deleteSecret({
secretName,
secretId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,

View File

@ -23,7 +23,7 @@ import {
memberPermissions
} from "../../services/RoleService";
import { BadRequestError } from "../../../utils/errors";
import Role from "../../models/role";
import { Role } from "../../models";
import { validateRequest } from "../../../helpers/validation";
import { packRules } from "@casl/ability/extra";
@ -212,6 +212,7 @@ export const getUserPermissions = async (req: Request, res: Response) => {
const {
params: { orgId }
} = await validateRequest(GetUserPermission, req);
const { permission } = await getUserOrgPermissions(req.user._id, orgId);
res.status(200).json({

View File

@ -298,6 +298,10 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
message: "Failed to delete service token"
});
await ServiceTokenDataV3Key.findOneAndDelete({
serviceTokenData: serviceTokenData._id
});
await EEAuditLogService.createAuditLog(
req.authData,
{

View File

@ -29,6 +29,4 @@ const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
});
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
export default GitAppInstallationSession;
export const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);

View File

@ -26,6 +26,4 @@ const gitAppOrganizationInstallation = new Schema<Installation>({
});
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
export default GitAppOrganizationInstallation;
export const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);

View File

@ -5,7 +5,7 @@ export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED";
export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED";
export const STATUS_UNRESOLVED = "UNRESOLVED";
export type GitRisks = {
export type IGitRisks = {
id: string;
description: string;
startLine: string;
@ -42,7 +42,7 @@ export type GitRisks = {
organization: Schema.Types.ObjectId,
}
const gitRisks = new Schema<GitRisks>({
const gitRisks = new Schema<IGitRisks>({
id: {
type: String,
},
@ -147,6 +147,4 @@ const gitRisks = new Schema<GitRisks>({
}
}, { timestamps: true });
const GitRisks = model<GitRisks>("GitRisks", gitRisks);
export default GitRisks;
export const GitRisks = model<IGitRisks>("GitRisks", gitRisks);

View File

@ -2,6 +2,7 @@ export * from "./secretSnapshot";
export * from "./secretVersion";
export * from "./folderVersion";
export * from "./log";
export * from "./role";
export * from "./action";
export * from "./ssoConfig";
export * from "./trustedIp";
@ -9,3 +10,5 @@ export * from "./auditLog";
export * from "./gitRisks";
export * from "./gitAppOrganizationInstallation";
export * from "./gitAppInstallationSession";
export * from "./secretApprovalPolicy";
export * from "./secretApprovalRequest";

View File

@ -50,6 +50,4 @@ const roleSchema = new Schema<IRole>(
roleSchema.index({ organization: 1, workspace: 1 });
const Role = model<IRole>("Role", roleSchema);
export default Role;
export const Role = model<IRole>("Role", roleSchema);

View File

@ -1,6 +1,8 @@
import { Probot } from "probot";
import GitRisks from "../../models/gitRisks";
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
import {
GitAppOrganizationInstallation,
GitRisks
} from "../../models";
import { scanGithubPushEventForSecretLeaks } from "../../../queues/secret-scanning/githubScanPushEvent";
export default async (app: Probot) => {
app.on("installation.deleted", async (context) => {

View File

@ -14,7 +14,7 @@ export const initDatabaseHelper = async ({
}) => {
try {
await mongoose.connect(mongoURL);
// allow empty strings to pass the required validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
@ -31,14 +31,10 @@ export const initDatabaseHelper = async ({
* Close database conection
*/
export const closeDatabaseHelper = async () => {
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
mongoose.connection.close()
.then(() => resolve("Database connection closed"));
} else {
resolve("Database connection already closed");
}
}),
]);
}
if (mongoose.connection && mongoose.connection.readyState === 1) {
await mongoose.connection.close();
return "Database connection closed";
} else {
return "Database connection already closed";
}
};

View File

@ -1,5 +1,43 @@
import { Types } from "mongoose";
import { MembershipOrg, Organization } from "../models";
import mongoose, { Types, mongo } from "mongoose";
import {
Bot,
BotKey,
BotOrg,
Folder,
IncidentContactOrg,
Integration,
IntegrationAuth,
Key,
Membership,
MembershipOrg,
Organization,
Secret,
SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag,
Webhook,
Workspace
} from "../models";
import {
Action,
AuditLog,
FolderVersion,
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks,
Log,
Role,
SSOConfig,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../ee/models";
import {
ACCEPTED,
} from "../variables";
@ -17,6 +55,7 @@ import {
import {
createBotOrg
} from "./botOrg";
import { InternalServerError, ResourceNotFoundError } from "../utils/errors";
/**
* Create an organization with name [name]
@ -65,6 +104,320 @@ export const createOrganization = async ({
return organization;
};
/**
* Delete organization with id [organizationId]
* @param {Object} obj
* @param {Types.ObjectId} obj.organizationId - id of organization to delete
* @returns
*/
export const deleteOrganization = async ({
organizationId,
existingSession
}: {
organizationId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const organization = await Organization.findByIdAndDelete(
organizationId,
{
session
}
);
if (!organization) throw ResourceNotFoundError();
await MembershipOrg.deleteMany({
organization: organization._id
}, {
session
});
await BotOrg.deleteMany({
organization: organization._id
}, {
session
});
await SSOConfig.deleteMany({
organization: organization._id
}, {
session
});
await Role.deleteMany({
organization: organization._id
}, {
session
});
await IncidentContactOrg.deleteMany({
organization: organization._id
}, {
session
});
await GitRisks.deleteMany({
organization: organization._id
}, {
session
});
await GitAppInstallationSession.deleteMany({
organization: organization._id
}, {
session
});
await GitAppOrganizationInstallation.deleteMany({
organization: organization._id
}, {
session
});
const workspaceIds = await Workspace.distinct("_id", {
organization: organization._id
});
await Workspace.deleteMany({
organization: organization._id
}, {
session
});
await Membership.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Key.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Bot.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await BotKey.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretBlindIndexData.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Secret.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretVersion.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretSnapshot.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretImport.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Folder.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await FolderVersion.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Webhook.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await TrustedIP.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Tag.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await IntegrationAuth.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Integration.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceToken.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenData.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenDataV3.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenDataV3Key.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await AuditLog.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Log.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Action.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretApprovalPolicy.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretApprovalRequest.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
if (organization.customerId) {
// delete from stripe here
await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}`
);
}
return organization;
} catch (err) {
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete organization"
});
} finally {
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}
/**
* Update organization subscription quantity to reflect number of members in
* the organization.

View File

@ -57,10 +57,10 @@ import { getAnImportedSecret } from "../services/SecretImportService";
/**
* Validate scope for service token v3
* @param authPayload
* @param environment
* @param secretPath
* @returns
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScopeV3 = ({
authPayload,
@ -68,37 +68,40 @@ export const isValidScopeV3 = ({
secretPath,
requiredPermissions
}: {
authPayload: IServiceTokenDataV3,
environment: string,
secretPath: string,
requiredPermissions: Permission[]
authPayload: IServiceTokenDataV3;
environment: string;
secretPath: string;
requiredPermissions: Permission[];
}) => {
const { scopes } = authPayload;
const validScope = scopes.find(
(scope) =>
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
scope.environment === environment
);
if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) {
if (
validScope &&
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
) {
return false;
}
return Boolean(validScope);
}
};
/**
* Validate scope for service token v2
* @param authPayload
* @param environment
* @param secretPath
* @returns
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScope = (
authPayload: IServiceTokenData,
environment: string,
secretPath: string,
secretPath: string
) => {
const { scopes: tkScopes } = authPayload;
const validScope = tkScopes.find(
@ -787,6 +790,7 @@ export const getSecretHelper = async ({
export const updateSecretHelper = async ({
secretName,
workspaceId,
secretId,
environment,
type,
authData,
@ -809,11 +813,20 @@ export const updateSecretHelper = async ({
workspaceId: new Types.ObjectId(workspaceId)
});
const oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
let oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
if (secretId) {
const secret = await Secret.findOne({
workspace: workspaceId,
environment,
_id: secretId
}).select("secretBlindIndex");
if (secret && secret.secretBlindIndex) oldSecretBlindIndex = secret.secretBlindIndex;
}
let secret: ISecret | null = null;
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
@ -888,6 +901,9 @@ export const updateSecretHelper = async ({
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
$inc: { version: 1 }
},
{
new: true
}
);
}
@ -1000,12 +1016,22 @@ export const deleteSecretHelper = async ({
environment,
type,
authData,
secretPath = "/"
secretPath = "/",
// used for update corner case and blindIndex goes wrong way
secretId
}: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
let secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
if (secretId) {
const secret = await Secret.findOne({
workspace: workspaceId,
environment,
_id: secretId
}).select("secretBlindIndex");
if (secret && secret.secretBlindIndex) secretBlindIndex = secret.secretBlindIndex;
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);

View File

@ -1,5 +1,27 @@
import { IUser, User } from "../models";
import mongoose, { Types, mongo } from "mongoose";
import {
APIKeyData,
BackupPrivateKey,
IUser,
Key,
Membership,
MembershipOrg,
TokenVersion,
User,
UserAction
} from "../models";
import {
Action,
Log
} from "../ee/models";
import { sendMail } from "./nodemailer";
import {
InternalServerError,
ResourceNotFoundError
} from "../utils/errors";
import { ADMIN } from "../variables";
import { deleteOrganization } from "../helpers/organization";
import { deleteWorkspace } from "../helpers/workspace";
/**
* Initialize a user under email [email]
@ -134,3 +156,207 @@ export const checkUserDevice = async ({
});
}
};
/**
* Check that if we delete user with id [userId] then
* there won't be any admin-less organizations or projects
* @param {Object} obj
* @param {String} obj.userId - id of user to check deletion conditions for
*/
const checkDeleteUserConditions = async ({
userId
}: {
userId: Types.ObjectId;
}) => {
const memberships = await Membership.find({
user: userId
});
const membershipOrgs = await MembershipOrg.find({
user: userId
});
// delete organizations where user is only member
for await (const membershipOrg of membershipOrgs) {
const orgMemberCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization,
});
const otherOrgAdminCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization,
user: { $ne: userId },
role: ADMIN
});
if (orgMemberCount > 1 && otherOrgAdminCount === 0) {
throw InternalServerError({
message: "Failed to delete account because an org would be admin-less"
});
}
}
// delete workspaces where user is only member
for await (const membership of memberships) {
const workspaceMemberCount = await Membership.countDocuments({
workspace: membership.workspace
});
const otherWorkspaceAdminCount = await Membership.countDocuments({
workspace: membership.workspace,
user: { $ne: userId },
role: ADMIN
});
if (workspaceMemberCount > 1 && otherWorkspaceAdminCount === 0) {
throw InternalServerError({
message: "Failed to delete account because a workspace would be admin-less"
});
}
}
}
/**
* Delete account with id [userId]
* @param {Object} obj
* @param {Types.ObjectId} obj.userId - id of user to delete
* @returns {User} user - deleted user
*/
export const deleteUser = async ({
userId,
existingSession
}: {
userId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const user = await User.findByIdAndDelete(userId, {
session
});
if (!user) throw ResourceNotFoundError();
await checkDeleteUserConditions({
userId: user._id
});
await UserAction.deleteMany({
user: user._id
}, {
session
});
await BackupPrivateKey.deleteMany({
user: user._id
}, {
session
});
await APIKeyData.deleteMany({
user: user._id
}, {
session
});
await Action.deleteMany({
user: user._id
}, {
session
});
await Log.deleteMany({
user: user._id
}, {
session
});
await TokenVersion.deleteMany({
user: user._id
});
await Key.deleteMany({
receiver: user._id
}, {
session
});
const membershipOrgs = await MembershipOrg.find({
user: userId
}, null, {
session
});
// delete organizations where user is only member
for await (const membershipOrg of membershipOrgs) {
const memberCount = await MembershipOrg.countDocuments({
organization: membershipOrg.organization
});
if (memberCount === 1) {
// organization only has 1 member (the current user)
await deleteOrganization({
organizationId: membershipOrg.organization,
existingSession: session
});
}
}
const memberships = await Membership.find({
user: userId
}, null, {
session
});
// delete workspaces where user is only member
for await (const membership of memberships) {
const memberCount = await Membership.countDocuments({
workspace: membership.workspace
});
if (memberCount === 1) {
// workspace only has 1 member (the current user) -> delete workspace
await deleteWorkspace({
workspaceId: membership.workspace,
existingSession: session
});
}
}
await MembershipOrg.deleteMany({
user: userId
}, {
session
});
await Membership.deleteMany({
user: userId
}, {
session
});
return user;
} catch (err) {
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete account"
})
} finally {
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}

View File

@ -1,18 +1,42 @@
import { Types } from "mongoose";
import mongoose, { Types, mongo } from "mongoose";
import {
Bot,
BotKey,
Folder,
Integration,
IntegrationAuth,
Key,
Membership,
Secret,
Workspace,
SecretBlindIndexData,
SecretImport,
ServiceToken,
ServiceTokenData,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Tag,
Webhook,
Workspace
} from "../models";
import {
Action,
AuditLog,
FolderVersion,
IPType,
Log,
SecretApprovalPolicy,
SecretApprovalRequest,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../ee/models";
import { createBot } from "../helpers/bot";
import { EELicenseService } from "../ee/services";
import { SecretService } from "../services";
import {
InternalServerError,
ResourceNotFoundError
} from "../utils/errors";
/**
* Create a workspace with name [name] in organization with id [organizationId]
@ -77,18 +101,190 @@ export const createWorkspace = async ({
* @param {Object} obj
* @param {String} obj.id - id of workspace to delete
*/
export const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id,
});
await Membership.deleteMany({
workspace: id,
});
await Secret.deleteMany({
workspace: id,
});
await Key.deleteMany({
workspace: id,
});
export const deleteWorkspace = async ({
workspaceId,
existingSession
}: {
workspaceId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const workspace = await Workspace.findByIdAndDelete(workspaceId, { session });
if (!workspace) throw ResourceNotFoundError();
await Membership.deleteMany({
workspace: workspace._id
}, {
session
});
await Key.deleteMany({
workspace: workspace._id
}, {
session
});
await Bot.deleteMany({
workspace: workspace._id
}, {
session
});
await BotKey.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretBlindIndexData.deleteMany({
workspace: workspace._id
}, {
session
});
await Secret.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretVersion.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretSnapshot.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretImport.deleteMany({
workspace: workspace._id
}, {
session
});
await Folder.deleteMany({
workspace: workspace._id
}, {
session
});
await FolderVersion.deleteMany({
workspace: workspace._id
}, {
session
});
await Webhook.deleteMany({
workspace: workspace._id
}, {
session
});
await TrustedIP.deleteMany({
workspace: workspace._id
}, {
session
});
await Tag.deleteMany({
workspace: workspace._id
}, {
session
});
await IntegrationAuth.deleteMany({
workspace: workspace._id
}, {
session
});
await Integration.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceToken.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenData.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenDataV3.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenDataV3Key.deleteMany({
workspace: workspace._id
}, {
session
});
await AuditLog.deleteMany({
workspace: workspace._id
}, {
session
});
await Log.deleteMany({
workspace: workspace._id
}, {
session
});
await Action.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretApprovalPolicy.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretApprovalRequest.deleteMany({
workspace: workspace._id
}, {
session
});
return workspace;
} catch (err) {
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete organization"
});
} finally {
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
};

View File

@ -87,6 +87,9 @@ import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require("smee-client"); // eslint-disable-line
import path from "path";
let handler: null | any = null;
const main = async () => {
await setup();
@ -147,6 +150,27 @@ const main = async () => {
next();
});
if ((await getNodeEnv()) === "production" && process.env.STANDALONE_BUILD === "true") {
const nextJsBuildPath = path.join(__dirname, "../frontend-build");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-var-requires
const conf = require("../frontend-build/.next/required-server-files.json").config;
const NextServer =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("../frontend-build/node_modules/next/dist/server/next-server").default;
const nextApp = new NextServer({
dev: false,
dir: nextJsBuildPath,
port: await getPort(),
conf,
hostname: "local",
customServer: false
});
handler = nextApp.getRequestHandler();
}
// (EE) routes
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
@ -209,6 +233,12 @@ const main = async () => {
// server status
app.use("/api", healthCheck);
if (handler) {
app.all("*", (req, res) => {
return handler(req, res);
});
}
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next) => {
if (res.headersSent) return next();

View File

@ -44,6 +44,7 @@ export interface GetSecretParams {
export interface UpdateSecretParams {
secretName: string;
newSecretName?: string;
secretId?: string;
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
@ -64,6 +65,7 @@ export interface UpdateSecretParams {
export interface DeleteSecretParams {
secretName: string;
secretId?: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";

View File

@ -3,8 +3,8 @@ import { ErrorRequestHandler } from "express";
import { TokenExpiredError } from "jsonwebtoken";
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
import { getNodeEnv } from "../config";
import RequestError from "../utils/requestError";
import { ForbiddenError } from "@casl/ability";
export const requestErrorHandler: ErrorRequestHandler = async (
error: RequestError | Error,
@ -14,41 +14,37 @@ export const requestErrorHandler: ErrorRequestHandler = async (
) => {
if (res.headersSent) return next();
if (await getNodeEnv() !== "production") {
/* eslint-disable no-console */
console.error(error);
}
//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 TokenExpiredError) {
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
} else if (!(error instanceof RequestError)) {
error = InternalServerError({
context: { exception: error.message },
stack: error.stack,
});
const logAndCaptureException = async (error: RequestError) => {
(await getLogger("backend-main")).log(
(<RequestError>error).levelName.toLowerCase(),
(<RequestError>error).message
`${error.stack}\n${error.message}`
);
}
//* Set Sentry user identification if req.user is populated
if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: (req.user as any).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(
(<RequestError>error).level
)
) {
//* Set Sentry user identification if req.user is populated
if (req.user !== undefined && req.user !== null) {
Sentry.setUser({ email: (req.user as any).email });
}
Sentry.captureException(error);
};
if (error instanceof RequestError) {
if (error instanceof TokenExpiredError) {
error = UnauthorizedRequestError({ stack: error.stack, message: "Token expired" });
}
await logAndCaptureException((<RequestError>error));
} else {
if (error instanceof ForbiddenError) {
error = UnauthorizedRequestError({ context: { exception: error.message }, stack: error.stack })
} else {
error = InternalServerError({ context: { exception: error.message }, stack: error.stack });
}
await logAndCaptureException((<RequestError>error));
}
res
.status((<RequestError>error).statusCode)
.json((<RequestError>error).format(req));
delete (<any>error).stacktrace // remove stack trace from being sent to client
res.status((<RequestError>error).statusCode).json(error);
next();
};

View File

@ -40,9 +40,9 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
const prefix = (integration.metadata?.secretPrefix || "");
const suffix = (integration.metadata?.secretSuffix || "");
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
}
}
}
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
@ -67,7 +67,7 @@ syncSecretsToThirdPartyServices.process(async (job: Job) => {
})
syncSecretsToThirdPartyServices.on("error", (error) => {
console.log("QUEUE ERROR:", error) // eslint-disable-line
// console.log("QUEUE ERROR:", error) // eslint-disable-line
})
export const syncSecretsToActiveIntegrationsQueue = (jobDetails: TSyncSecretsToThirdPartyServices) => {

View File

@ -2,7 +2,7 @@ import Queue, { Job } from "bull";
import { ProbotOctokit } from "probot"
import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks";
import { GitRisks } from "../../ee/models";
import { MembershipOrg, User } from "../../models";
import { ADMIN } from "../../variables";
import { convertKeysToLowercase, scanFullRepoContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";

View File

@ -3,7 +3,7 @@ import { ProbotOctokit } from "probot"
import { Commit } from "@octokit/webhooks-types";
import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks";
import { GitRisks } from "../../ee/models";
import { MembershipOrg, User } from "../../models";
import { ADMIN } from "../../variables";
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";

View File

@ -13,15 +13,6 @@ router.get(
organizationController.getOrganizations
);
router.post(
// not used on frontend
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationController.createOrganization
);
router.get(
"/:organizationId",
requireAuth({

View File

@ -54,4 +54,20 @@ router.get(
organizationsController.getOrganizationServiceAccounts
);
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.createOrganization
);
router.delete(
"/:organizationId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
organizationsController.deleteOrganizationById
);
export default router;

View File

@ -4,14 +4,6 @@ import { requireAuth } from "../../middleware";
import { usersController } from "../../controllers/v2";
import { AuthMode } from "../../variables";
router.get(
"/me",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.getMe
);
router.patch(
"/me/mfa",
requireAuth({
@ -84,4 +76,20 @@ router.delete(
usersController.deleteMySessions
);
router.get(
"/me",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.getMe
);
router.delete(
"/me",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
usersController.deleteMe
);
export default router;

View File

@ -5,7 +5,7 @@ import path from "path";
type TAppendFolderDTO = {
folderName: string;
parentFolderId?: string;
directory: string;
};
type TRenameFolderDTO = {
@ -50,9 +50,8 @@ export const folderBfsTraversal = async (
// bfs and then append to the folder
const appendChild = (folders: TFolderSchema, folderName: string) => {
const folder = folders.children.find(({ name }) => name === folderName);
if (folder) {
throw new Error("Folder already exists");
}
if (folder) return { folder, hasCreated: false };
const id = generateFolderId();
folders.version += 1;
folders.children.push({
@ -61,24 +60,32 @@ const appendChild = (folders: TFolderSchema, folderName: string) => {
children: [],
version: 1
});
return { id, name: folderName };
// last element that is the new one
return { folder: folders.children[folders.children.length - 1], hasCreated: true };
};
// root of append child wrapper
export const appendFolder = (
folders: TFolderSchema,
{ folderName, parentFolderId }: TAppendFolderDTO
) => {
const isRoot = !parentFolderId;
{ folderName, directory }: TAppendFolderDTO
): { parent: TFolderSchema; child: TFolderSchema; hasCreated?: boolean } => {
if (directory === "/") {
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
}
if (isRoot) {
return appendChild(folders, folderName);
const segments = directory.split("/").filter(Boolean);
const segment = segments.shift();
if (segment) {
const nestedFolders = appendChild(folders, segment);
return appendFolder(nestedFolders.folder, {
folderName,
directory: path.join("/", ...segments)
});
}
const folder = searchByFolderId(folders, parentFolderId);
if (!folder) {
throw new Error("Parent Folder not found");
}
return appendChild(folder, folderName);
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
};
export const renameFolder = (

View File

@ -291,8 +291,7 @@ const initializePassport = async () => {
});
interface ISAMLConfig {
path: string;
callbackURL: string;
callbackUrl: string;
entryPoint: string;
issuer: string;
cert: string;
@ -301,8 +300,7 @@ const initializePassport = async () => {
}
const samlConfig: ISAMLConfig = ({
path: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
callbackURL: `${await getSiteURL()}/api/v1/sso/saml2${ssoIdentifier}`,
callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer,
cert: ssoConfig.cert,
@ -313,6 +311,10 @@ const initializePassport = async () => {
samlConfig.wantAuthnResponseSigned = false;
}
if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) {
samlConfig.audience = `spn:${ssoConfig.issuer}`;
}
req.ssoConfig = ssoConfig;
done(null, samlConfig);

View File

@ -12,6 +12,27 @@ export enum LogLevel {
EMERGENCY = 600,
}
export const mapToWinstonLogLevel = (customLogLevel: LogLevel): string => {
switch (customLogLevel) {
case LogLevel.DEBUG:
return "debug";
case LogLevel.INFO:
return "info";
case LogLevel.NOTICE:
return "notice";
case LogLevel.WARNING:
return "warn";
case LogLevel.ERROR:
return "error";
case LogLevel.CRITICAL:
return "crit";
case LogLevel.ALERT:
return "alert";
case LogLevel.EMERGENCY:
return "emerg";
}
}
export type RequestErrorContext = {
logLevel?: LogLevel,
statusCode: number,
@ -87,7 +108,8 @@ export default class RequestError extends Error{
}, this.context)
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer
if(!(await getVerboseErrorOutput())){
const verboseErrorOutput = await getVerboseErrorOutput();
if (verboseErrorOutput !== undefined) {
_context = this._omit(_context, [
"stacktrace",
"exception",
@ -110,4 +132,4 @@ export default class RequestError extends Error{
return formatObject
}
}
}

View File

@ -4,7 +4,14 @@ import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
import { EESecretService } from "../../ee/services";
import { redisClient } from "../../services/RedisService"
import { IPType, ISecretVersion, SecretSnapshot, SecretVersion, TrustedIP } from "../../ee/models";
import {
IPType,
ISecretVersion,
Role,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../../ee/models";
import {
AuthMethod,
BackupPrivateKey,
@ -34,14 +41,12 @@ import {
MEMBER,
OWNER
} from "../../variables";
import { InternalServerError } from "../errors";
import {
ProjectPermissionActions,
ProjectPermissionSub,
memberProjectPermissions
} from "../../ee/services/ProjectRoleService";
import Role from "../../ee/models/role";
/**
* Backfill secrets to ensure that they're all versioned and have

View File

@ -55,9 +55,6 @@ export const setup = async () => {
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
await initializePassport();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY

View File

@ -136,12 +136,6 @@ export const GetOrgLicencesv1 = z.object({
params: z.object({ organizationId: z.string().trim() })
});
export const CreateOrgv1 = z.object({
body: z.object({
organizationName: z.string().trim()
})
});
export const GetOrgv1 = z.object({
params: z.object({
organizationId: z.string().trim()
@ -209,3 +203,13 @@ export const VerfiyUserToOrganizationV1 = z.object({
code: z.string().trim()
})
});
export const CreateOrgv2 = z.object({
body: z.object({
name: z.string().trim()
})
});
export const DeleteOrgv2 = z.object({
params: z.object({ organizationId: z.string().trim() })
});

View File

@ -353,6 +353,7 @@ export const UpdateSecretByNameV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretId: z.string().trim().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretPath: z.string().trim().default("/"),
secretValueCiphertext: z.string().trim(),
@ -379,7 +380,8 @@ export const DeleteSecretByNameV3 = z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretPath: z.string().trim().default("/")
secretPath: z.string().trim().default("/"),
secretId: z.string().trim().optional()
}),
params: z.object({
secretName: z.string()

View File

@ -88,7 +88,7 @@ Export environment variables from the platform into a file format.
</Accordion>
<Accordion title="--format">
Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv` and `json`
Format of the output file. Accepted values: `dotenv`, `dotenv-export`, `csv`, `json` and `yaml`
Default value: `dotenv`
</Accordion>

View File

@ -89,3 +89,13 @@ Back in Azure, navigate to the **Users and groups** tab and select **+ Add user/
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Azure.
![Azure SAML assignment](../../../images/sso/azure/enable-saml.png)
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `JWT_PROVIDER_AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `JWT_PROVIDER_AUTH_SECRET`: This is secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@ -29,9 +29,24 @@ Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OA
![GCP obtain OAuth2 credentials](../../../images/sso/github/credentials.png)
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
Back in your Infisical instance, make sure to set the following environment variables:
- `CLIENT_ID_GITHUB_LOGIN`: The **Client ID** of your GitHub OAuth application.
- `CLIENT_SECRET_GITHUB_LOGIN`: The **Client Secret** of your GitHub OAuth application.
- `JWT_PROVIDER_AUTH_SECRET`: A secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
Once added, restart your Infisical instance and log in with GitHub.
Once added, restart your Infisical instance and log in with GitHub.
## FAQ
<AccordionGroup>
<Accordion title="Why is GitHub SSO not working?">
It is likely that you have misconfigured your self-hosted instance of Infisical. You should:
- Check that you have set the `CLIENT_ID_GITHUB_LOGIN`, `CLIENT_SECRET_GITHUB_LOGIN`,
`JWT_PROVIDER_AUTH_SECRET`, and `SITE_URL` environment variables.
- Check that the **Authorization callback URL** specified in GitHub matches the `SITE_URL` environment variable.
For example, if the former is `https://app.infisical.com/api/v1/sso/github` then the latter should be `https://app.infisical.com`.
</Accordion>
</AccordionGroup>

View File

@ -28,10 +28,25 @@ Obtain the **Application ID** and **Secret** for your GitLab application.
![sso gitlab config](/images/sso/gitlab/credentials.png)
Back in your Infisical instance, add 2-3 new environment variables for the credentials of your GitLab application:
Back in your Infisical instance, make sure to set the following environment variables:
- `CLIENT_ID_GITLAB_LOGIN`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB_LOGIN`: The **Secret** of your GitLab application.
- (optional) `URL_GITLAB_LOGIN`: The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to `https://gitlab.com`.
- `JWT_PROVIDER_AUTH_SECRET`: A secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
Once added, restart your Infisical instance and log in with GitLab.
Once added, restart your Infisical instance and log in with GitLab.
## FAQ
<AccordionGroup>
<Accordion title="Why is GitLab SSO not working?">
It is likely that you have misconfigured your self-hosted instance of Infisical. You should:
- Check that you have set the `CLIENT_ID_GITLAB_LOGIN`, `CLIENT_SECRET_GITLAB_LOGIN`,
`JWT_PROVIDER_AUTH_SECRET`, and `SITE_URL` environment variables.
- Check that the **Redirect URI** specified in GitLab matches the `SITE_URL` environment variable.
For example, if the former is `https://app.infisical.com/api/v1/sso/gitlab` then the latter should be `https://app.infisical.com`.
</Accordion>
</AccordionGroup>

View File

@ -22,9 +22,24 @@ Obtain the **Client ID** and **Client Secret** for your GCP OAuth2 application.
![GCP obtain OAuth2 credentials](../../../images/sso/google/credentials.png)
Back in your Infisical instance, add two new environment variables for the credentials of your GCP OAuth2 application:
Back in your Infisical instance, make sure to set the following environment variables:
- `CLIENT_ID_GOOGLE_LOGIN`: The **Client ID** of your GCP OAuth2 application.
- `CLIENT_SECRET_GOOGLE_LOGIN`: The **Client Secret** of your GCP OAuth2 application.
- `JWT_PROVIDER_AUTH_SECRET`: A secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
Once added, restart your Infisical instance and log in with Google
Once added, restart your Infisical instance and log in with Google
## FAQ
<AccordionGroup>
<Accordion title="Why is Google SSO not working?">
It is likely that you have misconfigured your self-hosted instance of Infisical. You should:
- Check that you have set the `CLIENT_ID_GOOGLE_LOGIN`, `CLIENT_SECRET_GOOGLE_LOGIN`,
`JWT_PROVIDER_AUTH_SECRET`, and `SITE_URL` environment variables.
- Check that the **Authorized redirect URI** specified in GCP matches the `SITE_URL` environment variable.
For example, if the former is `https://app.infisical.com/api/v1/sso/google` then the latter should be `https://app.infisical.com`.
</Accordion>
</AccordionGroup>

View File

@ -72,3 +72,11 @@ Back in JumpCloud, navigate to the **User Groups** tab and assign users to the n
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via JumpCloud.
![JumpCloud SAML assignment](../../../images/sso/jumpcloud/enable-saml.png)
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `JWT_PROVIDER_AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `JWT_PROVIDER_AUTH_SECRET`: This is secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@ -77,3 +77,11 @@ At this point, you have configured everything you need within the context of the
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Okta.
![SAML Okta assignment](../../../images/sso/okta/enable-saml.png)
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `JWT_PROVIDER_AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `JWT_PROVIDER_AUTH_SECRET`: This is secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@ -1,32 +0,0 @@
module.exports = {
apps: [
{
name: 'frontend',
script: "./scripts/start.sh",
instances: 1,
cwd: "./app",
interpreter: 'sh',
exec_mode: "fork",
autorestart: true,
watch: false,
max_memory_restart: '500M',
},
{
name: 'backend',
script: 'npm',
args: 'run start',
cwd: "./backend",
instances: 1,
exec_mode: "fork",
autorestart: true,
watch: false,
max_memory_restart: '500M',
},
{
name: "nginx",
script: "nginx",
args: "-g 'daemon off;'",
exec_interpreter: "none",
},
],
};

View File

@ -1,8 +1,4 @@
// @ts-check
/**
* @type {import('next').NextConfig}
**/
const path = require("path");
const ContentSecurityPolicy = `
@ -53,7 +49,9 @@ const securityHeaders = [
value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim()
}
];
/**
* @type {import('next').NextConfig}
**/
module.exports = {
output: "standalone",
i18n: {

View File

@ -1,5 +1,5 @@
{
"name": "npm-proj-1695919945735-0.225773463026700768rr1Oh",
"name": "frontend",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -0,0 +1,13 @@
#!/bin/sh
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_POSTHOG_API_KEY"
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"
scripts/set-standalone-build-telemetry.sh true
else
echo "Client opted out of telemetry"
scripts/set-standalone-build-telemetry.sh false
fi

View File

@ -0,0 +1,16 @@
#!/bin/sh
ORIGINAL=$1
REPLACEMENT=$2
if [ "${ORIGINAL}" = "${REPLACEMENT}" ]; then
echo "Environment variable replacement is the same, skipping.."
exit 0
fi
echo "Replacing pre-baked value.."
find public .next -type f -name "*.js" |
while read file; do
sed -i "s|$ORIGINAL|$REPLACEMENT|g" "$file"
done

0
frontend/scripts/replace-variable.sh Normal file → Executable file
View File

View File

@ -0,0 +1,8 @@
#!/bin/sh
VALUE=$1
find public .next -type f -name "*.js" |
while read file; do
sed -i "s|TELEMETRY_CAPTURING_ENABLED|$VALUE|g" "$file"
done

0
frontend/scripts/set-telemetry.sh Normal file → Executable file
View File

View File

@ -10,7 +10,7 @@ export const initPostHog = () => {
try {
if (typeof window !== "undefined") {
// @ts-ignore
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED) {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST
});

View File

@ -2,8 +2,6 @@
import jsrp from "jsrp";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";
@ -122,17 +120,7 @@ const attemptLogin = async (
iv,
tag,
privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
if (orgUserProjects.length > 0) {
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
}
});
if (email) {
telemetry.identify(email, email);

View File

@ -2,9 +2,6 @@
import jsrp from "jsrp";
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
// import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
import KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
@ -19,7 +16,6 @@ interface IsMfaLoginSuccessful {
privateKey: string;
JTWToken: string;
}
}
/**
@ -93,16 +89,6 @@ const attemptLoginMfa = async ({
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
resolve({
success: true,
loginResponse:{

View File

@ -2,8 +2,6 @@
import jsrp from "jsrp";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";
@ -119,20 +117,6 @@ const attemptLogin = async (
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
if (orgUserProjects.length > 0) {
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
}
if (email) {
telemetry.identify(email, email);

View File

@ -2,8 +2,6 @@
import jsrp from "jsrp";
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
import KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
@ -83,16 +81,6 @@ const attemptLoginMfa = async ({
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
resolve(true);
} catch (err) {
reject(err);

View File

@ -13,7 +13,7 @@ class Capturer {
}
capture(item: string) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") {
try {
this.api.capture(item);
} catch (error) {
@ -23,7 +23,7 @@ class Capturer {
}
identify(id: string, email?: string) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") {
try {
this.api.identify(id, {
email: email

View File

@ -37,7 +37,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 z-[70] h-full w-full"
className="fixed inset-0 z-20 h-full w-full"
style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }}
/>
<DialogPrimitive.Content

View File

@ -2,6 +2,8 @@ export {
useAddOrgPmtMethod,
useAddOrgTaxId,
useCreateCustomerPortalSession,
useCreateOrg,
useDeleteOrgById,
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetOrganizations,

View File

@ -41,6 +41,25 @@ export const useGetOrganizations = () => {
});
}
export const useCreateOrg = () => {
return useMutation({
mutationFn: async ({
name
}: {
name: string;
}) => {
const { data: { organization } } = await apiRequest.post(
"/api/v2/organizations",
{
name
}
);
return organization;
}
});
};
export const useRenameOrg = () => {
const queryClient = useQueryClient();
@ -333,4 +352,33 @@ export const useGetOrgLicenses = (organizationId: string) => {
},
enabled: true
});
}
export const useDeleteOrgById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
}: {
organizationId: string;
}) => {
const { data: { organization } } = await apiRequest.delete<{ organization: Organization }>(
`/api/v2/organizations/${organizationId}`
);
return organization;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
queryClient.invalidateQueries(organizationKeys.getOrgPlanBillingInfo(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgPlanTable(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "monthly")); // You might need to invalidate for 'yearly' as well.
queryClient.invalidateQueries(organizationKeys.getOrgPlansTable(dto.organizationId, "yearly"));
queryClient.invalidateQueries(organizationKeys.getOrgBillingDetails(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgInvoices(dto.organizationId));
queryClient.invalidateQueries(organizationKeys.getOrgLicenses(dto.organizationId));
}
});
}

View File

@ -63,10 +63,12 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) =>
});
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
if (orgId === "") return [];
const { data } = await apiRequest.get<{
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
}>(`/api/v1/roles/organization/${orgId}/permissions`, {});
return data.data.permissions;
};
@ -74,7 +76,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
useQuery({
queryKey: roleQueryKeys.getUserOrgPermissions({ orgId }),
queryFn: () => getUserOrgPermissions({ orgId }),
enabled: Boolean(orgId),
// enabled: Boolean(orgId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });

View File

@ -131,6 +131,7 @@ export const useUpdateSecretV3 = ({
mutationFn: async ({
secretPath = "/",
type,
secretId,
environment,
workspaceId,
secretName,
@ -157,6 +158,7 @@ export const useUpdateSecretV3 = ({
environment,
type,
secretPath,
secretId,
...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment),
tags,
skipMultilineEncoding,
@ -189,12 +191,20 @@ export const useDeleteSecretV3 = ({
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretsV3DTO>({
mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => {
mutationFn: async ({
secretPath = "/",
type,
environment,
workspaceId,
secretName,
secretId
}) => {
const reqBody = {
workspaceId,
environment,
type,
secretPath
secretPath,
secretId
};
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {

View File

@ -109,6 +109,7 @@ export type TUpdateSecretsV3DTO = {
skipMultilineEncoding?: boolean;
newSecretName?: string;
secretName: string;
secretId?: string;
secretValue: string;
secretComment?: string;
tags?: string[];
@ -120,6 +121,7 @@ export type TDeleteSecretsV3DTO = {
type: "shared" | "personal";
secretPath: string;
secretName: string;
secretId?: string;
};
export type TCreateSecretBatchDTO = {

View File

@ -5,6 +5,7 @@ export {
useCreateAPIKey,
useDeleteAPIKey,
useDeleteOrgMembership,
useDeleteUser,
useGetMyAPIKeys,
useGetMyIp,
useGetMyOrganizationProjects,

View File

@ -42,6 +42,31 @@ export const fetchUserDetails = async () => {
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const { data: { user } } = await apiRequest.delete<{ user: User }>("/api/v2/users/me");
return user;
},
onSuccess: () => {
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
queryClient.clear();
}
});
};
export const fetchUserAction = async (action: string) => {
const { data } = await apiRequest.get<{ userAction: string }>("/api/v1/user-action", {
params: {
@ -208,21 +233,31 @@ export const useRegisterUserAction = () => {
});
};
export const useLogoutUser = () =>
useMutation({
export const useLogoutUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
await apiRequest.post("/api/v1/auth/logout");
},
onSuccess: () => {
setAuthToken("");
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem("publicKey", "");
localStorage.setItem("encryptedPrivateKey", "");
localStorage.setItem("iv", "");
localStorage.setItem("tag", "");
localStorage.setItem("PRIVATE_KEY", "");
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
queryClient.clear();
}
});
}
export const useGetMyIp = () => {
return useQuery({

View File

@ -34,7 +34,6 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
@ -115,6 +114,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// eslint-disable-next-line prefer-const
const { workspaces, currentWorkspace } = useWorkspace();
const { orgs, currentOrg } = useOrganization();
const { user } = useUser();
const { subscription } = useSubscription();
const workspaceId = currentWorkspace?._id || "";
@ -157,16 +157,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
try {
console.log("Logging out...");
await logout.mutateAsync();
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
router.push("/login");
} catch (error) {
console.error(error);
@ -223,7 +213,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
// }
};
putUserInOrg();
onboardingCheck({});
}, [router.query.id]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
@ -703,7 +692,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
{infisicalPlatformVersion && (
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Platform Version: {infisicalPlatformVersion}
Version: {infisicalPlatformVersion}
</div>
)}
</div>

View File

@ -1,6 +1,6 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
export type GitRisks = {
export type IGitRisks = {
_id: string;
description: string;
startLine: string;
@ -41,7 +41,7 @@ export type GitRisks = {
* Will create a new integration session and return it for the given org
* @returns
*/
const getRisksByOrganization = (oranizationId: string): Promise<GitRisks[]> =>
const getRisksByOrganization = (oranizationId: string): Promise<IGitRisks[]> =>
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, {
method: "GET",
headers: {

View File

@ -18,4 +18,4 @@ export default function SettingsOrg() {
);
}
SettingsOrg.requireAuth = true;
SettingsOrg.requireAuth = true;

View File

@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { NonePage } from "@app/views/Org/NonePage";
export default function NoneOrganization() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<NonePage />
</>
);
}
NoneOrganization.requireAuth = true;

View File

@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import axios from "axios"
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
@ -11,6 +10,8 @@ import {
MFAStep,
SAMLSSOStep
} from "./components";
// import { navigateUserToOrg } from "../../Login.utils";
import { navigateUserToOrg } from "./Login.utils";
export const Login = () => {
const router = useRouter();
@ -24,10 +25,6 @@ export const Login = () => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
try {
const userOrgs = await fetchOrganizations();
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrg = userOrgs[0] && userOrgs[0]._id;
// user details
const userDetails = await fetchUserDetails()
// send details back to client
@ -40,7 +37,8 @@ export const Login = () => {
const instance = axios.create()
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
}
router.push(`/org/${userOrg}/overview`);
await navigateUserToOrg(router);
} catch (error) {
console.log("Error - Not logged in yet");
}

View File

@ -0,0 +1,18 @@
import { NextRouter } from "next/router";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
export const navigateUserToOrg = async (router: NextRouter) => {
const userOrgs = await fetchOrganizations();
if (userOrgs.length > 0) {
// user is part of at least 1 org
const userOrg = userOrgs[0] && userOrgs[0]._id;
localStorage.setItem("orgData.id", userOrg);
router.push(`/org/${userOrg}/overview`);
} else {
// user is not part of any org
localStorage.removeItem("orgData.id");
router.push("/org/none");
}
}

View File

@ -12,9 +12,10 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { navigateUserToOrg } from "../../Login.utils";
type Props = {
setStep: (step: number) => void;
email: string;
@ -73,6 +74,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
email: email.toLowerCase(),
password
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
@ -82,15 +84,14 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
setIsLoading(false);
return;
}
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
await navigateUserToOrg(router);
// case: login does not require MFA step
createNotification({
text: "Successfully logged in",
type: "success"
});
router.push(`/org/${userOrg}/overview`);
}
}
} catch (err) {

View File

@ -12,10 +12,10 @@ import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import { Button } from "@app/components/v2";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { useSendMfaToken } from "@app/hooks/api/auth";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { AuthMethod } from "@app/hooks/api/users/types";
import { navigateUserToOrg } from "../../Login.utils";
// The style for the verification code input
const props = {
@ -127,8 +127,6 @@ export const MFAStep = ({
if (isLoginSuccessful) {
setIsLoading(false);
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
createNotification({
@ -144,7 +142,7 @@ export const MFAStep = ({
});
}
router.push(`/org/${userOrg}/overview`);
await navigateUserToOrg(router);
} else {
createNotification({
text: "Failed to log in",

View File

@ -10,9 +10,10 @@ import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { navigateUserToOrg } from "../../Login.utils";
type Props = {
providerAuthToken: string;
email: string;
@ -92,8 +93,6 @@ export const PasswordStep = ({
}
// case: login does not require MFA step
const userOrgs = await fetchOrganizations();
const userOrg = userOrgs[0]._id;
setIsLoading(false);
createNotification({
text: "Successfully logged in",
@ -108,7 +107,7 @@ export const PasswordStep = ({
});
}
router.push(`/org/${userOrg}/overview`);
await navigateUserToOrg(router);
}
}
} catch (err) {
@ -120,8 +119,6 @@ export const PasswordStep = ({
console.error(err);
}
};
return (
<form

View File

@ -0,0 +1,114 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent} from "@app/components/v2";
import { useCreateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
const schema = yup.object({
name: yup.string().required("Organization name is required"),
}).required();
export type FormData = yup.InferType<typeof schema>;
export const NonePage = () => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"createOrg",
] as const);
const { mutateAsync } = useCreateOrg();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
handlePopUpOpen("createOrg");
}, []);
const onFormSubmit = async ({ name }: FormData) => {
try {
const organization = await mutateAsync({
name
});
localStorage.setItem("orgData.id", organization._id);
createNotification({
text: "Successfully created organization",
type: "success"
});
window.location.href = `/org/${organization._id}/overview`;
reset();
handlePopUpToggle("createOrg", false);
} catch (err) {
console.error(err);
createNotification({
text: "Failed to created organization",
type: "error"
});
}
}
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<Modal
isOpen={popUp?.createOrg?.isOpen}
>
<ModalContent
title="Create Organization"
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="Acme Corp"
/>
</FormControl>
)}
/>
<Button
className=""
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</form>
</ModalContent>
</Modal>
</div>
);
}

View File

@ -0,0 +1 @@
export { NonePage } from "./NonePage";

View File

@ -124,13 +124,15 @@ export const SecretListView = ({
comment,
tags,
skipMultilineEncoding,
newKey
newKey,
secretId
}: Partial<{
value: string;
comment: string;
tags: string[];
skipMultilineEncoding: boolean;
newKey: string;
secretId: string;
}> = {}
) => {
if (operation === "delete") {
@ -139,7 +141,8 @@ export const SecretListView = ({
workspaceId,
secretPath,
secretName: key,
type
type,
secretId
});
return;
}
@ -150,6 +153,7 @@ export const SecretListView = ({
workspaceId,
secretPath,
secretName: key,
secretId,
secretValue: value || "",
type,
latestFileKey: decryptFileKey,
@ -198,11 +202,14 @@ export const SecretListView = ({
try {
// personal secret change
if (overrideAction === "deleted") {
await handleSecretOperation("delete", "personal", oldKey);
await handleSecretOperation("delete", "personal", oldKey, {
secretId: orgSecret.idOverride
});
} else if (overrideAction && idOverride) {
await handleSecretOperation("update", "personal", oldKey, {
value: valueOverride,
newKey: hasKeyChanged ? key : undefined,
secretId: orgSecret.idOverride,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
} else if (overrideAction) {
@ -215,6 +222,7 @@ export const SecretListView = ({
value,
tags: tagIds,
comment,
secretId: orgSecret._id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
@ -249,9 +257,9 @@ export const SecretListView = ({
);
const handleSecretDelete = useCallback(async () => {
const { key } = popUp.deleteSecret?.data as DecryptedSecret;
const { key, _id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
try {
await handleSecretOperation("delete", "shared", key);
await handleSecretOperation("delete", "shared", key, { secretId });
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);
@ -305,7 +313,6 @@ export const SecretListView = ({
>
{namespace}
</div>
{filteredSecrets.map((secret) => (
<SecretItem
environment={environment}

View File

@ -31,6 +31,7 @@ import {
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useCreateFolder,
useCreateSecretV3,
useDeleteSecretV3,
useGetFoldersByEnv,
@ -104,9 +105,24 @@ export const SecretOverviewPage = () => {
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const handleSecretCreate = async (env: string, key: string, value: string) => {
try {
// create folder if not existing
if (secretPath !== "/") {
const path = secretPath.split("/");
const directory = path.slice(0, -1).join("/");
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
environment: env,
directory,
folderName
});
}
}
await createSecretV3({
environment: env,
workspaceId,
@ -130,12 +146,13 @@ export const SecretOverviewPage = () => {
}
};
const handleSecretUpdate = async (env: string, key: string, value: string) => {
const handleSecretUpdate = async (env: string, key: string, value: string, secretId?: string) => {
try {
await updateSecretV3({
environment: env,
workspaceId,
secretPath,
secretId,
secretName: key,
secretValue: value,
type: "shared",
@ -154,13 +171,14 @@ export const SecretOverviewPage = () => {
}
};
const handleSecretDelete = async (env: string, key: string) => {
const handleSecretDelete = async (env: string, key: string, secretId?: string) => {
try {
await deleteSecretV3({
environment: env,
workspaceId,
secretPath,
secretName: key,
secretId,
type: "shared"
});
createNotification({
@ -188,7 +206,20 @@ export const SecretOverviewPage = () => {
});
};
const handleExploreEnvClick = (slug: string) => {
const handleExploreEnvClick = async (slug: string) => {
if (secretPath !== "/") {
const path = secretPath.split("/");
const directory = path.slice(0, -1).join("/");
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
environment: slug,
directory,
folderName
});
}
}
const query: Record<string, string> = { ...router.query, env: slug };
const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
if (envIndex !== -1) {
@ -212,7 +243,6 @@ export const SecretOverviewPage = () => {
);
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
@ -335,7 +365,14 @@ export const SecretOverviewPage = () => {
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}}
>
<Button className="mt-4" variant="outline_bg" colorSchema="primary" size="md">Go to {userAvailableEnvs?.[0]?.name}</Button>
<Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button>
</Link>
</EmptyState>
</Td>

View File

@ -12,13 +12,14 @@ import { useToggle } from "@app/hooks";
type Props = {
defaultValue?: string | null;
secretName: string;
secretId?: string;
isCreatable?: boolean;
isVisible?: boolean;
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
export const SecretEditRow = ({
@ -30,7 +31,8 @@ export const SecretEditRow = ({
onSecretDelete,
environment,
secretPath,
isVisible
isVisible,
secretId
}: Props) => {
const {
handleSubmit,
@ -40,7 +42,7 @@ export const SecretEditRow = ({
formState: { isDirty, isSubmitting }
} = useForm({
values: {
value: defaultValue
value: defaultValue || null
}
});
const [isDeleting, setIsDeleting] = useToggle();
@ -68,7 +70,7 @@ export const SecretEditRow = ({
if (isCreatable) {
await onSecretCreate(environment, secretName, value);
} else {
await onSecretUpdate(environment, secretName, value);
await onSecretUpdate(environment, secretName, value, secretId);
}
}
reset({ value });
@ -77,8 +79,8 @@ export const SecretEditRow = ({
const handleDeleteSecret = async () => {
setIsDeleting.on();
try {
await onSecretDelete(environment, secretName);
reset({ value: undefined });
await onSecretDelete(environment, secretName, secretId);
reset({ value: null });
} finally {
setIsDeleting.off();
}

View File

@ -23,8 +23,8 @@ type Props = {
expandableColWidth: number;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
export const SecretOverviewTableRow = ({
@ -149,6 +149,7 @@ export const SecretOverviewTableRow = ({
isVisible={isSecretVisible}
secretName={secretKey}
defaultValue={secret?.value}
secretId={secret?._id}
isCreatable={isCreatable}
onSecretDelete={onSecretDelete}
onSecretCreate={onSecretCreate}

View File

@ -14,14 +14,14 @@ import {
} from "@app/components/v2";
import timeSince from "@app/ee/utilities/timeSince";
import getRisksByOrganization, {
GitRisks
IGitRisks
} from "@app/pages/api/secret-scanning/getRisksByOrganization";
import { RiskStatusSelection } from "./RiskStatusSelection";
export const SecretScanningLogsTable = () => {
const [isLoading, setIsLoading] = useState(false);
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
const [gitRisks, setGitRisks] = useState<IGitRisks[]>([]);
useEffect(() => {
const fetchRisks = async () => {

View File

@ -71,7 +71,7 @@ export const PreviewSection = () => {
console.error(err);
}
};
return (
<div>
{subscription &&

View File

@ -0,0 +1,81 @@
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useOrganization, useUser } from "@app/context";
import {
useDeleteOrgById,
useGetOrgUsers
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
export const OrgDeleteSection = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
const { user } = useUser();
const { createNotification } = useNotificationContext();
const { data: members } = useGetOrgUsers(currentOrg?._id ?? "");
const membershipOrg = members?.find((member) => member.user._id === user._id);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteOrg"
] as const);
const { mutateAsync, isLoading } = useDeleteOrgById();
const handleDeleteOrgSubmit = async () => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
organizationId: currentOrg?._id
});
createNotification({
text: "Successfully deleted organization",
type: "success"
});
await navigateUserToOrg(router);
handlePopUpClose("deleteOrg");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete organization",
type: "error"
});
}
}
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<Button
isLoading={isLoading}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteOrg")}
isDisabled={(membershipOrg && membershipOrg.role !== "admin")}
>
{`Delete ${currentOrg?.name}`}
</Button>
<DeleteActionModal
isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?"
subTitle={`Permanently remove ${currentOrg?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteOrg", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteOrgSubmit}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export { OrgDeleteSection } from "./OrgDeleteSection";

View File

@ -1,11 +1,24 @@
import { useOrganization, useUser } from "@app/context";
import { useGetOrgUsers } from "@app/hooks/api";
import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection";
export const OrgGeneralTab = () => {
const { currentOrg } = useOrganization();
const { user } = useUser();
const { data: members } = useGetOrgUsers(currentOrg?._id ?? "");
const membershipOrg = members?.find((member) => member.user._id === user._id);
return (
<div>
<OrgNameChangeSection />
<OrgIncidentContactsSection />
{(membershipOrg && membershipOrg.role === "admin") && (
<OrgDeleteSection />
)}
</div>
);
};

View File

@ -18,7 +18,7 @@ export const OrgIncidentContactsSection = () => {
const permission = useOrgPermission();
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>

View File

@ -53,7 +53,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""

View File

@ -0,0 +1,64 @@
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useDeleteUser } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteAccountSection = () => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteAccount"
] as const);
const { mutateAsync: deleteUserMutateAsync, isLoading } = useDeleteUser();
const handleDeleteAccountSubmit = async () => {
try {
await deleteUserMutateAsync();
createNotification({
text: "Successfully deleted account",
type: "success"
});
router.push("/login");
handlePopUpClose("deleteAccount");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete account",
type: "error"
});
}
}
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<Button
isLoading={isLoading}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteAccount")}
>
Delete my account
</Button>
<DeleteActionModal
isOpen={popUp.deleteAccount.isOpen}
title="Are you sure want to delete your account?"
subTitle="Permanently remove this account and all of its data. This action is not reversible, so please be careful."
onChange={(isOpen) => handlePopUpToggle("deleteAccount", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteAccountSubmit}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export { DeleteAccountSection } from "./DeleteAccountSection";

View File

@ -1,4 +1,5 @@
import { ChangeLanguageSection } from "../ChangeLanguageSection";
import { DeleteAccountSection } from "../DeleteAccountSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
@ -10,6 +11,7 @@ export const PersonalGeneralTab = () => {
<ChangeLanguageSection />
<SessionsSection />
<EmergencyKitSection />
<DeleteAccountSection />
</div>
);
}

View File

@ -1,10 +1,11 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import {
Button,
DeleteActionModal,
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@ -13,78 +14,74 @@ import {
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useDeleteWorkspace } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteProjectSection = () => {
const { t } = useTranslation();
const router = useRouter();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
const [isDeleting, setIsDeleting] = useToggle();
const [deleteProjectInput, setDeleteProjectInput] = useState("");
const deleteWorkspace = useDeleteWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteWorkspace"
] as const);
const onDeleteWorkspace = async () => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const [isDeleting, setIsDeleting] = useToggle();
const deleteWorkspace = useDeleteWorkspace();
const handleDeleteWorkspaceSubmit = async () => {
setIsDeleting.on();
try {
if (!currentWorkspace?._id) return;
await deleteWorkspace.mutateAsync({
workspaceID: currentWorkspace?._id
});
// redirect user to the org overview
router.push(`/org/${currentOrg?._id}/overview`);
createNotification({
text: "Successfully deleted workspace",
text: "Successfully deleted project",
type: "success"
});
} catch (error) {
console.error(error);
router.push(`/org/${currentOrg?._id}/overview`);
handlePopUpClose("deleteWorkspace");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete workspace",
text: "Failed to delete project",
type: "error"
});
} finally {
setIsDeleting.off();
}
};
}
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
<FormControl
label={
<div className="mb-0.5 text-sm font-normal text-gray-400">
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
workspace
</div>
}
>
<Input
onChange={(e) => setDeleteProjectInput(e.target.value)}
value={deleteProjectInput}
placeholder="Type the project name to delete"
className="bg-mineshaft-800"
/>
</FormControl>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
colorSchema="danger"
onClick={onDeleteWorkspace}
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
isLoading={isDeleting}
>
{t("settings.project.delete-project")}
</Button>
)}
</ProjectPermissionCan>
<p className="mt-3 ml-0.5 text-xs text-gray-500">
{t("settings.project.delete-project-note")}
</p>
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently remove ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
</div>
);
};

View File

@ -54,9 +54,7 @@ export const SecretTagsSection = (): JSX.Element => {
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
console.log("x");
handlePopUpOpen("CreateSecretTag");
console.log("x2");
}}
isDisabled={!isAllowed}
>

View File

@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.3.4
version: 0.3.5
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

Some files were not shown because too many files have changed in this diff Show More