Merge branch 'Infisical:main' into cloudflare-workers

This commit is contained in:
Shreyas
2023-11-22 10:41:01 -08:00
committed by GitHub
125 changed files with 6243 additions and 1696 deletions

3029
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.77.0",
"@sentry/tracing": "^7.48.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
@@ -41,13 +42,14 @@
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"ora": "^5.4.1",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"pg": "^8.11.3",
"pino": "^8.16.1",
"pino-http": "^8.5.1",
"pg": "^8.11.3",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3",
@@ -70,7 +72,7 @@
"main": "src/index.js",
"scripts": {
"start": "node build/index.js",
"dev": "nodemon index.js | pino-pretty --colorize",
"dev": "nodemon index.js",
"swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
"lint": "eslint . --ext .ts",
@@ -95,6 +97,8 @@
"devDependencies": {
"@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4",
"@swc/core": "^1.3.99",
"@swc/helpers": "^0.5.3",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^4.10.0",
@@ -125,6 +129,7 @@
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"pino-pretty": "^10.2.3",
"regenerator-runtime": "^0.14.0",
"smee-client": "^1.2.3",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",

43
backend/src/bootstrap.ts Normal file
View File

@@ -0,0 +1,43 @@
import ora from "ora";
import nodemailer from "nodemailer";
import { getSmtpHost, getSmtpPort } from "./config";
import { logger } from "./utils/logging";
import mongoose from "mongoose";
import { redisClient } from "./services/RedisService";
type BootstrapOpt = {
transporter: nodemailer.Transporter;
};
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
const spinner = ora().start();
spinner.info("Checking configurations...");
spinner.info("Testing smtp connection");
await transporter
.verify()
.then(async () => {
spinner.succeed("SMTP successfully connected");
})
.catch(async (err) => {
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
logger.error(err);
});
spinner.info("Testing mongodb connection");
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
spinner.fail("Mongo DB - Failed to connect");
} else {
spinner.succeed("Mongodb successfully connected");
}
spinner.info("Testing redis connection");
const redisPing = await redisClient?.ping();
if (!redisPing) {
spinner.fail("Redis - Failed to connect");
} else {
spinner.succeed("Redis successfully connected");
}
spinner.stop();
};

View File

@@ -3,99 +3,171 @@ import { GITLAB_URL } from "../variables";
import InfisicalClient from "infisical-node";
export const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!,
token: process.env.INFISICAL_TOKEN!
});
export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000;
export const getEncryptionKey = async () => {
const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
};
export const getRootEncryptionKey = async () => {
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"
export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
export const getAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue ?? (await client.getSecret("AUTH_SECRET")).secretValue;
export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
};
export const getInviteOnlySignup = async () =>
(await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true";
export const getSaltRounds = async () =>
parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
export const getAuthSecret = async () =>
(await client.getSecret("JWT_AUTH_SECRET")).secretValue ??
(await client.getSecret("AUTH_SECRET")).secretValue;
export const getJwtAuthLifetime = async () =>
(await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
export const getJwtMfaLifetime = async () =>
(await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
export const getJwtRefreshLifetime = async () =>
(await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
export const getJwtServiceSecret = async () =>
(await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
export const getJwtSignupLifetime = async () =>
(await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
export const getJwtProviderAuthLifetime = async () =>
(await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
export const getNodeEnv = async () =>
(await client.getSecret("NODE_ENV")).secretValue || "production";
export const getVerboseErrorOutput = async () =>
(await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue;
export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue;
export const getClientIdHeroku = async () => (await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientIdGCPSecretManager = async () => (await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
export const getClientSecretNetlify = async () => (await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSecretGCPSecretManager = async () => (await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdHeroku = async () =>
(await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
export const getClientIdVercel = async () =>
(await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
export const getClientIdNetlify = async () =>
(await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
export const getClientIdGitHub = async () =>
(await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
export const getClientIdGitLab = async () =>
(await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdBitBucket = async () =>
(await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientIdGCPSecretManager = async () =>
(await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
export const getClientSecretAzure = async () =>
(await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () =>
(await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () =>
(await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
export const getClientSecretNetlify = async () =>
(await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getClientSecretGitHub = async () =>
(await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () =>
(await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretBitBucket = async () =>
(await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSecretGCPSecretManager = async () =>
(await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
export const getClientSlugVercel = async () =>
(await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () => (await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () => (await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getUrlGitLabLogin = async () => (await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
export const getClientIdGoogleLogin = async () =>
(await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () =>
(await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () =>
(await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getUrlGitLabLogin = async () =>
(await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
export const getAwsCloudWatchLog = async () => {
const logGroupName =
(await client.getSecret("AWS_CLOUDWATCH_LOG_GROUP_NAME")).secretValue || "infisical-log-stream";
const region = (await client.getSecret("AWS_CLOUDWATCH_LOG_REGION")).secretValue;
const accessKeyId = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID")).secretValue;
const accessKeySecret = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET"))
.secretValue;
const interval = parseInt(
(await client.getSecret("AWS_CLOUDWATCH_LOG_INTERVAL")).secretValue || 1000,
10
);
if (!region || !accessKeyId || !accessKeySecret) return;
return { logGroupName, region, accessKeySecret, accessKeyId, interval };
};
export const getPostHogHost = async () =>
(await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () =>
(await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue ||
"phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue;
export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue;
export const getSmtpSecure = async () => (await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
export const getSmtpPort = async () => parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
export const getSmtpSecure = async () =>
(await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
export const getSmtpPort = async () =>
parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue;
export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue;
export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSmtpFromAddress = async () =>
(await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () =>
(await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSecretScanningWebhookProxy = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () => (await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () => (await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () => (await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getSecretScanningWebhookProxy = async () =>
(await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () =>
(await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () =>
(await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () =>
(await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
export const getIsInfisicalCloud = async () =>
(await client.getSecret("INFISICAL_CLOUD")).secretValue === "true";
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
};
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
}
export const getLicenseServerUrl = async () => (await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
};
export const getLicenseServerUrl = async () =>
(await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
export const getTelemetryEnabled = async () => (await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
export const getTelemetryEnabled = async () =>
(await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
export const getSmtpConfigured = async () => (await client.getSecret("SMTP_HOST")).secretValue == "" || (await client.getSecret("SMTP_HOST")).secretValue == undefined ? false : true
export const getSmtpConfigured = async () =>
(await client.getSecret("SMTP_HOST")).secretValue == "" ||
(await client.getSecret("SMTP_HOST")).secretValue == undefined
? false
: true;
export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") {
// no https for anything other than prod
return false
return false;
}
if ((await client.getSecret("HTTPS_ENABLED")).secretValue == undefined || (await client.getSecret("HTTPS_ENABLED")).secretValue == "") {
if (
(await client.getSecret("HTTPS_ENABLED")).secretValue == undefined ||
(await client.getSecret("HTTPS_ENABLED")).secretValue == ""
) {
// default when no value present
return true
return true;
}
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true
}
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true;
};

View File

@@ -0,0 +1,24 @@
import { IServerConfig, ServerConfig } from "../models/serverConfig";
let serverConfig: IServerConfig;
export const serverConfigInit = async () => {
const cfg = await ServerConfig.findOne({});
if (!cfg) {
const cfg = new ServerConfig();
await cfg.save();
serverConfig = cfg;
} else {
serverConfig = cfg;
}
return serverConfig;
};
export const getServerConfig = () => serverConfig;
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
if (!cfg) throw new Error("Failed to update server config");
serverConfig = cfg;
return serverConfig;
};

View File

@@ -0,0 +1,100 @@
import { Request, Response } from "express";
import { getHttpsEnabled } from "../../config";
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import { User } from "../../models";
import { TelemetryService } from "../../services";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/admin";
export const getServerConfigInfo = (_req: Request, res: Response) => {
const config = getServerConfig();
return res.send({ config });
};
export const updateServerConfig = async (req: Request, res: Response) => {
const {
body: { allowSignUp }
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
const config = await setServerConfig({ allowSignUp });
return res.send({ config });
};
export const adminSignUp = async (req: Request, res: Response) => {
const cfg = getServerConfig();
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.SignupV1, req);
let user = await User.findOne({ email });
if (user) throw BadRequestError({ message: "User already exist" });
user = new User({
email,
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
superAdmin: true
});
await user.save();
await initializeDefaultOrg({ organizationName: "Admin Org", user });
await setServerConfig({ initialized: true });
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const token = tokens.token;
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "admin initialization",
properties: {
email: user.email,
lastName,
firstName
}
});
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: "Successfully set up admin account",
user,
token
});
};

View File

@@ -7,7 +7,7 @@ import * as reqValidator from "../../validation/bot";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
@@ -28,7 +28,11 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -70,7 +74,11 @@ export const setBotActiveState = async (req: Request, res: Response) => {
}
const userId = req.user._id;
const { permission } = await getUserProjectPermissions(userId, bot.workspace.toString());
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: bot.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations

View File

@@ -16,6 +16,8 @@ import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as adminController from "./adminController";
export {
authController,
botController,
@@ -34,5 +36,6 @@ export {
workspaceController,
secretScanningController,
webhookController,
secretImpsController
secretImpsController,
adminController
};

View File

@@ -25,7 +25,7 @@ import * as reqValidator from "../../validation/integrationAuth";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { getIntegrationAuthAccessHelper } from "../../helpers";
@@ -40,15 +40,15 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth)
return res.status(400).send({
message: "Failed to find integration authorization"
});
if (!integrationAuth) return res.status(400).send({
message: "Failed to find integration authorization"
});
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -79,7 +79,11 @@ export const oAuthExchange = async (req: Request, res: Response) => {
} = await validateRequest(reqValidator.OauthExchangeV1, req);
if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration");
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
@@ -131,7 +135,11 @@ export const saveIntegrationToken = async (req: Request, res: Response) => {
body: { workspaceId, integration, url, accessId, namespace, accessToken, refreshToken }
} = await validateRequest(reqValidator.SaveIntegrationAccessTokenV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
@@ -224,11 +232,12 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
const { integrationAuth, accessToken, accessId } = await getIntegrationAuthAccessHelper({
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -263,10 +272,11 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -299,10 +309,11 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -360,10 +371,11 @@ export const getIntegrationAuthChecklyGroups = async (req: Request, res: Respons
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -413,10 +425,11 @@ export const getIntegrationAuthQoveryOrgs = async (req: Request, res: Response)
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -465,10 +478,11 @@ export const getIntegrationAuthQoveryProjects = async (req: Request, res: Respon
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -526,10 +540,11 @@ export const getIntegrationAuthQoveryEnvironments = async (req: Request, res: Re
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -587,10 +602,11 @@ export const getIntegrationAuthQoveryApps = async (req: Request, res: Response)
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -648,10 +664,11 @@ export const getIntegrationAuthQoveryContainers = async (req: Request, res: Resp
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -709,10 +726,11 @@ export const getIntegrationAuthQoveryJobs = async (req: Request, res: Response)
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -771,10 +789,11 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -864,10 +883,11 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -988,10 +1008,11 @@ export const getIntegrationAuthBitBucketWorkspaces = async (req: Request, res: R
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -1044,10 +1065,11 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -1132,10 +1154,11 @@ export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res:
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -1201,10 +1224,11 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => {
integrationAuthId: new Types.ObjectId(integrationAuthId)
});
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations

View File

@@ -13,7 +13,7 @@ import * as reqValidator from "../../validation/integration";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
@@ -51,11 +51,12 @@ export const createIntegration = async (req: Request, res: Response) => {
);
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integrationAuth.workspace._id.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integrationAuth.workspace._id
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
@@ -164,10 +165,11 @@ export const updateIntegration = async (req: Request, res: Response) => {
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integration.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integration.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
@@ -234,10 +236,11 @@ export const deleteIntegration = async (req: Request, res: Response) => {
const integration = await Integration.findById(integrationId);
if (!integration) throw BadRequestError({ message: "Integration not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
integration.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: integration.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Integrations
@@ -285,7 +288,11 @@ export const manualSync = async (req: Request, res: Response) => {
body: { workspaceId, environment }
} = await validateRequest(reqValidator.ManualSyncV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations

View File

@@ -9,7 +9,7 @@ import * as reqValidator from "../../validation/key";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
@@ -26,7 +26,11 @@ export const uploadKey = async (req: Request, res: Response) => {
body: { key }
} = await validateRequest(reqValidator.UploadKeyV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member

View File

@@ -12,7 +12,7 @@ import * as reqValidator from "../../validation/membership";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
@@ -63,11 +63,12 @@ export const deleteMembership = async (req: Request, res: Response) => {
if (!membershipToDelete) {
throw new Error("Failed to delete workspace membership that doesn't exist");
}
const { permission } = await getUserProjectPermissions(
req.user._id,
membershipToDelete.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: membershipToDelete.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
@@ -118,10 +119,11 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
throw new Error("Failed to find membership to change role");
}
const { permission } = await getUserProjectPermissions(
req.user._id,
membershipToChangeRole.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: membershipToChangeRole.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
@@ -191,7 +193,12 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
params: { workspaceId },
body: { email }
} = await validateRequest(InviteUserToWorkspaceV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member

View File

@@ -1,4 +1,6 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService";
@@ -15,7 +17,7 @@ import * as reqValidator from "../../validation/secretImports";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
@@ -105,7 +107,11 @@ export const createSecretImp = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
@@ -313,10 +319,11 @@ export const updateSecretImport = async (req: Request, res: Response) => {
}
} else {
// non token entry check
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
@@ -442,10 +449,11 @@ export const deleteSecretImport = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
@@ -550,7 +558,11 @@ export const getSecretImports = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
@@ -604,7 +616,11 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
@@ -657,10 +673,11 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {

View File

@@ -17,7 +17,7 @@ import {
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders";
@@ -125,7 +125,11 @@ export const createFolder = async (req: Request, res: Response) => {
}
} else {
// user check
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
@@ -332,7 +336,11 @@ export const updateFolderById = async (req: Request, res: Response) => {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
@@ -502,7 +510,11 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
} else {
// check that user is a member of the workspace
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
@@ -649,7 +661,10 @@ export const getFolders = async (req: Request, res: Response) => {
}
} else {
// check that user is a member of the workspace
await getUserProjectPermissions(req.user._id, workspaceId);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });

View File

@@ -2,10 +2,8 @@ import { Request, Response } from "express";
import { AuthMethod, User } from "../../models";
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
import { createToken } from "../../helpers/auth";
import { BadRequestError } from "../../utils/errors";
import {
getAuthSecret,
getInviteOnlySignup,
getJwtSignupLifetime,
getSmtpConfigured
} from "../../config";
@@ -68,16 +66,6 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
});
}
if (await getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({});
if (userCount != 0) {
throw BadRequestError({
message: "New user sign ups are not allowed at this time. You must be invited to sign up."
});
}
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({

View File

@@ -16,7 +16,7 @@ import * as reqValidator from "../../validation/webhooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
@@ -26,7 +26,11 @@ export const createWebhook = async (req: Request, res: Response) => {
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.CreateWebhookV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
@@ -98,11 +102,11 @@ export const updateWebhook = async (req: Request, res: Response) => {
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
@@ -146,10 +150,11 @@ export const deleteWebhook = async (req: Request, res: Response) => {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
@@ -193,10 +198,11 @@ export const testWebhook = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
webhook.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
@@ -236,8 +242,12 @@ export const listWebhooks = async (req: Request, res: Response) => {
const {
query: { environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.ListWebhooksV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks

View File

@@ -25,7 +25,7 @@ import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
/**
@@ -39,7 +39,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
@@ -72,7 +76,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
@@ -195,7 +203,11 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Workspace
@@ -223,7 +235,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
body: { name }
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Workspace
@@ -257,7 +273,12 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -283,7 +304,11 @@ export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: R
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Integrations
@@ -309,7 +334,11 @@ export const getWorkspaceServiceTokens = async (req: Request, res: Response) =>
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens

View File

@@ -12,14 +12,12 @@ import {
import { EventType, SecretVersion } from "../../ee/models";
import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
import _ from "lodash";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/environments";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
@@ -114,7 +112,11 @@ export const createWorkspaceEnvironment = async (req: Request, res: Response) =>
body: { environmentName, environmentSlug }
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Environments
@@ -191,7 +193,11 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response)
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
@@ -322,7 +328,11 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
body: { environmentName, environmentSlug, oldEnvironmentSlug }
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
@@ -511,7 +521,11 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
body: { environmentSlug }
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Environments
@@ -587,98 +601,4 @@ export const deleteWorkspaceEnvironment = async (req: Request, res: Response) =>
workspace: workspaceId,
environment: environmentSlug
});
};
// TODO(akhilmhdh) after rbac this can be completely removed
export const getAllAccessibleEnvironmentsOfWorkspace = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get all accessible environments of a workspace'
#swagger.description = 'Fetch all environments that the user has access to in a specified workspace'
#swagger.security = [{
"apiKeyAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace",
"required": true,
"type": "string",
"in": "path"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"accessibleEnvironments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Development"
},
"slug": {
"type": "string",
"example": "development"
},
"isWriteDenied": {
"type": "boolean",
"example": false
},
"isReadDenied": {
"type": "boolean",
"example": false
}
}
}
}
},
"description": "List of environments the user has access to in the specified workspace"
}
}
}
}
*/
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetAllAccessibileEnvironmentsOfWorkspaceV2, req);
const { membership: workspacesUserIsMemberOf } = await getUserProjectPermissions(
req.user._id,
workspaceId
);
const accessibleEnvironments: any = [];
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions;
const relatedWorkspace = await Workspace.findById(workspaceId);
if (!relatedWorkspace) {
throw BadRequestError();
}
relatedWorkspace.environments.forEach((environment) => {
const isReadBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_READ_SECRETS
});
const isWriteBlocked = _.some(deniedPermission, {
environmentSlug: environment.slug,
ability: PERMISSION_WRITE_SECRETS
});
if (isReadBlocked && isWriteBlocked) {
return;
} else {
accessibleEnvironments.push({
name: environment.name,
slug: environment.slug,
isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked
});
}
});
res.json({ accessibleEnvironments });
};
};

View File

@@ -8,7 +8,7 @@ import { EEAuditLogService } from "../../ee/services";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { sendMail } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
@@ -27,7 +27,11 @@ export const addUserToWorkspace = async (req: Request, res: Response) => {
if (!workspace) throw new Error("Failed to find workspace");
// check permission
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member

View File

@@ -1,24 +1,16 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
Membership,
MembershipOrg,
Workspace
} from "../../models";
import { Membership, MembershipOrg, Workspace } from "../../models";
import { Role } from "../../ee/models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import {
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 { ACCEPTED, ADMIN, CUSTOM } from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
@@ -155,7 +147,7 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
OrgPermissionSubjects.Member
);
const isCustomRole = !["admin", "member", "owner"].includes(role);
const isCustomRole = !["admin", "member"].includes(role);
if (isCustomRole) {
const orgRole = await Role.findOne({ slug: role, isOrgRole: true });
if (!orgRole) throw BadRequestError({ message: "Role not found" });
@@ -336,7 +328,7 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { name }
} = await validateRequest(reqValidator.CreateOrgv2, req);
@@ -361,27 +353,27 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
/**
* Delete organization with id [organizationId]
* @param req
* @param res
* @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

@@ -39,7 +39,7 @@ import {
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
@@ -159,7 +159,11 @@ export const batchSecrets = async (req: Request, res: Response) => {
}
// not using service token using auth
if (!(req.authData.authPayload instanceof ServiceTokenData)) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (createSecrets.length)
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,

View File

@@ -11,7 +11,7 @@ import * as reqValidator from "../../validation/serviceTokenData";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { Types } from "mongoose";
@@ -75,7 +75,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
const {
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
@@ -151,10 +156,11 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
const { permission } = await getUserProjectPermissions(
req.user._id,
serviceTokenData.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens

View File

@@ -7,7 +7,7 @@ import { validateRequest } from "../../helpers/validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import * as reqValidator from "../../validation/tags";
@@ -17,7 +17,11 @@ export const createWorkspaceTag = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Tags
@@ -45,10 +49,11 @@ export const deleteWorkspaceTag = async (req: Request, res: Response) => {
throw BadRequestError();
}
const { permission } = await getUserProjectPermissions(
req.user._id,
tagFromDB.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: tagFromDB.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Tags
@@ -66,7 +71,12 @@ export const getWorkspaceTags = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Tags

View File

@@ -16,7 +16,7 @@ import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
@@ -272,7 +272,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
@@ -352,7 +356,11 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
body: { role }
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
@@ -420,7 +428,11 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
params: { workspaceId, membershipId }
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
@@ -452,7 +464,11 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
body: { autoCapitalization }
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Settings

View File

@@ -3,11 +3,16 @@ import { Types } from "mongoose";
import { EventService, SecretService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { containsGlobPatterns, isValidScopeV3, repackageSecretToRaw } from "../../helpers/secrets";
import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { Folder, IMembership, IServiceTokenData, IServiceTokenDataV3 } from "../../models";
import { Permission } from "../../models/serviceTokenDataV3";
import {
Folder,
IServiceTokenData,
Membership,
ServiceTokenData,
User
} from "../../models";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
@@ -15,13 +20,10 @@ import * as reqValidator from "../../validation/secrets";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
import {
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataV3ClientForWorkspace
} from "../../validation";
import { validateServiceTokenDataClientForWorkspace } from "../../validation";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
import { ActorType } from "../../ee/models";
import { UnauthorizedRequestError } from "../../utils/errors";
@@ -31,7 +33,7 @@ import {
getSecretPolicyOfBoard
} from "../../ee/services/SecretApprovalService";
import { CommitType } from "../../ee/models/secretApprovalRequest";
import { IRole } from "../../ee/models/role";
import { logger } from "../../utils/logging";
const checkSecretsPermission = async ({
authData,
@@ -47,36 +49,31 @@ const checkSecretsPermission = async ({
secretAction: ProjectPermissionActions; // CRUD
}): Promise<{
authVerifier: (env: string, secPath: string) => boolean;
membership?: Omit<IMembership, "customRole"> & { customRole: IRole };
}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
switch (secretAction) {
case ProjectPermissionActions.Create:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Read:
STV2RequiredPermissions = [PERMISSION_READ_SECRETS];
STV3RequiredPermissions = [Permission.READ];
break;
case ProjectPermissionActions.Edit:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
case ProjectPermissionActions.Delete:
STV2RequiredPermissions = [PERMISSION_WRITE_SECRETS];
STV3RequiredPermissions = [Permission.WRITE];
break;
}
switch (authData.actor.type) {
case ActorType.USER: {
const { permission, membership } = await getUserProjectPermissions(
authData.actor.metadata.userId,
workspaceId
);
const { permission } = await getAuthDataProjectPermissions({
authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
@@ -89,8 +86,7 @@ const checkSecretsPermission = async ({
environment: env,
secretPath: secPath
})
),
membership
)
};
}
case ActorType.SERVICE: {
@@ -104,22 +100,24 @@ const checkSecretsPermission = async ({
return { authVerifier: () => true };
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
const { permission } = await getAuthDataProjectPermissions({
authData,
serviceTokenData: authData.authPayload as IServiceTokenDataV3,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: STV3RequiredPermissions
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return {
authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
})
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
)
};
}
default: {
@@ -199,17 +197,22 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
query: { include_imports: includeImports }
} = validatedData;
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (
serviceTokenDetails &&
serviceTokenDetails.scopes.length == 1 &&
!containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)
) {
const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath;
environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString();
logger.info(`getSecretsRaw: fetch raw secrets [environment=${environment}] [workspaceId=${workspaceId}] [secretPath=${secretPath}] [includeImports=${includeImports}]`)
if (req.authData.authPayload instanceof ServiceTokenData) {
// if the service token has single scope, it will get all secrets for that scope by default
const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData;
if (
serviceTokenDetails &&
serviceTokenDetails.scopes.length == 1 &&
!containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)
) {
const scope = serviceTokenDetails.scopes[0];
secretPath = scope.secretPath;
environment = scope.environment;
workspaceId = serviceTokenDetails.workspace.toString();
}
}
if (!environment || !workspaceId)
@@ -353,6 +356,8 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
logger.info(`getSecretByNameRaw: fetch raw secret by name [environment=${environment}] [workspaceId=${workspaceId}] [secretPath=${secretPath}] [type=${type}] [include_imports=${include_imports}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -476,6 +481,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
logger.info(`createSecretRaw: create a secret raw by name and value [environment=${environment}] [workspaceId=${workspaceId}] [secretPath=${secretPath}] [type=${type}] [skipMultilineEncoding=${skipMultilineEncoding}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -621,6 +628,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
body: { workspaceId, environment, secretValue, secretPath, type, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
logger.info(`updateSecretByNameRaw: update raw secret by name [environment=${environment}] [workspaceId=${workspaceId}] [secretPath=${secretPath}] [type=${type}] [skipMultilineEncoding=${skipMultilineEncoding}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -743,6 +752,8 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
body: { environment, secretPath, type, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameRawV3, req);
logger.info(`deleteSecretByNameRaw: delete a secret by name [environment=${environment}] [workspaceId=${workspaceId}] [secretPath=${secretPath}] [type=${type}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -796,6 +807,8 @@ export const getSecrets = async (req: Request, res: Response) => {
query: { secretPath }
} = validatedData;
logger.info(`getSecrets: fetch encrypted secrets [environment=${environment}] [workspaceId=${workspaceId}] [includeImports=${includeImports}]`)
const { authVerifier: permissionCheckFn } = await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -850,6 +863,8 @@ export const getSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
logger.info(`getSecretByName: get a single secret by name [environment=${environment}] [workspaceId=${workspaceId}] [include_imports=${include_imports}] [type=${type}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
@@ -900,7 +915,9 @@ export const createSecret = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
const { membership } = await checkSecretsPermission({
logger.info(`createSecret: create an encrypted secret [environment=${environment}] [workspaceId=${workspaceId}] [skipMultilineEncoding=${skipMultilineEncoding}] [type=${type}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -908,35 +925,42 @@ export const createSecret = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Create
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.CREATE]: [
{
secretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.CREATE]: [
{
secretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
}
}
@@ -1005,11 +1029,13 @@ export const updateSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
logger.info(`updateSecretByName: update a encrypted secret by name [environment=${environment}] [workspaceId=${workspaceId}] [skipMultilineEncoding=${skipMultilineEncoding}] [type=${type}]`)
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
throw BadRequestError({ message: "Missing encrypted key" });
}
const { membership } = await checkSecretsPermission({
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -1017,37 +1043,44 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Edit
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.UPDATE]: [
{
secretName,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: {
[CommitType.UPDATE]: [
{
secretName,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
}
}
@@ -1097,7 +1130,9 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
const { membership } = await checkSecretsPermission({
logger.info(`deleteSecretByName: delete a encrypted secret by name [environment=${environment}] [workspaceId=${workspaceId}] [type=${type}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -1105,25 +1140,32 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Delete
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: [
{
secretName
}
]
}
});
return res.send({ approval: secretApprovalRequest });
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: [
{
secretName
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
}
}
@@ -1155,7 +1197,9 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
logger.info(`createSecretByNameBatch: create a list of secrets by their names [environment=${environment}] [workspaceId=${workspaceId}] [secretsLength=${secrets?.length}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -1163,21 +1207,28 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Create
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
authData: req.authData,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
}
}
}
@@ -1207,7 +1258,9 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
logger.info(`updateSecretByNameBatch: update a list of secrets by their names [environment=${environment}] [workspaceId=${workspaceId}] [secretsLength=${secrets?.length}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -1215,21 +1268,28 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Edit
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
}
}
}
@@ -1259,7 +1319,9 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
const { membership } = await checkSecretsPermission({
logger.info(`deleteSecretByNameBatch: delete a list of secrets by their names [environment=${environment}] [workspaceId=${workspaceId}] [secretsLength=${secrets?.length}]`)
await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -1267,21 +1329,28 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Delete
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
},
authData: req.authData
});
return res.send({ approval: secretApprovalRequest });
}
}
}

View File

@@ -1,9 +1,9 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenDataV3 } from "../../models";
import { Membership, Secret, ServiceTokenDataV3, User } from "../../models";
import { SecretService } from "../../services";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/workspace";
@@ -19,9 +19,22 @@ export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response)
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
@@ -41,9 +54,22 @@ export const getWorkspaceSecrets = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
const secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId)
@@ -65,9 +91,22 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
body: { secretsToUpdate }
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
@@ -109,7 +148,7 @@ export const getWorkspaceServiceTokenData = async (req: Request, res: Response)
const serviceTokenData = await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId)
});
}).populate("customRole");
return res.status(200).send({
serviceTokenData

View File

@@ -1,4 +1,6 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Membership, User } from "../../../models";
import {
CreateRoleSchema,
DeleteRoleSchema,
@@ -11,7 +13,7 @@ import {
ProjectPermissionActions,
ProjectPermissionSub,
adminProjectPermissions,
getUserProjectPermissions,
getAuthDataProjectPermissions,
memberProjectPermissions,
viewerProjectPermission
} from "../../services/ProjectRoleService";
@@ -39,7 +41,10 @@ export const createRole = async (req: Request, res: Response) => {
throw BadRequestError({ message: "user doesn't have the permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." });
}
@@ -82,7 +87,11 @@ export const updateRole = async (req: Request, res: Response) => {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
@@ -134,7 +143,11 @@ export const deleteRole = async (req: Request, res: Response) => {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, role.workspace.toString());
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: role.workspace
});
if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
@@ -162,7 +175,11 @@ export const getRoles = async (req: Request, res: Response) => {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
@@ -227,7 +244,19 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) =
const {
params: { workspaceId }
} = await validateRequest(GetUserProjectPermission, req);
const { permission, membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
let membership;
if (req.authData.authPayload instanceof User) {
membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
})
}
res.status(200).json({
data: {

View File

@@ -1,10 +1,11 @@
import { Types } from "mongoose";
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { nanoid } from "nanoid";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
@@ -19,7 +20,11 @@ export const createSecretApprovalPolicy = async (req: Request, res: Response) =>
body: { approvals, secretPath, approvers, environment, workspaceId, name }
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
@@ -49,10 +54,11 @@ export const updateSecretApprovalPolicy = async (req: Request, res: Response) =>
const secretApproval = await SecretApprovalPolicy.findById(id);
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
const { permission } = await getUserProjectPermissions(
req.user._id,
secretApproval.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: secretApproval.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretApproval
@@ -78,10 +84,11 @@ export const deleteSecretApprovalPolicy = async (req: Request, res: Response) =>
const secretApproval = await SecretApprovalPolicy.findById(id);
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
const { permission } = await getUserProjectPermissions(
req.user._id,
secretApproval.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: secretApproval.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
@@ -99,7 +106,11 @@ export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
query: { workspaceId }
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretApproval
@@ -117,7 +128,11 @@ export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response
query: { workspaceId, environment, secretPath }
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })

View File

@@ -1,7 +1,6 @@
import { Request, Response } from "express";
import { getUserProjectPermissions } from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
import { Folder } from "../../../models";
import { Folder, Membership, User } from "../../../models";
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
import * as reqValidator from "../../validation/secretApprovalRequest";
import { getFolderWithPathFromId } from "../../../services/FolderService";
@@ -17,7 +16,15 @@ export const getSecretApprovalRequestCount = async (req: Request, res: Response)
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
const approvalRequestCount = await SecretApprovalRequest.aggregate([
{
$match: {
@@ -65,7 +72,14 @@ export const getSecretApprovalRequests = async (req: Request, res: Response) =>
query: { status, committer, workspaceId, environment, limit, offset }
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
const query = {
workspace: new Types.ObjectId(workspaceId),
@@ -148,10 +162,15 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
// allow to fetch only if its admin or is the committer or approver
if (
membership.role !== "admin" &&
@@ -190,10 +209,15 @@ export const updateSecretApprovalReviewStatus = async (req: Request, res: Respon
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
@@ -227,10 +251,15 @@ export const mergeSecretApprovalRequest = async (req: Request, res: Response) =>
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
@@ -272,10 +301,14 @@ export const updateSecretApprovalRequestStatus = async (req: Request, res: Respo
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
if (
membership.role !== "admin" &&

View File

@@ -5,7 +5,7 @@ import { Folder, Secret } from "../../../models";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
@@ -74,7 +74,11 @@ export const getSecretVersions = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Failed to find secret" });
}
const { permission } = await getUserProjectPermissions(req.user._id, secret.workspace.toString());
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: secret.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
@@ -157,10 +161,12 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
if (!toBeUpdatedSec) {
throw BadRequestError({ message: "Failed to find secret" });
}
const { permission } = await getUserProjectPermissions(
req.user._id,
toBeUpdatedSec.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: toBeUpdatedSec.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback

View File

@@ -1,11 +1,12 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../validation/secretRotation";
import * as secretRotationService from "../../secretRotation/service";
import {
getUserProjectPermissions,
ProjectPermissionActions,
ProjectPermissionSub
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
@@ -23,7 +24,11 @@ export const createSecretRotation = async (req: Request, res: Response) => {
}
} = await validateRequest(reqValidator.createSecretRotationV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
@@ -49,7 +54,12 @@ export const restartSecretRotations = async (req: Request, res: Response) => {
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
const doc = await secretRotationService.getSecretRotationById({ id });
const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString());
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: doc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretRotation
@@ -65,7 +75,12 @@ export const deleteSecretRotations = async (req: Request, res: Response) => {
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
const doc = await secretRotationService.getSecretRotationById({ id });
const { permission } = await getUserProjectPermissions(req.user._id, doc.workspace.toString());
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: doc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation
@@ -80,7 +95,11 @@ export const getSecretRotations = async (req: Request, res: Response) => {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretRotationV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation

View File

@@ -1,11 +1,12 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../validation/secretRotationProvider";
import * as secretRotationProviderService from "../../secretRotation/service";
import {
getUserProjectPermissions,
ProjectPermissionActions,
ProjectPermissionSub
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
@@ -14,7 +15,11 @@ export const getProviderTemplates = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation

View File

@@ -4,7 +4,7 @@ import { validateRequest } from "../../../helpers/validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import * as reqValidator from "../../../validation/secretSnapshot";
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
@@ -33,10 +33,11 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
const { permission } = await getUserProjectPermissions(
req.user._id,
secretSnapshot.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: secretSnapshot.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback

View File

@@ -27,7 +27,6 @@ import {
} from "../../models";
import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
// import Folder, { TFolderSchema } from "../../../models/folder";
import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
@@ -46,7 +45,7 @@ import {
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../../utils/errors";
@@ -107,7 +106,11 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
query: { environment, directory, offset, limit }
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
@@ -148,7 +151,11 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
query: { environment, directory }
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
@@ -238,7 +245,11 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
body: { directory, environment, version }
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback
@@ -572,7 +583,11 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(GetWorkspaceAuditLogsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs
@@ -632,7 +647,11 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res
params: { workspaceId }
} = await validateRequest(GetWorkspaceAuditLogActorFilterOptsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.AuditLogs
@@ -700,7 +719,11 @@ export const getWorkspaceTrustedIps = async (req: Request, res: Response) => {
params: { workspaceId }
} = await validateRequest(GetWorkspaceTrustedIpsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.IpAllowList
@@ -726,7 +749,11 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => {
body: { comment, isActive, ipAddress: ip }
} = await validateRequest(AddWorkspaceTrustedIpV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.IpAllowList
@@ -792,7 +819,11 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => {
body: { ipAddress: ip, comment }
} = await validateRequest(UpdateWorkspaceTrustedIpV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.IpAllowList
@@ -884,7 +915,11 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => {
params: { workspaceId, trustedIpId }
} = await validateRequest(DeleteWorkspaceTrustedIpV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.IpAllowList

View File

@@ -8,13 +8,11 @@ import {
ServiceTokenDataV3Key,
Workspace
} from "../../../models";
import {
IServiceTokenV3Scope,
IServiceTokenV3TrustedIp
} from "../../../models/serviceTokenDataV3";
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
import {
ActorType,
EventType
EventType,
Role
} from "../../models";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/serviceTokenDataV3";
@@ -22,14 +20,14 @@ import { createToken } from "../../../helpers/auth";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getUserProjectPermissions
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, VIEWER } from "../../../variables";
/**
* Return project key for service token V3
@@ -163,7 +161,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
name,
workspaceId,
publicKey,
scopes,
role,
trustedIps,
expiresIn,
accessTokenTTL,
@@ -172,7 +170,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
nonce, // for ServiceTokenDataV3Key
}
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
@@ -181,6 +183,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
let customRole;
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: workspace._id
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
@@ -219,7 +234,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
accessTokenUsageCount: 0,
tokenVersion: 1,
trustedIps: reformattedTrustedIps,
scopes,
role: isCustomRole ? CUSTOM : role,
customRole,
isActive,
expiresAt,
accessTokenTTL,
@@ -250,7 +266,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
metadata: {
name,
isActive,
scopes: scopes as Array<IServiceTokenV3Scope>,
role,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
@@ -278,7 +294,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
body: {
name,
isActive,
scopes,
role,
trustedIps,
expiresIn,
accessTokenTTL,
@@ -291,10 +307,10 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
message: "Service token not found"
});
const { permission } = await getUserProjectPermissions(
req.user._id,
serviceTokenData.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
@@ -304,6 +320,20 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
const workspace = await Workspace.findById(serviceTokenData.workspace);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: workspace._id
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
}
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
@@ -335,7 +365,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
{
name,
isActive,
scopes,
role: customRole ? CUSTOM : role,
...(customRole ? {
customRole
} : {}),
...(role && !customRole ? { // non-custom role
$unset: {
customRole: 1
}
} : {}),
trustedIps: reformattedTrustedIps,
expiresAt,
accessTokenTTL,
@@ -357,7 +395,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
metadata: {
name: serviceTokenData.name,
isActive,
scopes: scopes as Array<IServiceTokenV3Scope>,
role,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
@@ -388,10 +426,10 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
message: "Service token not found"
});
const { permission } = await getUserProjectPermissions(
req.user._id,
serviceTokenData.workspace.toString()
);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
@@ -415,7 +453,7 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
metadata: {
name: serviceTokenData.name,
isActive: serviceTokenData.isActive,
scopes: serviceTokenData.scopes as Array<IServiceTokenV3Scope>,
role: serviceTokenData.role,
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt: serviceTokenData.expiresAt
}

View File

@@ -2,7 +2,7 @@ export enum ActorType {
USER = "user",
SERVICE = "service",
SERVICE_V3 = "service-v3",
Machine = "machine"
// Machine = "machine"
}
export enum UserAgentType {

View File

@@ -1,5 +1,5 @@
import { ActorType, EventType } from "./enums";
import { IServiceTokenV3Scope, IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
interface UserActorMetadata {
userId: string;
@@ -26,11 +26,11 @@ export interface ServiceActorV3 {
metadata: ServiceActorMetadata;
}
export interface MachineActor {
type: ActorType.Machine;
}
// export interface MachineActor {
// type: ActorType.Machine;
// }
export type Actor = UserActor | ServiceActor | ServiceActorV3 | MachineActor;
export type Actor = UserActor | ServiceActor | ServiceActorV3;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@@ -225,7 +225,7 @@ interface CreateServiceTokenV3Event {
metadata: {
name: string;
isActive: boolean;
scopes: Array<IServiceTokenV3Scope>;
role: string;
trustedIps: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date;
};
@@ -236,7 +236,7 @@ interface UpdateServiceTokenV3Event {
metadata: {
name?: string;
isActive?: boolean;
scopes?: Array<IServiceTokenV3Scope>;
role?: string;
trustedIps?: Array<IServiceTokenV3TrustedIp>;
expiresAt?: Date;
};
@@ -247,7 +247,7 @@ interface DeleteServiceTokenV3Event {
metadata: {
name: string;
isActive: boolean;
scopes: Array<IServiceTokenV3Scope>;
role: string;
expiresAt?: Date;
trustedIps: Array<IServiceTokenV3TrustedIp>;
};

View File

@@ -1,3 +1,4 @@
import { Types } from "mongoose";
import {
AbilityBuilder,
ForcedSubject,
@@ -6,11 +7,14 @@ import {
buildMongoQueryMatcher,
createMongoAbility
} from "@casl/ability";
import { Membership } from "../../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { UnauthorizedRequestError } from "../../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { AuthData } from "../../interfaces/middleware";
import { ActorType, IRole } from "../models";
import { Membership, ServiceTokenData, ServiceTokenDataV3 } from "../../models";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { checkIPAgainstBlocklist } from "../../utils/ip";
const $glob: FieldInstruction<string> = {
type: "field",
@@ -239,31 +243,89 @@ const buildViewerPermission = () => {
export const viewerProjectPermission = buildViewerPermission();
export const getUserProjectPermissions = async (userId: string, workspaceId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole")
.exec();
/**
* Return permissions for user/service pertaining to workspace with id [workspaceId]
*
* Note: should not rely on this function for ST V2 authorization logic
* b/c ST V2 does not support role-based access control
*/
export const getAuthDataProjectPermissions = async ({
authData,
workspaceId
}: {
authData: AuthData;
workspaceId: Types.ObjectId;
}) => {
let role: "admin" | "member" | "viewer" | "custom";
let customRole;
switch (authData.actor.type) {
case ActorType.USER: {
const membership = await Membership.findOne({
user: authData.authPayload._id,
workspace: workspaceId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole")
.exec();
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError();
}
role = membership.role;
customRole = membership.customRole;
break;
}
case ActorType.SERVICE: {
const serviceTokenData = await ServiceTokenData.findById(authData.authPayload._id);
if (!serviceTokenData || !serviceTokenData.workspace.equals(workspaceId)) throw UnauthorizedRequestError();
role = "viewer";
break;
}
case ActorType.SERVICE_V3: {
const serviceTokenData = await ServiceTokenDataV3
.findById(authData.authPayload._id)
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole")
.exec();
if (!serviceTokenData || (serviceTokenData.role === "custom" && !serviceTokenData.customRole)) {
throw UnauthorizedRequestError();
}
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
checkIPAgainstBlocklist({
ipAddress: authData.ipAddress,
trustedIps: serviceTokenData.trustedIps
});
role = serviceTokenData.role;
customRole = serviceTokenData.customRole;
break;
}
default:
throw UnauthorizedRequestError();
}
if (membership.role === "admin") return { permission: adminProjectPermissions, membership };
if (membership.role === "member") return { permission: memberProjectPermissions, membership };
if (membership.role === "viewer") return { permission: viewerProjectPermission, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
switch (role) {
case ADMIN:
return { permission: adminProjectPermissions };
case MEMBER:
return { permission: memberProjectPermissions };
case VIEWER:
return { permission: viewerProjectPermission };
case CUSTOM: {
if (!customRole) throw UnauthorizedRequestError();
return {
permission: createMongoAbility<ProjectPermissionSet>(
customRole.permissions,
{ conditionsMatcher }
)
};
}
default:
throw UnauthorizedRequestError();
}
throw BadRequestError({ message: "User role not found" });
};
}

View File

@@ -13,13 +13,11 @@ import {
Folder,
ISecret,
IServiceTokenData,
IServiceTokenDataV3,
Secret,
SecretBlindIndexData,
ServiceTokenData,
TFolderRootSchema
} from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { EventType, SecretVersion } from "../ee/models";
import {
BadRequestError,
@@ -51,42 +49,6 @@ import picomatch from "picomatch";
import path from "path";
import { getAnImportedSecret } from "../services/SecretImportService";
/**
* Validate scope for service token v3
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScopeV3 = ({
authPayload,
environment,
secretPath,
requiredPermissions
}: {
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))
) {
return false;
}
return Boolean(validScope);
};
/**
* Validate scope for service token v2
* @param authPayload

View File

@@ -5,7 +5,7 @@ import express from "express";
require("express-async-errors");
import helmet from "helmet";
import cors from "cors";
import { logger } from "./utils/logging";
import { initLogger, logger } from "./utils/logging";
import httpLogger from "pino-http";
import { DatabaseService } from "./services";
import { EELicenseService, GithubSecretScanningService } from "./ee/services";
@@ -35,6 +35,7 @@ import {
import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import {
admin as v1AdminRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
integrationAuth as v1IntegrationAuthRouter,
@@ -81,6 +82,7 @@ import { healthCheck } from "./routes/status";
import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
import {
getMongoURL,
getNodeEnv,
getPort,
getSecretScanningGitAppId,
@@ -94,12 +96,20 @@ import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecre
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require("smee-client"); // eslint-disable-line
import path from "path";
import { serverConfigInit } from "./config/serverConfig";
import { initRedis } from "./services/RedisService";
let handler: null | any = null;
const main = async () => {
await initLogger();
const port = await getPort();
// initializing the database connection + redis
await initRedis()
await DatabaseService.initDatabase(await getMongoURL());
const serverCfg = await serverConfigInit();
await setup();
await EELicenseService.initGlobalFeatureSet();
@@ -203,6 +213,7 @@ const main = async () => {
// v1 routes
app.use("/api/v1/signup", v1SignupRouter);
app.use("/api/v1/auth", v1AuthRouter);
app.use("/api/v1/admin", v1AdminRouter);
app.use("/api/v1/bot", v1BotRouter);
app.use("/api/v1/user", v1UserRouter);
app.use("/api/v1/user-action", v1UserActionRouter);
@@ -271,7 +282,22 @@ const main = async () => {
app.use(requestErrorHandler);
const server = app.listen(port, async () => {
logger.info(`Server started listening at port ${port}`);
if (!serverCfg.initialized) {
logger.info(`Welcome to Infisical
Create your Infisical administrator account at:
http://localhost:${port}/admin/signup
`);
} else {
logger.info(`Welcome back!
To access Infisical Administrator Panel open
http://localhost:${port}/admin
To access Infisical server
http://localhost:${port}
`);
}
});
// await createTestUserForDevelopment();

View File

@@ -7,17 +7,21 @@ import requireSecretAuth from "./requireSecretAuth";
import requireSecretsAuth from "./requireSecretsAuth";
import requireBlindIndicesEnabled from "./requireBlindIndicesEnabled";
import requireE2EEOff from "./requireE2EEOff";
import { requireSuperAdminAccess } from "./requireSuperAdminAccess";
import validateRequest from "./validateRequest";
import { disableSignUpByServerCfg } from "./serverAdmin";
export {
requireAuth,
requireMfaAuth,
requireSignupAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
requireSecretAuth,
requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
validateRequest,
requireAuth,
requireMfaAuth,
requireSignupAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
requireSecretAuth,
requireSecretsAuth,
requireBlindIndicesEnabled,
requireE2EEOff,
validateRequest,
requireSuperAdminAccess,
disableSignUpByServerCfg
};

View File

@@ -15,7 +15,7 @@ export const requestErrorHandler: ErrorRequestHandler = async (
if (res.headersSent) return next();
let error: RequestError;
switch (true) {
case err instanceof TokenExpiredError:
error = UnauthorizedRequestError({ stack: err.stack, message: "Token expired" });
@@ -31,7 +31,7 @@ export const requestErrorHandler: ErrorRequestHandler = async (
break;
}
logger[mapToPinoLogLevel(error.level)](error);
logger[mapToPinoLogLevel(error.level)]({ msg: error });
if (req.user) {
Sentry.setUser({ email: (req.user as any).email });

View File

@@ -0,0 +1,8 @@
import { NextFunction, Request, Response } from "express";
import { UnauthorizedRequestError } from "../utils/errors";
export const requireSuperAdminAccess = (req: Request, _res: Response, next: NextFunction) => {
const isSuperAdmin = req.user.superAdmin;
if (!isSuperAdmin) throw UnauthorizedRequestError({ message: "Requires superadmin access" });
return next();
};

View File

@@ -0,0 +1,9 @@
import { NextFunction, Request, Response } from "express";
import { getServerConfig } from "../config/serverConfig";
import { BadRequestError } from "../utils/errors";
export const disableSignUpByServerCfg = (_req: Request, _res: Response, next: NextFunction) => {
const cfg = getServerConfig();
if (!cfg.allowSignUp) throw BadRequestError({ message: "Signup are disabled" });
return next();
};

View File

@@ -0,0 +1,25 @@
import { Schema, Types, model } from "mongoose";
export interface IServerConfig {
_id: Types.ObjectId;
initialized: boolean;
allowSignUp: boolean;
}
const serverConfigSchema = new Schema<IServerConfig>(
{
initialized: {
type: Boolean,
default: false
},
allowSignUp: {
type: Boolean,
default: true
}
},
{
timestamps: true
}
);
export const ServerConfig = model<IServerConfig>("ServerConfig", serverConfigSchema);

View File

@@ -1,16 +1,6 @@
import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models";
export enum Permission {
READ = "read",
WRITE = "write"
}
export interface IServiceTokenV3Scope {
environment: string;
secretPath: string;
permissions: Permission[];
}
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
export interface IServiceTokenV3TrustedIp {
ipAddress: string;
@@ -33,7 +23,8 @@ export interface IServiceTokenDataV3 extends Document {
isRefreshTokenRotationEnabled: boolean;
expiresAt?: Date;
accessTokenTTL: number;
scopes: Array<IServiceTokenV3Scope>;
role: "admin" | "member" | "viewer" | "custom";
customRole: Types.ObjectId;
trustedIps: Array<IServiceTokenV3TrustedIp>;
}
@@ -100,28 +91,15 @@ const serviceTokenDataV3Schema = new Schema(
default: 7200,
required: true
},
scopes: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
default: "/",
required: true
},
permissions: {
type: [String],
enum: [Permission.READ, Permission.WRITE],
default: [Permission.READ],
required: true
}
}
],
role: {
type: String,
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
required: true
},
customRole: {
type: Schema.Types.ObjectId,
ref: "Role"
},
trustedIps: {
type: [
{

View File

@@ -1,125 +1,141 @@
import { Document, Schema, Types, model } from "mongoose";
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml",
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export interface IUser extends Document {
_id: Types.ObjectId;
authProvider?: AuthMethod;
authMethods: AuthMethod[];
email: string;
firstName?: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
salt?: string;
verifier?: string;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
ip: string;
userAgent: string;
}[];
_id: Types.ObjectId;
authProvider?: AuthMethod;
authMethods: AuthMethod[];
email: string;
superAdmin?: boolean;
firstName?: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
salt?: string;
verifier?: string;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
ip: string;
userAgent: string;
}[];
}
const userSchema = new Schema<IUser>(
{
authProvider: { // TODO field: deprecate
type: String,
enum: AuthMethod,
},
authMethods: {
type: [{
type: String,
enum: AuthMethod,
}],
default: [AuthMethod.EMAIL],
required: true
},
email: {
type: String,
required: true,
unique: true,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
encryptionVersion: {
type: Number,
select: false,
default: 1, // to resolve backward-compatibility issues
},
protectedKey: { // introduced as part of encryption version 2
type: String,
select: false,
},
protectedKeyIV: { // introduced as part of encryption version 2
type: String,
select: false,
},
protectedKeyTag: { // introduced as part of encryption version 2
type: String,
select: false,
},
publicKey: {
type: String,
select: false,
},
encryptedPrivateKey: {
type: String,
select: false,
},
iv: { // iv of [encryptedPrivateKey]
type: String,
select: false,
},
tag: { // tag of [encryptedPrivateKey]
type: String,
select: false,
},
salt: {
type: String,
select: false,
},
verifier: {
type: String,
select: false,
},
isMfaEnabled: {
type: Boolean,
default: false,
},
mfaMethods: [{
type: String,
}],
devices: {
type: [{
ip: String,
userAgent: String,
}],
default: [],
select: false,
},
},
{
timestamps: true,
}
{
authProvider: {
// TODO field: deprecate
type: String,
enum: AuthMethod
},
authMethods: {
type: [
{
type: String,
enum: AuthMethod
}
],
default: [AuthMethod.EMAIL],
required: true
},
email: {
type: String,
required: true,
unique: true
},
firstName: {
type: String
},
lastName: {
type: String
},
encryptionVersion: {
type: Number,
select: false,
default: 1 // to resolve backward-compatibility issues
},
protectedKey: {
// introduced as part of encryption version 2
type: String,
select: false
},
protectedKeyIV: {
// introduced as part of encryption version 2
type: String,
select: false
},
protectedKeyTag: {
// introduced as part of encryption version 2
type: String,
select: false
},
publicKey: {
type: String,
select: false
},
encryptedPrivateKey: {
type: String,
select: false
},
superAdmin: {
type: Boolean
},
iv: {
// iv of [encryptedPrivateKey]
type: String,
select: false
},
tag: {
// tag of [encryptedPrivateKey]
type: String,
select: false
},
salt: {
type: String,
select: false
},
verifier: {
type: String,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
},
mfaMethods: [
{
type: String
}
],
devices: {
type: [
{
ip: String,
userAgent: String
}
],
default: [],
select: false
}
},
{
timestamps: true
}
);
export const User = model<IUser>("User", userSchema);
export const User = model<IUser>("User", userSchema);

View File

@@ -0,0 +1,20 @@
import express from "express";
import { adminController } from "../../controllers/v1";
const router = express.Router();
import { requireAuth, requireSuperAdminAccess } from "../../middleware";
import { AuthMode } from "../../variables";
router.get("/config", adminController.getServerConfigInfo);
router.post("/signup", adminController.adminSignUp);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
requireSuperAdminAccess,
adminController.updateServerConfig
);
export default router;

View File

@@ -18,6 +18,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import webhooks from "./webhook";
import secretImps from "./secretImps";
import admin from "./admin";
export {
signup,
@@ -39,5 +40,6 @@ export {
secretsFolder,
webhooks,
secretImps,
sso
sso,
admin
};

View File

@@ -2,18 +2,21 @@ import express from "express";
const router = express.Router();
import { signupController } from "../../controllers/v1";
import { authLimiter } from "../../helpers/rateLimiter";
import { disableSignUpByServerCfg } from "../../middleware";
// TODO: consider moving to users/v3/signup
router.post(
// TODO endpoint: consider moving to v3/users/signup/mail
"/email/signup",
disableSignUpByServerCfg,
authLimiter,
signupController.beginEmailSignup
);
router.post(
"/email/verify", // TODO endpoint: consider moving to v3/users/signup/verify
disableSignUpByServerCfg,
authLimiter,
signupController.verifyEmailSignup
);

View File

@@ -36,12 +36,4 @@ router.delete(
environmentController.deleteWorkspaceEnvironment
);
router.get(
"/:workspaceId/environments",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}),
environmentController.getAllAccessibleEnvironmentsOfWorkspace
);
export default router;

View File

@@ -1,49 +1,51 @@
import express from "express";
const router = express.Router();
import { body } from "express-validator";
import { requireSignupAuth, validateRequest } from "../../middleware";
import { disableSignUpByServerCfg, requireSignupAuth, validateRequest } from "../../middleware";
import { signupController } from "../../controllers/v2";
import { authLimiter } from "../../helpers/rateLimiter";
router.post(
"/complete-account/signup", // TODO endpoint: deprecate (moved to v3/signup/complete/account-signup)
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountSignup
"/complete-account/signup", // TODO endpoint: deprecate (moved to v3/signup/complete/account-signup),
disableSignUpByServerCfg,
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
body("organizationName").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountSignup
);
router.post(
"/complete-account/invite", // TODO: consider moving to v3/users/new/complete-account/invite
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountInvite
"/complete-account/invite", // TODO: consider moving to v3/users/new/complete-account/invite
disableSignUpByServerCfg,
authLimiter,
requireSignupAuth,
body("email").exists().isString().trim().notEmpty().isEmail(),
body("firstName").exists().isString().trim().notEmpty(),
body("lastName").exists().isString().trim().notEmpty(),
body("protectedKey").exists().isString().trim().notEmpty(),
body("protectedKeyIV").exists().isString().trim().notEmpty(),
body("protectedKeyTag").exists().isString().trim().notEmpty(),
body("publicKey").exists().trim().notEmpty(),
body("encryptedPrivateKey").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(),
body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(),
body("salt").exists().isString().trim().notEmpty(),
body("verifier").exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountInvite
);
export default router;
export default router;

View File

@@ -2,10 +2,11 @@ import express from "express";
const router = express.Router();
import { signupController } from "../../controllers/v3";
import { authLimiter } from "../../helpers/rateLimiter";
import { validateRequest } from "../../middleware";
import { disableSignUpByServerCfg, validateRequest } from "../../middleware";
router.post(
"/complete-account/signup", // TODO: consider moving endpoint to v3/users/new/complete-account/signup
disableSignUpByServerCfg,
authLimiter,
validateRequest,
signupController.completeAccountSignup

View File

@@ -3,11 +3,14 @@ import { logger } from "../utils/logging";
let redisClient: TRedis | null;
if (process.env.REDIS_URL) {
redisClient = new Redis(process.env.REDIS_URL as string);
} else {
logger.warn("Redis URL not set, skipping Redis initialization.");
redisClient = null;
export const initRedis = async () => {
if (process.env.REDIS_URL) {
redisClient = new Redis(process.env.REDIS_URL as string);
} else {
logger.warn("Redis URL not set, skipping Redis initialization.");
redisClient = null;
}
}
export { redisClient };

View File

@@ -8,30 +8,28 @@ import {
SMTP_HOST_ZOHOMAIL
} from "../variables";
import SMTPConnection from "nodemailer/lib/smtp-connection";
import * as Sentry from "@sentry/node";
import {
getSmtpHost,
getSmtpPassword,
getSmtpPort,
getSmtpSecure,
getSmtpUsername,
getSmtpUsername
} from "../config";
import { logger } from "../utils/logging";
export const initSmtp = async () => {
const mailOpts: SMTPConnection.Options = {
host: await getSmtpHost(),
port: await getSmtpPort(),
port: await getSmtpPort()
};
if ((await getSmtpUsername()) && (await getSmtpPassword())) {
mailOpts.auth = {
user: await getSmtpUsername(),
pass: await getSmtpPassword(),
pass: await getSmtpPassword()
};
}
if ((await getSmtpSecure()) ? (await getSmtpSecure()) : false) {
if ((await getSmtpSecure()) ? await getSmtpSecure() : false) {
switch (await getSmtpHost()) {
case SMTP_HOST_SENDGRID:
mailOpts.requireTLS = true;
@@ -39,38 +37,38 @@ export const initSmtp = async () => {
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_GMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
break;
case SMTP_HOST_OFFICE365:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2"
}
};
break;
default:
if ((await getSmtpHost()).includes("amazonaws.com")) {
mailOpts.tls = {
ciphers: "TLSv1.2",
}
ciphers: "TLSv1.2"
};
} else {
mailOpts.secure = true;
}
@@ -79,20 +77,6 @@ export const initSmtp = async () => {
}
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
.then(async () => {
Sentry.setUser(null);
Sentry.captureMessage("SMTP - Successfully connected");
logger.info("SMTP - Successfully connected");
})
.catch(async (err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()} \n\t${err}`
);
logger.error(err, `SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
});
return transporter;
};

View File

@@ -1,62 +1,65 @@
import {
AuthMethod,
User
} from "../../../models";
import { AuthMethod, User } from "../../../models";
import { createToken } from "../../../helpers/auth";
import { AuthTokenType } from "../../../variables";
import { getAuthSecret, getJwtProviderAuthLifetime} from "../../../config";
import { getAuthSecret, getJwtProviderAuthLifetime } from "../../../config";
import { getServerConfig } from "../../../config/serverConfig";
interface SSOUserTokenFlowParams {
email: string;
firstName: string;
lastName: string;
authMethod: AuthMethod;
callbackPort?: string;
email: string;
firstName: string;
lastName: string;
authMethod: AuthMethod;
callbackPort?: string;
}
export const handleSSOUserTokenFlow = async ({
email,
firstName,
lastName,
authMethod,
callbackPort
email,
firstName,
lastName,
authMethod,
callbackPort
}: SSOUserTokenFlowParams) => {
let user = await User.findOne({
email
}).select("+publicKey");
if (!user) {
user = await new User({
email,
authMethods: [authMethod],
firstName,
lastName
}).save();
}
let user = await User.findOne({
email
}).select("+publicKey");
let isLinkingRequired = false;
if (!user.authMethods.includes(authMethod)) {
const serverCfg = getServerConfig();
if (!user && !serverCfg.allowSignUp) throw new Error("User signup disabled");
if (!user) {
user = await new User({
email,
authMethods: [authMethod],
firstName,
lastName
}).save();
}
let isLinkingRequired = false;
if (!user.authMethods.includes(authMethod)) {
isLinkingRequired = true;
}
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod,
isUserCompleted,
isLinkingRequired,
...(callbackPort ? {
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod,
isUserCompleted,
isLinkingRequired,
...(callbackPort
? {
callbackPort
} : {})
}
: {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getAuthSecret(),
});
return { isUserCompleted, providerAuthToken };
}
secret: await getAuthSecret()
});
return { isUserCompleted, providerAuthToken };
};

View File

@@ -1 +1 @@
export { logger } from "./logger";
export { logger, initLogger } from "./logger";

View File

@@ -1,15 +1,69 @@
import pino from "pino";
import pino, { Logger } from "pino";
import { getAwsCloudWatchLog, getNodeEnv } from "../../config";
export const logger = pino({
level: process.env.PINO_LOG_LEVEL || "trace",
timestamp: pino.stdTimeFunctions.isoTime,
formatters: {
bindings: (bindings) => {
return {
export let logger: Logger;
// https://github.com/pinojs/pino/blob/master/lib/levels.js#L13-L20
const logLevelToSeverityLookup: Record<string, string> = {
'10': 'TRACE',
'20': 'DEBUG',
'30': 'INFO',
'40': 'WARNING',
'50': 'ERROR',
'60': 'CRITICAL'
}
export const initLogger = async () => {
const awsCloudWatchLogCfg = await getAwsCloudWatchLog();
const nodeEnv = await getNodeEnv();
const isProduction = nodeEnv === "production";
const targets: pino.TransportMultiOptions["targets"][number][] = [
isProduction
? { level: "info", target: "pino/file", options: {} }
: {
level: "info",
target: "pino-pretty", // must be installed separately
options: {
colorize: true
}
}
];
if (awsCloudWatchLogCfg) {
targets.push({
target: "@serdnam/pino-cloudwatch-transport",
level: "info",
options: {
logGroupName: awsCloudWatchLogCfg.logGroupName,
logStreamName: awsCloudWatchLogCfg.logGroupName,
awsRegion: awsCloudWatchLogCfg.region,
awsAccessKeyId: awsCloudWatchLogCfg.accessKeyId,
awsSecretAccessKey: awsCloudWatchLogCfg.accessKeySecret,
interval: awsCloudWatchLogCfg.interval
}
});
}
const transport = pino.transport({
targets
});
logger = pino(
{
mixin(_context, level) {
return { 'severity': logLevelToSeverityLookup[level] || logLevelToSeverityLookup['30'] }
},
level: process.env.PINO_LOG_LEVEL || "info",
formatters: {
bindings: (bindings) => {
return {
pid: bindings.pid,
hostname: bindings.hostname
// node_version: process.version
};
};
}
}
},
}
});
transport
);
};

View File

@@ -2,12 +2,12 @@ import crypto from "crypto";
import { Types } from "mongoose";
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
import { EESecretService } from "../../ee/services";
import { redisClient } from "../../services/RedisService"
import {
IPType,
ISecretVersion,
Role,
SecretSnapshot,
import { redisClient } from "../../services/RedisService";
import {
IPType,
ISecretVersion,
Role,
SecretSnapshot,
SecretVersion,
TrustedIP
} from "../../ee/models";
@@ -30,7 +30,7 @@ import {
Workspace
} from "../../models";
import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import { client, getEncryptionKey, getIsInfisicalCloud, getRootEncryptionKey } from "../../config";
import {
ADMIN,
ALGORITHM_AES_256_GCM,
@@ -47,6 +47,7 @@ import {
memberProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { logger } from "../logging";
import { getServerConfig, updateServerConfig } from "../../config/serverConfig";
/**
* Backfill secrets to ensure that they're all versioned and have
@@ -693,7 +694,7 @@ export const backfillUserAuthMethods = async () => {
export const backfillPermission = async () => {
const lockKey = "backfill_permission_lock";
const timeout = 900000; // 15 min lock timeout in milliseconds
const timeout = 900000; // 15 min lock timeout in milliseconds
const lock = await redisClient?.set(lockKey, 1, "PX", timeout, "NX");
if (lock) {
@@ -705,13 +706,16 @@ export const backfillPermission = async () => {
$exists: true,
$ne: []
},
role: MEMBER,
role: MEMBER
})
.populate<{ workspace: IWorkspace }>("workspace")
.lean();
// group memberships that need the same permission set
const roleMap = new Map<string, { membershipIds: string[], permissions: any[], organizationId: string, workspaceId: string }>();
const roleMap = new Map<
string,
{ membershipIds: string[]; permissions: any[]; organizationId: string; workspaceId: string }
>();
for (const membership of memberships) {
// get permissions of members except secret permission
@@ -729,11 +733,11 @@ export const backfillPermission = async () => {
});
// environments that are not listed in deniedPermissions should be set to allowed for both read & and write
membership.workspace.environments.forEach(env => {
membership.workspace.environments.forEach((env) => {
if (!secretAccessRule?.[env.slug]) {
secretAccessRule[env.slug] = { read: true, write: true };
}
})
});
const secretPermissions: any = [];
Object.entries(secretAccessRule).forEach(([envSlug, { read, write }]) => {
@@ -769,21 +773,27 @@ export const backfillPermission = async () => {
const value = roleMap.get(key);
if (value) {
value.membershipIds.push(membership._id.toString());
value.organizationId = membership.workspace.organization.toString()
value.workspaceId = membership.workspace._id.toString()
value.organizationId = membership.workspace.organization.toString();
value.workspaceId = membership.workspace._id.toString();
} else {
roleMap.set(key, { membershipIds: [membership._id.toString()], permissions: [...customPermissions, ...secretPermissions], organizationId: membership.workspace.organization.toString(), workspaceId: membership.workspace._id.toString() });
roleMap.set(key, {
membershipIds: [membership._id.toString()],
permissions: [...customPermissions, ...secretPermissions],
organizationId: membership.workspace.organization.toString(),
workspaceId: membership.workspace._id.toString()
});
}
}
for (const [key, value] of roleMap.entries()) {
const { membershipIds, permissions, workspaceId, organizationId } = value
const membership_identity = crypto.randomBytes(3).toString("hex")
const { membershipIds, permissions, workspaceId, organizationId } = value;
const membership_identity = crypto.randomBytes(3).toString("hex");
const role = new Role({
name: `Limited [${membership_identity.toUpperCase()}]`,
organization: organizationId,
workspace: workspaceId,
description: "This role was auto generated by Infisical in effort to migrate your project members to our new permission system",
description:
"This role was auto generated by Infisical in effort to migrate your project members to our new permission system",
isOrgRole: false,
slug: `custom-role-${membership_identity}`,
permissions: permissions
@@ -792,7 +802,8 @@ export const backfillPermission = async () => {
await role.save();
for (const id of membershipIds) {
await Membership.findByIdAndUpdate(id, { // document db doesn't support update many so we must loop
await Membership.findByIdAndUpdate(id, {
// document db doesn't support update many so we must loop
$set: {
role: CUSTOM,
customRole: role
@@ -815,11 +826,9 @@ export const backfillPermission = async () => {
);
logger.info("Backfill: Finished converting owner role to member");
} catch (error) {
logger.error(error, "An error occurred when running script [backfillPermission]");
}
} else {
logger.info("Could not acquire lock for script [backfillPermission], skipping");
}
@@ -838,4 +847,33 @@ export const migrateRoleFromOwnerToAdmin = async () => {
);
logger.info("Backfill: Finished converting owner role to member");
}
};
export const migrationAssignSuperadmin = async () => {
const users = await User.find({}).sort({ createdAt: 1 }).limit(2);
const serverCfg = getServerConfig();
if (serverCfg.initialized) return;
if (await getIsInfisicalCloud()) {
await updateServerConfig({ initialized: true });
logger.info("Backfill: Infisical Cloud(initialized)");
return;
}
if (users.length) {
let superAdminUserId = "";
const firstAccount = users?.[0];
if (firstAccount.email === "test@localhost.local" && users.length === 2) {
superAdminUserId = users?.[1]?._id.toString();
} else {
superAdminUserId = firstAccount._id.toString();
}
if (superAdminUserId) {
const user = await User.findByIdAndUpdate(superAdminUserId, { superAdmin: true });
await updateServerConfig({ initialized: true });
logger.info(`Migrated ${user?.email} to superuser`);
}
logger.info("Backfill: Migrated first infisical user to super admin");
}
};

View File

@@ -1,9 +1,8 @@
import * as Sentry from "@sentry/node";
import { DatabaseService, TelemetryService } from "../../services";
import { TelemetryService } from "../../services";
import { setTransporter } from "../../helpers/nodemailer";
import { EELicenseService } from "../../ee/services";
import { initSmtp } from "../../services/smtp";
import { createTestUserForDevelopment } from "../addDevelopmentUser";
// eslint-disable-next-line @typescript-eslint/no-var-requires
import { validateEncryptionKeysConfig } from "./validateConfig";
import {
@@ -18,14 +17,15 @@ import {
backfillServiceTokenMultiScope,
backfillTrustedIps,
backfillUserAuthMethods,
migrateRoleFromOwnerToAdmin
migrateRoleFromOwnerToAdmin,
migrationAssignSuperadmin
} from "./backfillData";
import {
reencryptBotOrgKeys,
reencryptBotPrivateKeys,
reencryptSecretBlindIndexDataSalts
} from "./reencryptData";
import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config";
import { getNodeEnv, getRedisUrl, getSentryDSN } from "../../config";
import {
initializeGitHubStrategy,
initializeGitLabStrategy,
@@ -33,6 +33,7 @@ import {
initializeSamlStrategy
} from "../authn/passport";
import { logger } from "../logging";
import { bootstrap } from "../../bootstrap";
/**
* Prepare Infisical upon startup. This includes tasks like:
@@ -55,14 +56,15 @@ export const setup = async () => {
await TelemetryService.logTelemetryMessage();
// initializing SMTP configuration
setTransporter(await initSmtp());
const transporter = await initSmtp();
setTransporter(transporter);
// initializing global feature set
await EELicenseService.initGlobalFeatureSet();
// initializing auth strategies
await initializeGoogleStrategy();
await initializeGitHubStrategy()
await initializeGitHubStrategy();
await initializeGitLabStrategy();
await initializeSamlStrategy();
@@ -71,8 +73,7 @@ export const setup = async () => {
// await reencryptBotPrivateKeys();
// await reencryptSecretBlindIndexDataSalts();
// initializing the database connection
await DatabaseService.initDatabase(await getMongoURL());
await bootstrap({ transporter });
/**
* NOTE: the order in this setup function is critical.
@@ -92,7 +93,8 @@ export const setup = async () => {
await backfillTrustedIps();
await backfillUserAuthMethods();
// await backfillPermission();
await migrateRoleFromOwnerToAdmin()
await migrateRoleFromOwnerToAdmin();
await migrationAssignSuperadmin();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY
@@ -108,5 +110,7 @@ export const setup = async () => {
environment: await getNodeEnv()
});
await createTestUserForDevelopment();
// akhilmhdh: removed dev account as we have now admin account onboarding flow
// That will be user's first account going forward
// await createTestUserForDevelopment();
};

View File

@@ -0,0 +1,24 @@
import { z } from "zod";
export const UpdateServerConfigV1 = z.object({
body: z.object({
allowSignUp: z.boolean().optional()
})
});
export const SignupV1 = z.object({
body: z.object({
email: z.string().email().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
})
});

View File

@@ -1,64 +1,5 @@
import { Types } from "mongoose";
import { IServiceTokenDataV3 } from "../models";
import { Permission } from "../models/serviceTokenDataV3";
import { z } from "zod";
import { UnauthorizedRequestError } from "../utils/errors";
import { isValidScopeV3 } from "../helpers";
import { AuthData } from "../interfaces/middleware";
import { checkIPAgainstBlocklist } from "../utils/ip";
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} acceptedPermissions - accepted permissions as part of the endpoint
*/
export const validateServiceTokenDataV3ClientForWorkspace = async ({
authData,
serviceTokenData,
workspaceId,
environment,
secretPath = "/",
requiredPermissions
}: {
authData: AuthData;
serviceTokenData: IServiceTokenDataV3;
workspaceId: Types.ObjectId;
environment?: string;
secretPath?: string;
requiredPermissions: Permission[];
}) => {
// validate ST V3 IP address
checkIPAgainstBlocklist({
ipAddress: authData.ipAddress,
trustedIps: serviceTokenData.trustedIps
});
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: "Failed service token authorization for the given workspace"
});
}
if (environment) {
const isValid = isValidScopeV3({
authPayload: serviceTokenData,
environment,
secretPath,
requiredPermissions
});
if (!isValid) throw UnauthorizedRequestError({
message: "Failed service token authorization for the given workspace"
});
}
};
import { MEMBER } from "../variables";
export const RefreshTokenV3 = z.object({
body: z.object({
@@ -71,20 +12,14 @@ export const CreateServiceTokenV3 = z.object({
name: z.string().trim(),
workspaceId: z.string().trim(),
publicKey: z.string().trim(),
scopes: z
.object({
permissions: z.enum(["read", "write"]).array(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
.array()
.min(1),
trustedIps: z
role: z.string().trim().min(1).default(MEMBER),
trustedIps: z // TODO: provide default
.object({
ipAddress: z.string().trim(),
})
.array()
.min(1),
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }]),
expiresIn: z.number().optional(),
accessTokenTTL: z.number().int().min(1),
encryptedKey: z.string().trim(),
@@ -100,15 +35,7 @@ export const UpdateServiceTokenV3 = z.object({
body: z.object({
name: z.string().trim().optional(),
isActive: z.boolean().optional(),
scopes: z
.object({
permissions: z.enum(["read", "write"]).array(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
.array()
.min(1)
.optional(),
role: z.string().trim().min(1).optional(),
trustedIps: z
.object({
ipAddress: z.string().trim()

View File

@@ -15,6 +15,9 @@
"skipLibCheck": true,
"typeRoots": ["./src/types", "./node_modules/@types"]
},
"ts-node": {
"swc": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -18,6 +18,7 @@ services:
redis:
image: redis
container_name: infisical-dev-redis
env_file: .env
environment:
- ALLOW_EMPTY_PASSWORD=yes
ports:

View File

@@ -336,5 +336,9 @@
"step5-invite-team": "Invite your team",
"step5-subtitle": "Infisical is meant to be used with your teammates. Invite them to test it out.",
"step5-skip": "Skip"
},
"admin": {
"signup-title": "Admin Sign Up",
"dashboard": "Admin Dashboard"
}
}

View File

@@ -2,7 +2,11 @@ import { useTranslation } from "react-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import issueBackupKey from "../utilities/cryptography/issueBackupKey";
import { useToggle } from "@app/hooks";
import { generateUserBackupKey } from "@app/lib/crypto";
import { useNotificationContext } from "../context/Notifications/NotificationProvider";
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
import { Button } from "../v2";
interface DownloadBackupPDFStepProps {
@@ -28,35 +32,56 @@ export default function DonwloadBackupPDFStep({
name
}: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const [isLoading, setIsLoading] = useToggle();
const handleBackupKeyGenerate = async () => {
try {
setIsLoading.on();
const generatedKey = await generateUserBackupKey(email, password);
await generateBackupPDFAsync({
generatedKey,
personalEmail: email,
personalName: name
});
incrementStep();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Faield to generate backup key"
});
} finally {
setIsLoading.off();
}
};
return (
<div className="flex flex-col items-center w-full h-full md:px-6 mx-auto mb-36 md:mb-16">
<p className="text-xl text-center font-medium flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />{t("signup.step4-message")}
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
{t("signup.step4-message")}
</p>
<div className="flex flex-col pb-2 bg-mineshaft-800 border border-mineshaft-600 items-center justify-center text-center lg:w-1/6 w-full md:min-w-[24rem] mt-8 max-w-md text-bunker-300 text-md rounded-md">
<div className="w-full mt-4 md:mt-8 flex flex-row text-center items-center m-2 text-bunker-300 rounded-md lg:w-1/6 lg:w-1/6 w-full md:min-w-[23rem] px-3 mx-auto">
<span className='mb-2'>{t("signup.step4-description1")} {t("signup.step4-description3")}</span>
<span className="mb-2">
{t("signup.step4-description1")} {t("signup.step4-description3")}
</span>
</div>
<div className="flex flex-col items-center px-3 justify-center mt-0 md:mt-4 mb-2 md:mb-4 lg:w-1/6 w-full md:min-w-[20rem] mt-2 md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: () => { },
setBackupKeyIssued: () => { }
});
incrementStep();
}}
onClick={handleBackupKeyGenerate}
size="sm"
isFullWidth
className='h-12'
isLoading={isLoading}
isDisabled={isLoading}
className="h-12"
colorSchema="primary"
variant="outline_bg"
> Download PDF </Button>
>
Download PDF
</Button>
</div>
</div>
</div>

View File

@@ -8,9 +8,6 @@ import Telemetry from "./telemetry/Telemetry";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface IsLoginSuccessful {
mfaEnabled: boolean;
success: boolean;
@@ -22,118 +19,100 @@ interface IsLoginSuccessful {
* @param {string} email - email of user to log in
* @param {string} password - password of user to log in
*/
const attemptLogin = async (
{
email,
password,
providerAuthToken,
}: {
email: string;
password: string;
providerAuthToken?: string;
}
): Promise<IsLoginSuccessful> => {
const attemptLogin = async ({
email,
password,
providerAuthToken
}: {
email: string;
password: string;
providerAuthToken?: string;
}): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1({
email,
clientPublicKey,
providerAuthToken,
});
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2(
{
email,
clientProof,
providerAuthToken,
}
);
if (mfaEnabled) {
// case: MFA is enabled
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
resolve({
mfaEnabled,
success: true
});
} else if (
!mfaEnabled &&
encryptionVersion &&
encryptedPrivateKey &&
iv &&
tag &&
token
) {
// case: MFA is not enabled
// unset provider auth token in case it was used
SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
resolve({
mfaEnabled: false,
success: true
});
}
} catch (err) {
reject(err);
}
}
);
// eslint-disable-next-line new-cap
const client = new jsrp.client();
await new Promise((resolve) => {
client.init({ username: email, password }, () => resolve(null));
});
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1({
email,
clientPublicKey,
providerAuthToken
});
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2({
email,
clientProof,
providerAuthToken
});
if (mfaEnabled) {
// case: MFA is enabled
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
return {
mfaEnabled,
success: true
};
}
if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// case: MFA is not enabled
// unset provider auth token in case it was used
SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
return {
mfaEnabled: false,
success: true
};
}
return { success: false, mfaEnabled: false };
};
export default attemptLogin;

View File

@@ -1,6 +1,7 @@
import { fetchOrgUsers,fetchUserAction } from "@app/hooks/api/users/queries";
import { fetchOrgUsers, fetchUserAction } from "@app/hooks/api/users/queries";
interface OnboardingCheckProps {
orgId: string;
setTotalOnboardingActionsDone?: (value: number) => void;
setHasUserClickedSlack?: (value: boolean) => void;
setHasUserClickedIntro?: (value: boolean) => void;
@@ -12,6 +13,7 @@ interface OnboardingCheckProps {
* This function checks which onboarding steps a user has already finished.
*/
const onboardingCheck = async ({
orgId,
setTotalOnboardingActionsDone,
setHasUserClickedSlack,
setHasUserClickedIntro,
@@ -19,9 +21,7 @@ const onboardingCheck = async ({
setUsersInOrg
}: OnboardingCheckProps) => {
let countActions = 0;
const userActionSlack = await fetchUserAction(
"slack_cta_clicked"
);
const userActionSlack = await fetchUserAction("slack_cta_clicked");
if (userActionSlack) {
countActions += 1;
@@ -41,9 +41,8 @@ const onboardingCheck = async ({
}
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
const orgId = localStorage.getItem("orgData.id");
const orgUsers = await fetchOrgUsers(orgId || "");
if (orgUsers.length > 1) {
countActions += 1;
}

View File

@@ -16,7 +16,6 @@ const yyyy = today.getFullYear();
const todayFormatted = `${mm}/${dd}/${yyyy}`;
function createPdfHeader(doc: jsPDF, personalName: string) {
doc.setFillColor(255, 255, 255);
doc.rect(0, 0, 600, 900, "F");
@@ -92,5 +91,15 @@ function generateBackupPDF({ personalName, personalEmail, generatedKey }: PDFPro
doc.save("Infisical Emergency Kit.pdf");
}
export default generateBackupPDF;
/**
* This function generate a pdf with a secret key for a user.
*/
export function generateBackupPDFAsync({ personalName, personalEmail, generatedKey }: PDFProps) {
// eslint-disable-next-line new-cap
const doc = new jsPDF("p", "pt", "a4", true);
createPdfHeader(doc, personalName);
createPdfContent(doc, personalEmail, generatedKey);
return doc.save("Infisical Emergency Kit.pdf", { returnPromise: true });
}
export default generateBackupPDF;

View File

@@ -3,13 +3,15 @@
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
type Props = {
text?: string | string[];
frequency?: number;
className?: string;
};
export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
const [pos, setPos] = useState(0);
const isTextArray = Array.isArray(text);
useEffect(() => {
@@ -23,7 +25,12 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
}, []);
return (
<div className="container mx-auto flex relative flex-col h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8">
<div
className={twMerge(
"container mx-auto flex relative flex-col h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8",
className
)}
>
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
{text && isTextArray && (
<AnimatePresence exitBeforeEnter>

View File

@@ -3,7 +3,7 @@ import * as SwitchPrimitive from "@radix-ui/react-switch";
import { twMerge } from "tailwind-merge";
export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, "checked" | "disabled" | "required"> & {
children: ReactNode;
children?: ReactNode;
id: string;
isChecked?: boolean;
isRequired?: boolean;

View File

@@ -1,7 +1,5 @@
import { useOrganization, useSubscription } from "@app/context";
import {
useGetOrgTrialUrl
} from "@app/hooks/api";
import { useGetOrgTrialUrl } from "@app/hooks/api";
import { Button } from "../Button";
import { Modal, ModalContent } from "../Modal";
@@ -16,38 +14,36 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
const link = (subscription && subscription.slug !== null)
? `/org/${currentOrg?._id}/billing`
: "https://infisical.com/scheduledemo";
const link =
subscription && subscription.slug !== null
? `/org/${currentOrg?._id}/billing`
: "https://infisical.com/scheduledemo";
const handleUpgradeBtnClick = async () => {
try {
if (!subscription || !currentOrg) return;
if (!subscription.has_used_trial) {
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg._id,
success_url: window.location.href
});
window.location.href = url;
} else {
// direct user to upgrade their plan
window.location.href = link;
}
} catch (err) {
console.error(err);
}
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Unleash Infisical's Full Power"
>
<ModalContent title="Unleash Infisical's Full Power">
<p className="mb-2 text-bunker-300">{text}</p>
<p className="text-bunker-300">
Upgrade and get access to this, as well as to other powerful enhancements.
@@ -59,10 +55,10 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
onClick={handleUpgradeBtnClick}
className="mr-4"
>
{(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
{subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
</Button>
<Button
colorSchema="secondary"
<Button
colorSchema="secondary"
variant="plain"
onClick={() => onOpenChange && onOpenChange(false)}
>
@@ -71,5 +67,5 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele
</div>
</ModalContent>
</Modal>
)
}
);
};

View File

@@ -21,7 +21,8 @@ export const publicPaths = [
"/saml-sso",
"/login/provider/success", // TODO: change
"/login/provider/error", // TODO: change
"/login/sso"
"/login/sso",
"/admin/signup"
];
export const languageMap = {
@@ -50,7 +51,8 @@ const plansProd: Mapping = {
export const plans = plansProd || plansDev;
export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?";
export const leaveConfirmDefaultMessage =
"Your changes will be lost if you leave the page. Are you sure you want to continue?";
export const secretTagsColors = [
{
@@ -115,5 +117,5 @@ export const secretTagsColors = [
rgba: "rgb(255,0,0, 0.8)",
name: "Red",
selected: false
},
]
}
];

View File

@@ -18,7 +18,7 @@ type Props = {
// Provide a context for whole app to notify user is authorized or not
export const AuthProvider = ({ children }: Props): JSX.Element => {
const { isLoading } = useGetAuthToken();
const { pathname, push } = useRouter();
const { pathname, push, asPath } = useRouter();
const [isReady, setIsReady] = useToggle(false);
useEffect(() => {
@@ -26,7 +26,7 @@ export const AuthProvider = ({ children }: Props): JSX.Element => {
if (!isLoading) {
// not a public path and not authenticated kick to login page
if (!publicPaths.includes(pathname) && !isLoggedIn()) {
push("/login").then(() => {
push({ pathname: "/login", query: { redirect: asPath } }).then(() => {
setIsReady.on();
});
} else {
@@ -40,7 +40,12 @@ export const AuthProvider = ({ children }: Props): JSX.Element => {
if (isLoading || !isReady) {
return (
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
<img src="/images/loading/loading.gif" height={70} width={120} alt="infisical loading indicator" />
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="infisical loading indicator"
/>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
import { useRouter } from "next/router";
import { ContentLoader } from "@app/components/v2/ContentLoader";
import { useGetServerConfig } from "@app/hooks/api";
import { TServerConfig } from "@app/hooks/api/admin/types";
type TServerConfigContext = {
config: TServerConfig;
};
const ServerConfigContext = createContext<TServerConfigContext | null>(null);
type Props = {
children: ReactNode;
};
export const ServerConfigProvider = ({ children }: Props): JSX.Element => {
const router = useRouter();
const { data, isLoading } = useGetServerConfig();
// memorize the workspace details for the context
const value = useMemo<TServerConfigContext>(() => {
return {
config: data!
};
}, [data]);
useEffect(() => {
if (!isLoading && data && !data.initialized) {
router.push("/admin/signup");
}
}, [isLoading, data]);
if (isLoading || (!data?.initialized && router.pathname !== "/admin/signup")) {
return (
<div className="bg-bunker-800">
<ContentLoader text="Loading configurations" />
</div>
);
}
return <ServerConfigContext.Provider value={value}>{children}</ServerConfigContext.Provider>;
};
export const useServerConfig = () => {
const ctx = useContext(ServerConfigContext);
if (!ctx) {
throw new Error("useServerConfig has to be used within <UserContext.Provider>");
}
return ctx;
};

View File

@@ -0,0 +1 @@
export { ServerConfigProvider,useServerConfig } from "./ServerConfigContext";

View File

@@ -14,6 +14,7 @@ export {
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";
export { ServerConfigProvider,useServerConfig } from "./ServerConfigContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
export { UserProvider, useUser } from "./UserContext";
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";

View File

@@ -0,0 +1,2 @@
export { useCreateAdminUser, useUpdateServerConfig } from "./mutation";
export { useGetServerConfig } from "./queries";

View File

@@ -0,0 +1,39 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { User } from "../users/types";
import { adminQueryKeys } from "./queries";
import { TCreateAdminUserDTO, TServerConfig } from "./types";
export const useCreateAdminUser = () => {
const queryClient = useQueryClient();
return useMutation<{ user: User; token: string }, {}, TCreateAdminUserDTO>({
mutationFn: async (opt) => {
const { data } = await apiRequest.post("/api/v1/admin/signup", opt);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
}
});
};
export const useUpdateServerConfig = () => {
const queryClient = useQueryClient();
return useMutation<TServerConfig, {}, Partial<TServerConfig>>({
mutationFn: async (opt) => {
const { data } = await apiRequest.patch<{ config: TServerConfig }>(
"/api/v1/admin/config",
opt
);
return data.config;
},
onSuccess: (data) => {
queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
queryClient.invalidateQueries(adminQueryKeys.serverConfig());
}
});
};

View File

@@ -0,0 +1,34 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TServerConfig } from "./types";
export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const
};
const fetchServerConfig = async () => {
const { data } = await apiRequest.get<{ config: TServerConfig }>("/api/v1/admin/config");
return data.config;
};
export const useGetServerConfig = ({
options = {}
}: {
options?: Omit<
UseQueryOptions<
TServerConfig,
unknown,
TServerConfig,
ReturnType<typeof adminQueryKeys.serverConfig>
>,
"queryKey" | "queryFn"
>;
} = {}) =>
useQuery({
queryKey: adminQueryKeys.serverConfig(),
queryFn: fetchServerConfig,
...options,
enabled: options?.enabled ?? true
});

View File

@@ -0,0 +1,19 @@
export type TServerConfig = {
initialized: boolean;
allowSignUp: boolean;
};
export type TCreateAdminUserDTO = {
email: string;
firstName: string;
lastName?: string;
protectedKey: string;
protectedKeyTag: string;
protectedKeyIV: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
publicKey: string;
verifier: string;
salt: string;
};

View File

@@ -4,17 +4,6 @@ import {
UserAgentType
} from "./enums";
enum Permission {
READ = "read",
READ_WRITE = "readWrite"
}
interface Scope {
environment: string;
secretPath: string;
permission: Permission;
}
interface UserActorMetadata {
userId: string;
email: string;
@@ -211,7 +200,7 @@ interface CreateServiceTokenV3Event {
metadata: {
name: string;
isActive: boolean;
scopes: Array<Scope>;
role: string;
expiresAt?: Date;
}
}
@@ -221,7 +210,7 @@ interface UpdateServiceTokenV3Event {
metadata: {
name?: string;
isActive?: boolean;
scopes?: Array<Scope>;
role?: string;
expiresAt?: Date;
}
}
@@ -231,7 +220,7 @@ interface DeleteServiceTokenV3Event {
metadata: {
name: string;
isActive: boolean;
scopes: Array<Scope>;
role?: string;
expiresAt?: Date;
}
}

View File

@@ -1,4 +1,5 @@
export * from "./apiKeys";
export * from "./admin"
export * from "./auditLogs";
export * from "./auth";
export * from "./bots";

View File

@@ -85,8 +85,8 @@ export const useUpdateServiceTokenV3 = () => {
mutationFn: async ({
serviceTokenDataId,
name,
role,
isActive,
scopes,
trustedIps,
expiresIn,
accessTokenTTL,
@@ -94,8 +94,8 @@ export const useUpdateServiceTokenV3 = () => {
}) => {
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
name,
role,
isActive,
scopes,
trustedIps,
expiresIn,
accessTokenTTL,

View File

@@ -1,5 +1,3 @@
import { Permission } from "./enums";
export type ServiceTokenScope = {
environment: string;
secretPath: string;
@@ -38,12 +36,6 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
// --- v3
export type ServiceTokenV3Scope = {
permissions: Permission[];
environment: string;
secretPath: string;
};
export type ServiceTokenV3TrustedIp = {
_id: string;
ipAddress: string;
@@ -54,13 +46,17 @@ export type ServiceTokenV3TrustedIp = {
export type ServiceTokenDataV3 = {
_id: string;
name: string;
role: string;
customRole?: {
name: string;
slug: string;
};
workspace: string;
isActive: boolean;
refreshTokenLastUsed?: string;
accessTokenLastUsed?: string;
refreshTokenUsageCount: number;
accessTokenUsageCount: number;
scopes: ServiceTokenV3Scope[];
trustedIps: ServiceTokenV3TrustedIp[];
expiresAt?: string;
accessTokenTTL: number;
@@ -71,9 +67,9 @@ export type ServiceTokenDataV3 = {
export type CreateServiceTokenDataV3DTO = {
name: string;
role?: string;
workspaceId: string;
publicKey: string;
scopes: ServiceTokenV3Scope[];
trustedIps: {
ipAddress: string;
}[];
@@ -93,7 +89,7 @@ export type UpdateServiceTokenDataV3DTO = {
serviceTokenDataId: string;
isActive?: boolean;
name?: string;
scopes?: ServiceTokenV3Scope[];
role?: string;
trustedIps?: {
ipAddress: string;
}[];

View File

@@ -14,6 +14,7 @@ export type User = {
createdAt: Date;
updatedAt: Date;
email: string;
superAdmin: boolean;
firstName?: string;
lastName?: string;
authProvider?: AuthMethod;

View File

@@ -7,7 +7,6 @@ export {
useDeleteWsEnvironment,
useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
useGetUserWsEnvironments,
useGetWorkspaceAuthorizations,
useGetWorkspaceById,
useGetWorkspaceIndexStatus,

View File

@@ -12,14 +12,12 @@ import {
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
GetWsEnvironmentDTO,
NameWorkspaceSecretsDTO,
RenameWorkspaceDTO,
ReorderEnvironmentsDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
Workspace,
WorkspaceEnv
Workspace
} from "./types";
export const workspaceKeys = {
@@ -31,7 +29,6 @@ export const workspaceKeys = {
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const,
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const,
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceServiceTokenDataV3: (workspaceId: string) =>
@@ -103,21 +100,6 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
return data;
};
const fetchUserWsEnvironments = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ accessibleEnvironments: WorkspaceEnv[] }>(
`/api/v2/workspace/${workspaceId}/environments`
);
return data.accessibleEnvironments;
};
export const useGetUserWsEnvironments = ({ workspaceId, onSuccess }: GetWsEnvironmentDTO) =>
useQuery({
enabled: Boolean(workspaceId),
onSuccess,
queryKey: workspaceKeys.getUserWsEnvironments(workspaceId),
queryFn: () => fetchUserWsEnvironments(workspaceId)
});
// to get all userids in an org with the workspace they are part of
export const useGetUserWorkspaceMemberships = (orgId: string) =>
useQuery({

View File

@@ -30,11 +30,6 @@ export type CreateWorkspaceDTO = {
organizationId: string;
};
export type GetWsEnvironmentDTO = {
workspaceId: string;
onSuccess?: (data: WorkspaceEnv[]) => void;
};
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };

View File

@@ -0,0 +1,306 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable vars-on-top */
/* eslint-disable no-var */
/* eslint-disable func-names */
// @ts-nocheck
import { useTranslation } from "react-i18next";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faArrowLeft,
faArrowUpRightFromSquare,
faBook,
faEnvelope,
faInfinity,
faInfo,
faMobile,
faPlus,
faQuestion
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@app/components/v2";
import { useOrganization, useSubscription, useUser } from "@app/context";
import {
useGetOrgTrialUrl,
useGetUserAction,
useLogoutUser,
useRegisterUserAction
} from "@app/hooks/api";
interface LayoutProps {
children: React.ReactNode;
}
const supportOptions = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:support@infisical.com"
]
];
export const AdminLayout = ({ children }: LayoutProps) => {
const router = useRouter();
const { mutateAsync } = useGetOrgTrialUrl();
// eslint-disable-next-line prefer-const
const { currentOrg } = useOrganization();
const { user } = useUser();
const { subscription } = useSubscription();
const { data: updateClosed } = useGetUserAction("september_update_closed");
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
const { t } = useTranslation();
const registerUserAction = useRegisterUserAction();
const closeUpdate = async () => {
await registerUserAction.mutateAsync("september_update_closed");
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
console.log("Logging out...");
await logout.mutateAsync();
router.push("/login");
} catch (error) {
console.error(error);
}
};
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
{!router.asPath.includes("personal") && (
<div className="flex h-12 cursor-default justify-between items-center px-3 pt-6">
<Link href={`/org/${currentOrg?._id}/overview`}>
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
<DropdownMenu>
<DropdownMenuTrigger
asChild
className="p-1 hover:bg-primary-400 hover:text-black data-[state=open]:bg-primary-400 data-[state=open]:text-black"
>
<div
className="child flex items-center justify-center rounded-full bg-mineshaft pr-1 text-mineshaft-300 hover:bg-mineshaft-500"
style={{ fontSize: "11px", width: "26px", height: "26px" }}
>
{user?.firstName?.charAt(0)}
{user?.lastName && user?.lastName?.charAt(0)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
<Link href="/personal-settings">
<DropdownMenuItem>Personal Settings</DropdownMenuItem>
</Link>
<a
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Documentation
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
<a
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300 hover:text-mineshaft-100"
>
<DropdownMenuItem>
Join Slack Community
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] pl-1.5 text-xxs"
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link href="/admin" legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Admin Panel
</DropdownMenuItem>
</Link>
)}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
</button>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
<div
className={`relative mt-10 ${
subscription && subscription.slug === "starter" && !subscription.has_used_trial
? "mb-2"
: "mb-4"
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
>
<div
className={`${
!updateClosed ? "block" : "hidden"
} relative z-10 mb-6 flex pb-2 w-52 flex-col items-center justify-start rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3`}
>
<div className="text-md mt-2 w-full font-semibold text-mineshaft-100">
Infisical September update
</div>
<div className="mt-1 mb-1 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300">
Improved RBAC, new integrations, dashboard remake, and more!
</div>
<div className="mt-2 h-[6.77rem] w-full rounded-md border border-mineshaft-700">
<Image
src="/images/infisical-update-september-2023.png"
height={319}
width={539}
alt="kubernetes image"
className="rounded-sm"
/>
</div>
<div className="mt-3 flex w-full items-center justify-between px-0.5">
<button
type="button"
onClick={() => closeUpdate()}
className="text-mineshaft-400 duration-200 hover:text-mineshaft-100"
>
Close
</button>
<a
href="https://infisical.com/blog/infisical-update-september-2023"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-normal leading-[1.2rem] text-mineshaft-400 duration-200 hover:text-mineshaft-100"
>
Learn More{" "}
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-0.5 text-xs" />
</a>
</div>
</div>
{router.asPath.includes("org") && (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
className="w-full"
>
<div className="mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faPlus} className="mr-3" />
Invite people
</div>
</div>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
Help & Support
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{supportOptions.map(([icon, text, url]) => (
<DropdownMenuItem key={url}>
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{subscription &&
subscription.slug === "starter" &&
!subscription.has_used_trial && (
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg._id,
success_url: window.location.href
});
window.location.href = url;
}}
className="mt-1.5 w-full"
>
<div className="justify-left mb-1.5 mt-1.5 flex w-full items-center rounded-md bg-mineshaft-600 py-1 pl-4 text-mineshaft-300 duration-200 hover:bg-mineshaft-500 hover:text-primary-400">
<FontAwesomeIcon
icon={faInfinity}
className="mr-3 ml-0.5 py-2 text-primary"
/>
Start Free Pro Trial
</div>
</button>
)}
{infisicalPlatformVersion && (
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Version: {infisicalPlatformVersion}
</div>
)}
</div>
</nav>
</aside>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
{children}
</main>
</div>
</div>
<div className="z-[200] flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
</p>
</div>
</>
);
};

View File

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

View File

@@ -399,6 +399,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
/>
</DropdownMenuItem>
</a>
{user?.superAdmin && (
<Link href="/admin" legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Admin Panel
</DropdownMenuItem>
</Link>
)}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
@@ -498,7 +505,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
}
icon="system-outline-96-groups"
>
{t("nav.menu.members")}
Access Control
</MenuItem>
</a>
</Link>
@@ -541,19 +548,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
{/* <Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
}
icon="system-outline-126-verified"
>
IP Allowlist
</MenuItem>
</a>
</Link> */}
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<a>
<MenuItem

View File

@@ -1 +1,2 @@
export { AdminLayout } from "./AdminLayout";
export { AppLayout } from "./AppLayout";

View File

@@ -0,0 +1,126 @@
import crypto from "crypto";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
export const generateUserBackupKey = async (email: string, password: string) => {
// eslint-disable-next-line new-cap
const clientKey = new jsrp.client();
// eslint-disable-next-line new-cap
const clientPassword = new jsrp.client();
await new Promise((resolve) => {
clientPassword.init({ username: email, password }, () => resolve(null));
});
const clientPublicKey = clientPassword.getPublicKey();
const srpKeys = await srp1({ clientPublicKey });
clientPassword.setSalt(srpKeys.salt);
clientPassword.setServerPublicKey(srpKeys.serverPublicKey);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
await new Promise((resolve) => {
clientKey.init({ username: email, password: generatedKey }, () => resolve(null));
});
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
(resolve, reject) => {
clientKey.createVerifier((err, res) => {
if (err) return reject(err);
return resolve(res);
});
}
);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: String(localStorage.getItem("PRIVATE_KEY")),
secret: generatedKey
});
await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt,
verifier,
clientProof
});
return generatedKey;
};
export const generateUserPassKey = async (email: string, password: string) => {
// eslint-disable-next-line new-cap
const client = new jsrp.client();
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
await new Promise((resolve) => {
client.init({ username: email, password }, () => resolve(null));
});
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
(resolve, reject) => {
client.createVerifier((err, res) => {
if (err) return reject(err);
return resolve(res);
});
}
);
const derivedKey = await deriveArgonKey({
password,
salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
return {
protectedKey,
protectedKeyTag,
protectedKeyIV,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
publicKey,
verifier,
salt,
privateKey
};
};

View File

@@ -20,6 +20,7 @@ import {
OrgPermissionProvider,
OrgProvider,
ProjectPermissionProvider,
ServerConfigProvider,
SubscriptionProvider,
UserProvider,
WorkspaceProvider
@@ -84,36 +85,42 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
return (
<QueryClientProvider client={queryClient}>
<NotificationProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
<ServerConfigProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ServerConfigProvider>
</NotificationProvider>
</QueryClientProvider>
);
}
const Layout = Component?.layout || AppLayout;
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<AuthProvider>
<OrgProvider>
<OrgPermissionProvider>
<WorkspaceProvider>
<ProjectPermissionProvider>
<SubscriptionProvider>
<UserProvider>
<NotificationProvider>
<AppLayout>
<Component {...pageProps} />
</AppLayout>
</NotificationProvider>
</UserProvider>
</SubscriptionProvider>
</ProjectPermissionProvider>
</WorkspaceProvider>
</OrgPermissionProvider>
</OrgProvider>
</AuthProvider>
<NotificationProvider>
<ServerConfigProvider>
<AuthProvider>
<OrgProvider>
<OrgPermissionProvider>
<WorkspaceProvider>
<ProjectPermissionProvider>
<SubscriptionProvider>
<UserProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</UserProvider>
</SubscriptionProvider>
</ProjectPermissionProvider>
</WorkspaceProvider>
</OrgPermissionProvider>
</OrgProvider>
</AuthProvider>
</ServerConfigProvider>
</NotificationProvider>
</TooltipProvider>
</QueryClientProvider>
);

View File

@@ -0,0 +1,30 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { AdminLayout } from "@app/layouts";
import { AdminDashboardPage } from "@app/views/admin/DashboardPage";
const AdminDashboard = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("admin.dashboard") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("admin.dashboard.og-title") ?? ""} />
<meta name="og:description" content={t("admin.dashboard.og-description") ?? ""} />
</Head>
<div className="h-full">
<AdminDashboardPage />
</div>
</>
);
};
export default AdminDashboard;
AdminDashboard.requireAuth = true;
AdminDashboard.layout = AdminLayout;

View File

@@ -0,0 +1,21 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { SignUpPage } from "@app/views/admin/SignUpPage";
export default function LoginPage() {
const { t } = useTranslation();
return (
<div className="flex min-h-screen max-h-screen overflow-y-auto flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
<Head>
<title>{t("common.head-title", { title: t("signup.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={t("signup.og-title") ?? ""} />
<meta name="og:description" content={t("signup.og-description") ?? ""} />
</Head>
<SignUpPage />
</div>
);
}

View File

@@ -559,6 +559,7 @@ const OrganizationPage = withPermission(
useEffect(() => {
onboardingCheck({
orgId: currentOrg,
setHasUserClickedIntro,
setHasUserClickedSlack,
setHasUserPushedSecrets,

View File

@@ -12,6 +12,7 @@ import InitialSignupStep from "@app/components/signup/InitialSignupStep";
import TeamInviteStep from "@app/components/signup/TeamInviteStep";
import UserInfoStep from "@app/components/signup/UserInfoStep";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { useServerConfig } from "@app/context";
import { useVerifyEmailVerificationCode } from "@app/hooks/api";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@@ -34,6 +35,13 @@ export default function SignUp() {
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
const { t } = useTranslation();
const { mutateAsync } = useVerifyEmailVerificationCode();
const { config } = useServerConfig();
useEffect(() => {
if (!config.allowSignUp) {
router.push("/login");
}
}, [config.allowSignUp]);
useEffect(() => {
const tryAuth = async () => {
@@ -65,7 +73,7 @@ export default function SignUp() {
const { token } = await mutateAsync({ email, code });
SecurityClient.setSignupToken(token);
setStep(3);
} catch(err) {
} catch (err) {
console.error(err);
setCodeError(true);
}

View File

@@ -12,7 +12,7 @@ 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 { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { useServerConfig } from "@app/context";
import { navigateUserToOrg } from "../../Login.utils";
@@ -30,7 +30,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { data: serverDetails } = useFetchServerStatus();
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
@@ -84,7 +84,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
setIsLoading(false);
return;
}
await navigateUserToOrg(router);
// case: login does not require MFA step
@@ -225,7 +225,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
</Button>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{!serverDetails?.inviteOnlySignup ? (
{config.allowSignUp ? (
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link href="/signup">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
@@ -236,7 +236,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
) : (
<div />
)}
<div className="flex flex-row text-sm text-bunker-400">
<div className="flex flex-row text-sm text-bunker-400 mt-2">
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Forgot password? Recover your account

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