Compare commits

..

26 Commits

Author SHA1 Message Date
Maidul Islam
8bc952388c add log 2024-02-07 12:23:48 -05:00
Maidul Islam
eef29cd2d4 patch get secret by name 2024-02-07 12:11:58 -05:00
Akhil Mohan
6ef873f3a0 Merge pull request #1377 from Infisical/allow-name-initial-org
add initial org rename
2024-02-07 20:51:30 +05:30
Maidul Islam
fe99c12c0d add initial org rename 2024-02-07 10:18:41 -05:00
Akhil Mohan
332b0e2cc3 Merge pull request #1374 from Infisical/admin-ui-fix
fix admin dashboard styling
2024-02-07 12:18:09 +05:30
Vladyslav Matsiiako
8bc9a5fed6 fix admin dashboard styling 2024-02-06 22:45:58 -08:00
Maidul Islam
55e75bbbef Merge pull request #1373 from akhilmhdh/feat/patch-server-cfg-init
feat: fixed server cfg stale in replication
2024-02-07 01:09:42 -05:00
Akhil Mohan
61ff732ec0 feat: fixed server cfg stale in replication 2024-02-07 11:36:13 +05:30
Maidul Islam
609b224ca9 patch init sign up 2024-02-07 00:33:39 -05:00
Maidul Islam
c23e16105b debug: remove object freeze 2024-02-06 21:08:52 -05:00
Maidul Islam
c10f4ece51 test 2024-02-06 21:08:52 -05:00
vmatsiiako
bcdb1b11bc Update role-based-access-controls.mdx 2024-02-06 13:36:08 -08:00
vmatsiiako
01d850f7e8 Update role-based-access-controls.mdx 2024-02-06 13:35:39 -08:00
Maidul Islam
2d1b60a520 Merge pull request #1362 from akhilmhdh/fix/tsup-cp-template
feat: enabled tsup code splitting and esm directory import, removed manual copy of files
2024-02-06 12:22:59 -05:00
Maidul Islam
8de2302d98 update comment 2024-02-06 12:22:04 -05:00
Maidul Islam
0529b50ad7 Merge pull request #1371 from akhilmhdh/fix/sort-order-ws-env
fix: resolved sort order for environment going unpredictable
2024-02-06 11:41:57 -05:00
Akhil Mohan
c74fe0ca73 fix: resolved sort order for environment going unpredictable 2024-02-06 16:40:31 +05:30
vmatsiiako
d5f8526a84 Update README.md 2024-02-05 17:31:44 -08:00
Maidul Islam
782ae7a41d Update values.yaml 2024-02-05 13:41:02 -05:00
Maidul Islam
d355956daf Merge pull request #1365 from Infisical/pg-ssl
Add Knex SSL configuration support
2024-02-05 12:36:49 -05:00
Maidul Islam
5b9c0438a2 Merge pull request #1367 from Infisical/fix-ph-events
remove certain python sdk events
2024-02-04 16:24:38 -05:00
Maidul Islam
11399d73dc fix eslint errors 2024-02-04 16:24:01 -05:00
Vladyslav Matsiiako
38ed39c2f8 remove certain python sdk events 2024-02-04 09:37:56 -08:00
Tuan Dang
4e3827780f Merge remote-tracking branch 'origin' into pg-ssl 2024-02-03 15:16:47 -08:00
Tuan Dang
644cdf5a67 Add knex SSL configuration support 2024-02-03 15:16:43 -08:00
Akhil Mohan
b3d4787e21 feat: enabled tsup code splitting and esm directory import, removed manual copy of files 2024-02-02 16:22:08 +05:30
78 changed files with 985 additions and 2370 deletions

2
.github/values.yaml vendored
View File

@@ -26,7 +26,7 @@ infisical:
pullPolicy: Always
deploymentAnnotations:
secrets.infisical.com/auto-reload: "false"
secrets.infisical.com/auto-reload: "true"
kubeSecretRef: "infisical-gamma-secrets"

View File

@@ -104,7 +104,6 @@ ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
WORKDIR /
COPY --from=backend-runner /app /backend
COPY --from=backend-runner /app/dist/services/smtp/templates /backend/dist/templates
COPY --from=frontend-runner /app ./backend/frontend-build

View File

@@ -33,7 +33,7 @@
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
</a>
<a href="https://cloudsmith.io/~infisical/repos/">
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
<img src="https://img.shields.io/badge/Downloads-6.95M-orange" alt="Cloudsmith downloads" />
</a>
<a href="https://infisical.com/slack">
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
@@ -53,17 +53,19 @@ We're on a mission to make secret management more accessible to everyone, not ju
## Features
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.)
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like GitHub, Vercel, Netlify, and more
- [**Automatic Kubernetes deployment secret reloads**](https://infisical.com/docs/documentation/getting-started/kubernetes)
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery]()** to version every secret and project state
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
- **Role-based Access Controls** per environment
- [**Simple on-premise deployments** to AWS, Digital Ocean, and more](https://infisical.com/docs/self-hosting/overview)
- [**Secret Scanning and Leak Prevention**](https://infisical.com/docs/cli/scanning-overview)
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
And much more.
@@ -115,9 +117,9 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://cal.com/vmatsiiako/infisical-demo):
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
<a href="https://cal.com/vmatsiiako/infisical-demo"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Security

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "./dist/main.mjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"build": "rimraf dist && tsup && cp -R ./src/lib/validator/disposable_emails.txt ./dist && cp -R ./src/services/smtp/templates ./dist",
"build": "tsup",
"start": "node dist/main.mjs",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
@@ -44,7 +44,13 @@
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
@@ -55,6 +61,7 @@
"prompt-sync": "^4.2.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.8",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.0.1",
"tsx": "^4.4.0",
@@ -80,8 +87,6 @@
"@octokit/webhooks-types": "^7.3.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "^2.2.1",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
@@ -91,9 +96,6 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.1.1",
"dotenv": "^16.3.1",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"fastify": "^4.24.3",
"fastify-plugin": "^4.5.1",
"handlebars": "^4.7.8",
@@ -108,7 +110,6 @@
"nanoid": "^5.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.7",
"ora": "^7.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",

View File

@@ -1,10 +1,18 @@
import knex from "knex";
export type TDbClient = ReturnType<typeof initDbConnection>;
export const initDbConnection = (dbConnectionUri: string) => {
export const initDbConnection = ({ dbConnectionUri, dbRootCert }: { dbConnectionUri: string; dbRootCert?: string }) => {
const db = knex({
client: "pg",
connection: dbConnectionUri
connection: {
connectionString: dbConnectionUri,
ssl: dbRootCert
? {
rejectUnauthorized: true,
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
}
: false
}
});
return db;

View File

@@ -1,37 +0,0 @@
import { Knex } from "knex";
import { ProjectVersion, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "ghost");
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
if (!hasGhostUserColumn) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.boolean("ghost").defaultTo(false).notNullable();
});
}
if (!hasProjectVersionColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("version").defaultTo(ProjectVersion.V1).notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasGhostUserColumn = await knex.schema.hasColumn(TableName.Users, "ghost");
const hasProjectVersionColumn = await knex.schema.hasColumn(TableName.Project, "version");
if (hasGhostUserColumn) {
await knex.schema.alterTable(TableName.Users, (t) => {
t.dropColumn("ghost");
});
}
if (hasProjectVersionColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("version");
});
}
}

View File

@@ -111,11 +111,6 @@ export enum SecretType {
Personal = "personal"
}
export enum ProjectVersion {
V1 = "v1",
V2 = "v2"
}
export enum IdentityAuthMethod {
Univeral = "universal-auth"
}

View File

@@ -14,8 +14,7 @@ export const ProjectsSchema = z.object({
autoCapitalization: z.boolean().default(true).nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
version: z.string().default("v1")
updatedAt: z.date()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -19,8 +19,7 @@ export const UsersSchema = z.object({
mfaMethods: z.string().array().nullable().optional(),
devices: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
ghost: z.boolean().default(false)
updatedAt: z.date()
});
export type TUsers = z.infer<typeof UsersSchema>;

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/no-mutable-exports */
import crypto from "node:crypto";
import argon2, { argon2id } from "argon2";
@@ -15,12 +14,9 @@ import {
import { TUserEncryptionKeys } from "./schemas";
export let userPrivateKey: string | undefined;
export let userPublicKey: string | undefined;
export const seedData1 = {
id: "3dafd81d-4388-432b-a4c5-f735616868c1",
email: process.env.TEST_USER_EMAIL || "test@localhost.local",
email: "test@localhost.local",
password: process.env.TEST_USER_PASSWORD || "testInfisical@1",
organization: {
id: "180870b7-f464-4740-8ffe-9d11c9245ea7",
@@ -37,12 +33,6 @@ export const seedData1 = {
},
token: {
id: "a9dfafba-a3b7-42e3-8618-91abb702fd36"
},
// We set these values during user creation, and later re-use them during project seeding.
encryptionKeys: {
publicKey: "",
privateKey: ""
}
};

View File

@@ -1,14 +1,8 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { Knex } from "knex";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { AuthMethod } from "../../services/auth/auth-type";
import { TableName } from "../schemas";
import { seedData1 } from "../seed-data";
import { generateUserSrpKeys, seedData1 } from "../seed-data";
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
@@ -24,7 +18,6 @@ export async function seed(knex: Knex): Promise<void> {
id: seedData1.id,
email: seedData1.email,
superAdmin: true,
ghost: false,
firstName: "test",
lastName: "",
authMethods: [AuthMethod.EMAIL],
@@ -36,7 +29,7 @@ export async function seed(knex: Knex): Promise<void> {
])
.returning("*");
const encKeys = await generateUserSrpKeys(seedData1.email, seedData1.password);
const encKeys = await generateUserSrpKeys(seedData1.password);
// password: testInfisical@1
await knex(TableName.UserEncryptionKey).insert([
{
@@ -65,9 +58,4 @@ export async function seed(knex: Knex): Promise<void> {
refreshVersion: 1,
lastUsed: new Date()
});
seedData1.encryptionKeys = {
publicKey: encKeys.publicKey,
privateKey: encKeys.plainPrivateKey
};
}

View File

@@ -1,6 +1,3 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas";

View File

@@ -1,15 +1,7 @@
/* eslint-disable simple-import-sort/imports */
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import crypto from "crypto";
import { Knex } from "knex";
import { createSecretBlindIndex, encryptAsymmetric } from "@app/lib/crypto";
import { OrgMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
import { getConfig, initEnvConfig } from "@app/lib/config/env";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
@@ -18,8 +10,6 @@ export const DEFAULT_PROJECT_ENVS = [
];
export async function seed(knex: Knex): Promise<void> {
initEnvConfig();
const appCfg = getConfig();
// Deletes ALL existing entries
await knex(TableName.Project).del();
await knex(TableName.Environment).del();
@@ -31,38 +21,14 @@ export async function seed(knex: Knex): Promise<void> {
orgId: seedData1.organization.id,
slug: "first-project",
// @ts-expect-error exluded type id needs to be inserted here to keep it testable
id: seedData1.project.id,
version: "v1"
id: seedData1.project.id
})
.returning("*");
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
await knex(TableName.SecretBlindIndex).insert({
projectId: project.id,
algorithm: blindIndex.algorithm,
keyEncoding: blindIndex.keyEncoding,
saltIV: blindIndex.iv,
encryptedSaltCipherText: blindIndex.ciphertext,
saltTag: blindIndex.tag
});
const randomBytes = crypto.randomBytes(16).toString("hex"); // Project key
// const encKeys = await generateUserSrpKeys(seedData1.email, seedData1.password); // User keys
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
randomBytes,
seedData1.encryptionKeys.publicKey,
seedData1.encryptionKeys.privateKey
);
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
senderId: seedData1.id,
receiverId: seedData1.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv
});
// await knex(TableName.ProjectKeys).insert({
// projectId: project.id,
// senderId: seedData1.id
// });
await knex(TableName.ProjectMembership).insert({
projectId: project.id,

View File

@@ -79,7 +79,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line
async (req, profile, cb) => {
try {
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
if (!profile) throw new BadRequestError({ message: "Missing profile" });
const { firstName } = profile;
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved

View File

@@ -57,7 +57,6 @@ export const auditLogServiceFactory = ({
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
}
return auditLogQueue.pushToLog(data);
};

View File

@@ -1,14 +1,8 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
SecretApprovalRequestsSecretsSchema,
TableName,
TSecretApprovalRequestsSecrets,
TSecretApprovalRequestsSecretsUpdate,
TSecretTags
} from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { SecretApprovalRequestsSecretsSchema, TableName, TSecretTags } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApprovalRequestSecretDALFactory>;
@@ -17,27 +11,6 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
const bulkUpdateNoVersionIncrement = async (
data: Array<{ filter: Partial<TSecretApprovalRequestsSecrets>; data: TSecretApprovalRequestsSecretsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.SecretApprovalRequestSecret)
.where(filter)
.update(updateData)
.returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const findByRequestId = async (requestId: string, tx?: Knex) => {
try {
const doc = await (tx || db)({
@@ -217,7 +190,6 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
return {
...secretApprovalRequestSecretOrm,
findByRequestId,
bulkUpdateNoVersionIncrement,
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
};
};

View File

@@ -15,9 +15,11 @@ const envSchema = z
PORT: z.coerce.number().default(4000),
REDIS_URL: zpStr(z.string()),
HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
SALT_ROUNDS: z.coerce.number().default(10),
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),
// TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()),
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),

View File

@@ -10,4 +10,3 @@ export {
generateAsymmetricKeyPair
} from "./encryption";
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
export { decodeBase64, encodeBase64 } from "tweetnacl-util";

View File

@@ -1,12 +1,4 @@
import argon2, { argon2id } from "argon2";
import crypto from "crypto";
import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas";
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap
@@ -32,97 +24,3 @@ export const srpCheckClientProof = async (
server.setClientPublicKey(clientPublicKey);
return server.checkClientProof(clientProof);
};
// FOR GHOST USER STUFF
export const generateUserSrpKeys = async (email: string, password: string) => {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
// eslint-disable-next-line
const client = new jsrp.client();
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 argon2.hash(password, {
salt: Buffer.from(salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2id,
raw: true
});
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
} = encryptSymmetric(privateKey, key.toString("base64"));
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = encryptSymmetric(key.toString("hex"), derivedKey.toString("base64"));
return {
protectedKey,
plainPrivateKey: privateKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
};
};
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2id,
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({
ciphertext: user.protectedKey!,
iv: user.protectedKeyIV!,
tag: user.protectedKeyTag!,
key: derivedKey.toString("base64")
});
const privateKey = decryptSymmetric({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
key
});
return privateKey;
};
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {
const randomBytes = crypto.randomBytes(16).toString("hex");
const { nonce, ciphertext } = encryptAsymmetric(randomBytes, publickey, privateKey);
return { nonce, ciphertext };
};

View File

@@ -1,60 +0,0 @@
import crypto from "crypto";
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric } from "../crypto";
type AddUserToWsDTO = {
decryptKey: TProjectKeys & { sender: { publicKey: string } };
userPrivateKey: string;
members: {
orgMembershipId: string;
projectMembershipRole: ProjectMembershipRole;
userPublicKey: string;
}[];
};
export const createWsMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
const key = decryptAsymmetric({
ciphertext: decryptKey.encryptedKey,
nonce: decryptKey.nonce,
publicKey: decryptKey.sender.publicKey,
privateKey: userPrivateKey
});
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
key,
userPublicKey,
userPrivateKey
);
return {
orgMembershipId,
projectRole: projectMembershipRole,
workspaceEncryptedKey: inviteeCipherText,
workspaceEncryptedNonce: inviteeNonce
};
});
return newWsMembers;
};
type TCreateProjectKeyDTO = {
publicKey: string;
privateKey: string;
};
export const createProjectKey = ({ publicKey, privateKey }: TCreateProjectKeyDTO) => {
// 3. Create a random key that we'll use as the project key.
const randomBytes = crypto.randomBytes(16).toString("hex");
// 4. Encrypt the project key with the users key pair.
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
randomBytes,
publicKey,
privateKey
);
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
};

View File

@@ -1,126 +0,0 @@
import { z } from "zod";
import { SecretKeyEncoding, TProjectKeys } from "@app/db/schemas";
import { decryptAsymmetric, decryptSymmetric } from "../crypto";
import { decryptSymmetric128BitHexKeyUTF8, TDecryptSymmetricInput } from "../crypto/encryption";
export enum SecretDocType {
Secret = "secret",
SecretVersion = "secretVersion",
ApprovalSecret = "approvalSecret"
}
const PartialSecretSchema = z.object({
id: z.string(),
secretKeyCiphertext: z.string(),
secretKeyIV: z.string(),
secretKeyTag: z.string(),
secretValueCiphertext: z.string(),
secretValueIV: z.string(),
secretValueTag: z.string(),
secretCommentCiphertext: z.string().nullish(),
secretCommentIV: z.string().nullish(),
secretCommentTag: z.string().nullish(),
docType: z.nativeEnum(SecretDocType),
keyEncoding: z.string()
});
const PartialDecryptedSecretSchema = z.object({
id: z.string(),
secretKey: z.string(),
secretValue: z.string(),
secretComment: z.string().optional(),
docType: z.nativeEnum(SecretDocType)
});
export type TPartialSecret = z.infer<typeof PartialSecretSchema>;
export type TPartialDecryptedSecret = z.infer<typeof PartialDecryptedSecretSchema>;
const symmetricDecrypt = ({
keyEncoding,
ciphertext,
tag,
iv,
key,
isApprovalSecret
}: TDecryptSymmetricInput & { keyEncoding: SecretKeyEncoding; isApprovalSecret: boolean }) => {
if (keyEncoding === SecretKeyEncoding.UTF8 || isApprovalSecret) {
const data = decryptSymmetric128BitHexKeyUTF8({ key, iv, tag, ciphertext });
return data;
}
if (keyEncoding === SecretKeyEncoding.BASE64) {
const data = decryptSymmetric({ key, iv, tag, ciphertext });
return data;
}
throw new Error("Missing both encryption keys");
};
export const decryptSecrets = (
encryptedSecrets: TPartialSecret[],
privateKey: string,
latestKey: TProjectKeys & {
sender: {
publicKey: string;
};
}
) => {
const key = decryptAsymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey
});
const secrets: TPartialDecryptedSecret[] = [];
encryptedSecrets.forEach((encSecret) => {
const secretKey = symmetricDecrypt({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key,
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
});
const secretValue = symmetricDecrypt({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key,
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
});
const secretComment =
encSecret.secretCommentCiphertext && encSecret.secretCommentIV && encSecret.secretCommentTag
? symmetricDecrypt({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key,
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
})
: "";
const decryptedSecret: TPartialDecryptedSecret = {
id: encSecret.id,
secretKey,
secretValue,
secretComment,
docType: encSecret.docType
};
secrets.push(decryptedSecret);
});
return secrets;
};

View File

@@ -12,7 +12,11 @@ dotenv.config();
const run = async () => {
const logger = await initLogger();
const appCfg = initEnvConfig(logger);
const db = initDbConnection(appCfg.DB_CONNECTION_URI);
const db = initDbConnection({
dbConnectionUri: appCfg.DB_CONNECTION_URI,
dbRootCert: appCfg.DB_ROOT_CERT
});
const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(appCfg.REDIS_URL);
@@ -36,7 +40,7 @@ const run = async () => {
port: appCfg.PORT,
host: appCfg.HOST,
listenTextResolver: (address) => {
bootstrap();
void bootstrap();
return address;
}
});

View File

@@ -14,11 +14,10 @@ import fasitfy from "fastify";
import { Knex } from "knex";
import { Logger } from "pino";
import { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { getConfig } from "@lib/config/env";
import { globalRateLimiterCfg } from "./config/rateLimiter";
import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
@@ -40,6 +39,7 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => {
const server = fasitfy({
logger,
trustProxy: true,
connectionTimeout: 30 * 1000,
ignoreTrailingSlash: true
}).withTypeProvider<ZodTypeProvider>();
@@ -75,7 +75,7 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => {
if (appCfg.isProductionMode) {
await server.register(registerExternalNextjs, {
standaloneMode: appCfg.STANDALONE_MODE,
dir: path.join(__dirname, "../"),
dir: path.join(__dirname, "../../"),
port: appCfg.PORT
});
}

View File

@@ -12,9 +12,9 @@ type BootstrapOpt = {
db: Knex;
};
const bootstrapCb = () => {
const bootstrapCb = async () => {
const appCfg = getConfig();
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
if (!serverCfg.initialized) {
console.info(`Welcome to Infisical

View File

@@ -45,6 +45,9 @@ export const registerExternalNextjs = async (
server.route({
method: ["GET", "PUT", "PATCH", "POST", "DELETE"],
url: "/*",
schema: {
hide: true
},
handler: (req, res) =>
nextApp
.getRequestHandler()(req.raw, res.raw)

View File

@@ -43,6 +43,7 @@ export const fastifySwagger = fp(async (fastify) => {
});
await fastify.register(swaggerUI, {
routePrefix: "/docs"
routePrefix: "/api/docs",
prefix: "/api/docs"
});
});

View File

@@ -266,13 +266,19 @@ export const registerRoutes = async (
secretScanningDAL,
secretScanningQueue
});
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
const projectService = projectServiceFactory({
permissionService,
projectDAL,
secretBlindIndexDAL,
projectEnvDAL,
projectMembershipDAL,
folderDAL,
licenseService
});
const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDAL,
projectDAL,
permissionService,
projectBotDAL,
orgDAL,
userDAL,
smtpService,
@@ -280,31 +286,6 @@ export const registerRoutes = async (
projectRoleDAL,
licenseService
});
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDAL,
projectMembershipDAL
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
secretBlindIndexDAL,
identityProjectDAL,
identityOrgMembershipDAL,
projectBotDAL,
secretDAL,
orgDAL,
secretApprovalRequestDAL,
secretApprovalSecretDAL: sarSecretDAL,
projectKeyDAL,
secretVersionDAL,
userDAL,
projectEnvDAL,
orgService,
projectMembershipDAL,
folderDAL,
licenseService
});
const projectEnvService = projectEnvServiceFactory({
permissionService,
projectEnvDAL,
@@ -312,7 +293,11 @@ export const registerRoutes = async (
projectDAL,
folderDAL
});
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDAL,
projectMembershipDAL
});
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
const snapshotService = secretSnapshotServiceFactory({
@@ -349,6 +334,7 @@ export const registerRoutes = async (
secretImportDAL,
secretDAL
});
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
const integrationAuthService = integrationAuthServiceFactory({
integrationAuthDAL,
integrationDAL,
@@ -527,9 +513,9 @@ export const registerRoutes = async (
})
}
},
handler: () => {
handler: async () => {
const cfg = getConfig();
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
return {
date: new Date(),
message: "Ok" as const,

View File

@@ -20,8 +20,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
})
}
},
handler: () => {
const config = getServerCfg();
handler: async () => {
const config = await getServerCfg();
return { config };
}
});
@@ -78,7 +78,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
},
handler: async (req, res) => {
const appCfg = getConfig();
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
if (serverCfg.initialized)
throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" });
const { user, token } = await server.services.superAdmin.adminSignUp({

View File

@@ -1,7 +1,6 @@
import { z } from "zod";
import { ProjectBotsSchema } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -27,16 +26,6 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const project = await server.services.project.getAProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.projectId
});
if (project.version === "v2") {
throw new BadRequestError({ message: "Failed to find bot, project has E2EE disabled" });
}
const bot = await server.services.projectBot.findBotByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
@@ -76,12 +65,6 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const project = await server.services.projectBot.findProjectByBotId(req.params.botId);
if (project?.version === "v2") {
throw new BadRequestError({ message: "Failed to set bot active, project has E2EE disabled" });
}
const bot = await server.services.projectBot.setBotActiveState({
actor: req.permission.type,
actorId: req.permission.id,

View File

@@ -48,7 +48,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter);
},
{ prefix: "/workspace" }
);

View File

@@ -1,12 +1,6 @@
import { z } from "zod";
import {
OrgMembershipsSchema,
ProjectMembershipRole,
ProjectMembershipsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { OrgMembershipsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -77,10 +71,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
members: req.body.members.map((member) => ({
...member,
projectRole: ProjectMembershipRole.Member
}))
members: req.body.members
});
await server.services.auditLog.createAuditLog({

View File

@@ -128,6 +128,32 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/",
method: "POST",
schema: {
body: z.object({
workspaceName: z.string().trim(),
organizationId: z.string().trim()
}),
response: {
200: z.object({
workspace: projectWithEnv
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.createProject({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.body.organizationId,
workspaceName: req.body.workspaceName
});
return { workspace };
}
});
server.route({
url: "/:workspaceId",
method: "DELETE",
@@ -216,7 +242,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
// Is this actually used..?
server.route({
url: "/:workspaceId/invite-signup",
method: "POST",
@@ -229,35 +254,32 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
invitees: UsersSchema.array(),
invitee: UsersSchema,
latestKey: ProjectKeysSchema.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { invitees, latestKey } = await server.services.projectMembership.inviteUserToProject({
const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
emails: [req.body.email]
email: req.body.email
});
for (const invitee of invitees) {
// eslint-disable-next-line no-await-in-loop
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee.id,
email: invitee.email
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.ADD_WORKSPACE_MEMBER,
metadata: {
userId: invitee.id,
email: invitee.email
}
});
}
return { invitees, latestKey };
}
});
return { invitee, latestKey };
}
});

View File

@@ -42,7 +42,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
async (req, _accessToken, _refreshToken, profile, cb) => {
try {
const email = profile?.emails?.[0]?.value;
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
if (!email)
throw new BadRequestError({
message: "Email not found",
@@ -84,7 +84,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
try {
const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email,
firstName: profile.displayName,
@@ -120,7 +120,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try {
const email = profile.emails[0].value;
const serverCfg = getServerCfg();
const serverCfg = await getServerCfg();
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email,
firstName: profile.displayName,

View File

@@ -2,7 +2,6 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router";
import { registerUserRouter } from "./user-router";
@@ -22,7 +21,6 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerIdentityProjectRouter);
await projectServer.register(registerProjectMembershipRouter);
},
{ prefix: "/workspace" }
);

View File

@@ -1,51 +0,0 @@
import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { authRateLimit } from "@app/server/config/rateLimiter";
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/memberships",
config: {
rateLimit: authRateLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
body: z.object({
emails: z.string().email().array()
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.array()
})
}
},
handler: async (req) => {
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
projectId: req.params.projectId,
actorId: req.permission.id,
actor: req.permission.type,
emails: req.body.emails
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
metadata: memberships.map(({ userId, id }) => ({
userId: userId || "",
membershipId: id,
email: ""
}))
}
});
return { memberships };
}
});
};

View File

@@ -1,26 +1,17 @@
import { z } from "zod";
import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas";
import { ProjectKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const projectWithEnv = ProjectsSchema.merge(
z.object({
_id: z.string(),
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
})
);
export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Get project key */
server.route({
url: "/:projectId/encrypted-key",
url: "/:workspaceId/encrypted-key",
method: "GET",
schema: {
params: z.object({
projectId: z.string().trim()
workspaceId: z.string().trim()
}),
response: {
200: ProjectKeysSchema.merge(
@@ -37,12 +28,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
const key = await server.services.projectKey.getLatestProjectKey({
actor: req.permission.type,
actorId: req.permission.id,
projectId: req.params.projectId
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.projectId,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_WORKSPACE_KEY,
metadata: {
@@ -54,60 +45,4 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return key;
}
});
server.route({
url: "/:projectId/upgrade",
method: "POST",
schema: {
params: z.object({
projectId: z.string().trim()
}),
body: z.object({
userPrivateKey: z.string().trim()
}),
response: {
200: z.object({})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
await server.services.project.upgradeProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.projectId,
userPrivateKey: req.body.userPrivateKey
});
}
});
/* Create new project */
server.route({
method: "POST",
url: "/",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
projectName: z.string().trim(),
organizationId: z.string().trim()
}),
response: {
200: z.object({
project: projectWithEnv
})
}
},
handler: async (req) => {
const project = await server.services.project.createProject({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.body.organizationId,
workspaceName: req.body.projectName
});
return { project };
}
});
};

View File

@@ -472,14 +472,16 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
// TODO: Move to telemetry plugin
let shouldRecordK8Event = false;
if (req.headers["user-agent"] === "k8-operator") {
if (req.headers["user-agent"] === "k8-operatoer") {
const randomNumber = Math.random();
if (randomNumber > 0.95) {
shouldRecordK8Event = true;
}
}
const shouldCapture = req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event;
const shouldCapture =
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" &&
(req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event);
const approximateNumberTotalSecrets = secrets.length * 20;
if (shouldCapture) {
server.services.telemetry.sendPostHogEvents({

View File

@@ -224,7 +224,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
if (!user) {
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], ghost: false });
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
}
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const isUserCompleted = user.isAccepted;

View File

@@ -74,8 +74,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ ghost: false }); // MAKE SURE USER IS NOT A GHOST USER
);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
@@ -85,76 +84,6 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
const findOrgMembersByEmail = async (orgId: string, emails: string[]) => {
try {
const members = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.whereIn("email", emails);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
}
};
const findOrgGhostUser = async (orgId: string) => {
try {
const [member] = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
)
.where({ ghost: true });
return member;
} catch (error) {
return null;
}
};
const ghostUserExists = async (orgId: string) => {
try {
const [member] = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
.where({ ghost: true });
return !!member;
} catch (error) {
return false;
}
};
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
try {
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
@@ -252,9 +181,6 @@ export const orgDALFactory = (db: TDbClient) => {
findAllOrgMembers,
findOrgById,
findAllOrgsByUserId,
ghostUserExists,
findOrgMembersByEmail,
findOrgGhostUser,
create,
updateById,
deleteById,

View File

@@ -1,9 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import crypto from "crypto";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { nanoid } from "nanoid";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
@@ -14,7 +11,6 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
@@ -32,7 +28,6 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
import {
TDeleteOrgMembershipDTO,
TFindAllWorkspacesDTO,
TFindOrgMembersByEmailDTO,
TInviteUserToOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
@@ -97,15 +92,6 @@ export const orgServiceFactory = ({
return members;
};
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const members = await orgDAL.findOrgMembersByEmail(orgId, emails);
return members;
};
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
@@ -131,54 +117,6 @@ export const orgServiceFactory = ({
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
};
const addGhostUser = async (orgId: string, tx?: Knex) => {
const email = `ghost@${nanoid(8)}-${orgId}.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
const password = crypto.randomBytes(128).toString("hex");
const user = await userDAL.create(
{
ghost: true,
authMethods: [AuthMethod.EMAIL],
email,
isAccepted: true
},
tx
);
const encKeys = await generateUserSrpKeys(email, password);
await userDAL.upsertUserEncryptionKey(
user.id,
{
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier
},
tx
);
const createMembershipData = {
orgId,
userId: user.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted
};
await orgDAL.createMembership(createMembershipData, tx);
return {
user,
keys: encKeys
};
};
/*
* Update organization settings
* */
@@ -356,8 +294,7 @@ export const orgServiceFactory = ({
{
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL],
ghost: false
authMethods: [AuthMethod.EMAIL]
},
tx
);
@@ -507,12 +444,10 @@ export const orgServiceFactory = ({
inviteUserToOrganization,
verifyUserToOrg,
updateOrgName,
findOrgMembersByEmail,
createOrganization,
deleteOrganizationById,
deleteOrgMembership,
findAllWorkspaces,
addGhostUser,
updateOrgMembership,
// incident contacts
findIncidentContacts,

View File

@@ -25,13 +25,6 @@ export type TVerifyUserToOrgDTO = {
code: string;
};
export type TFindOrgMembersByEmailDTO = {
actor: ActorType;
actorId: string;
orgId: string;
emails: string[];
};
export type TFindAllWorkspacesDTO = {
actor: ActorType;
actorId: string;

View File

@@ -27,19 +27,5 @@ export const projectBotDALFactory = (db: TDbClient) => {
}
};
const findProjectByBotId = async (botId: string) => {
try {
const project = await db(TableName.ProjectBot)
.where({ [`${TableName.ProjectBot}.id` as "id"]: botId })
.join(TableName.Project, `${TableName.ProjectBot}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.Project))
.first();
return project || null;
} catch (error) {
throw new DatabaseError({ error, name: "Find project by bot id" });
}
};
return { ...projectBotOrm, findOne, findProjectByBotId };
return { ...projectBotOrm, findOne };
};

View File

@@ -1,15 +1,22 @@
import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex";
import { SecretKeyEncoding } from "@app/db/schemas";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getConfig } from "@app/lib/config/env";
import {
decryptAsymmetric,
decryptSymmetric,
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair
} from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types";
import { TProjectBotDALFactory } from "./project-bot-dal";
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
import { TSetActiveStateDTO } from "./project-bot-types";
type TProjectBotServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -19,82 +26,101 @@ type TProjectBotServiceFactoryDep = {
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const getBotKey = async (projectId: string) => {
const appCfg = getConfig();
const encryptionKey = appCfg.ENCRYPTION_KEY;
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
const botPrivateKey = getBotPrivateKey({ bot });
if (rootEncryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.BASE64) {
const privateKeyBot = decryptSymmetric({
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey,
key: rootEncryptionKey
});
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: privateKeyBot,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
}
if (encryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.UTF8) {
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: privateKeyBot,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
}
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
throw new BadRequestError({
message: "Failed to obtain bot copy of workspace key needed for operation"
});
};
const findBotByProjectId = async ({
actorId,
actor,
projectId,
privateKey,
publicKey,
botKey
}: TFindBotByProjectIdDTO) => {
const findBotByProjectId = async ({ actorId, actor, projectId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const appCfg = getConfig();
const bot = await projectBotDAL.transaction(async (tx) => {
const doc = await projectBotDAL.findOne({ projectId }, tx);
if (doc) return doc;
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey);
return projectBotDAL.create(
{
name: "Infisical Bot",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: false,
publicKey: keys.publicKey,
algorithm,
keyEncoding: encoding,
...(botKey && {
encryptedProjectKey: botKey.encryptedKey,
encryptedProjectKeyNonce: botKey.nonce
})
},
tx
);
const { publicKey, privateKey } = generateAsymmetricKeyPair();
if (appCfg.ROOT_ENCRYPTION_KEY) {
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
return projectBotDAL.create(
{
name: "Infisical Bot",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: false,
publicKey,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.BASE64
},
tx
);
}
if (appCfg.ENCRYPTION_KEY) {
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY);
return projectBotDAL.create(
{
name: "Infisical Bot",
projectId,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: false,
publicKey,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
}
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
});
return bot;
};
const findProjectByBotId = async (botId: string) => {
try {
const bot = await projectBotDAL.findProjectByBotId(botId);
return bot;
} catch (e) {
throw new BadRequestError({ message: "Failed to find bot by ID" });
}
};
const setBotActiveState = async ({ actor, botId, botKey, actorId, isActive }: TSetActiveStateDTO, tx?: Knex) => {
const setBotActiveState = async ({ actor, botId, botKey, actorId, isActive }: TSetActiveStateDTO) => {
const bot = await projectBotDAL.findById(botId);
if (!bot) throw new BadRequestError({ message: "Bot not found" });
@@ -105,16 +131,12 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
if (!botKey?.nonce || !botKey?.encryptedKey) {
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
}
const doc = await projectBotDAL.updateById(
botId,
{
isActive: true,
encryptedProjectKey: botKey.encryptedKey,
encryptedProjectKeyNonce: botKey.nonce,
senderId: actorId
},
tx
);
const doc = await projectBotDAL.updateById(botId, {
isActive: true,
encryptedProjectKey: botKey.encryptedKey,
encryptedProjectKeyNonce: botKey.nonce,
senderId: actorId
});
if (!doc) throw new BadRequestError({ message: "Failed to update bot active state" });
return doc;
}
@@ -131,8 +153,6 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
return {
findBotByProjectId,
setBotActiveState,
getBotPrivateKey,
findProjectByBotId,
getBotKey
};
};

View File

@@ -1,5 +1,3 @@
// import { SecretKeyEncoding } from "@app/db/schemas";
import { TProjectBots } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TSetActiveStateDTO = {
@@ -10,21 +8,3 @@ export type TSetActiveStateDTO = {
};
botId: string;
} & Omit<TProjectPermission, "projectId">;
export type TFindBotByProjectIdDTO = {
privateKey?: string;
publicKey?: string;
botKey?: {
nonce: string;
encryptedKey: string;
};
} & TProjectPermission;
export type TGetPrivateKeyDTO = {
// encoding: SecretKeyEncoding;
// nonce: string;
// tag: string;
// encryptedPrivateKey: string;
bot: TProjectBots;
};

View File

@@ -1,5 +1,3 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TProjectKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
@@ -12,11 +10,10 @@ export const projectKeyDALFactory = (db: TDbClient) => {
const findLatestProjectKey = async (
userId: string,
projectId: string,
tx?: Knex
projectId: string
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
try {
const projectKey = await (tx || db)(TableName.ProjectKeys)
const projectKey = await db(TableName.ProjectKeys)
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
.where({ projectId, receiverId: userId })
@@ -32,9 +29,9 @@ export const projectKeyDALFactory = (db: TDbClient) => {
}
};
const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => {
const findAllProjectUserPubKeys = async (projectId: string) => {
try {
const pubKeys = await (tx || db)(TableName.ProjectMembership)
const pubKeys = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)

View File

@@ -1,5 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -22,10 +21,14 @@ export const projectKeyServiceFactory = ({
projectMembershipDAL,
permissionService
}: TProjectKeyServiceFactoryDep) => {
const uploadProjectKeys = async (
{ receiverId, actor, actorId, projectId, nonce, encryptedKey }: TUploadProjectKeyDTO,
tx?: Knex
) => {
const uploadProjectKeys = async ({
receiverId,
actor,
actorId,
projectId,
nonce,
encryptedKey
}: TUploadProjectKeyDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
@@ -39,7 +42,7 @@ export const projectKeyServiceFactory = ({
name: "Upload project keys"
});
await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId }, tx);
await projectKeyDAL.create({ projectId, receiverId, encryptedKey, nonce, senderId: actorId });
};
const getLatestProjectKey = async ({ actorId, projectId, actor }: TGetLatestProjectKeyDTO) => {

View File

@@ -1,7 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify } from "@app/lib/knex";
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
@@ -24,37 +24,20 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("role").withSchema(TableName.ProjectMembership),
db.ref("roleId").withSchema(TableName.ProjectMembership),
db.ref("ghost").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId")
);
// .where({ ghost: false });
return members.map(({ email, firstName, lastName, publicKey, ghost, ...data }) => ({
return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: data.userId, publicKey, ghost }
user: { email, firstName, lastName, id: data.userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all project members" });
}
};
const findProjectGhostUser = async (projectId: string) => {
try {
const ghostUser = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.where({ ghost: true })
.first();
return ghostUser;
} catch (error) {
throw new DatabaseError({ error, name: "Find project ghost user" });
}
};
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser };
return { ...projectMemberOrm, findAllProjectMembers };
};

View File

@@ -1,26 +1,15 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import {
OrgMembershipStatus,
ProjectMembershipRole,
SecretKeyEncoding,
TableName,
TProjectMemberships,
TUsers
} from "@app/db/schemas";
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { createWsMembers } from "@app/lib/project";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
@@ -28,7 +17,6 @@ import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import {
TAddUsersToWorkspaceDTO,
TAddUsersToWorkspaceNonE2EEDTO,
TDeleteProjectMembershipDTO,
TGetProjectMembershipDTO,
TInviteUserToProjectDTO,
@@ -38,12 +26,11 @@ import {
type TProjectMembershipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
smtpService: TSmtpService;
projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: TProjectMembershipDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
projectDAL: Pick<TProjectDALFactory, "findById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@@ -55,7 +42,6 @@ export const projectMembershipServiceFactory = ({
projectMembershipDAL,
smtpService,
projectRoleDAL,
projectBotDAL,
orgDAL,
userDAL,
projectDAL,
@@ -69,74 +55,64 @@ export const projectMembershipServiceFactory = ({
return projectMembershipDAL.findAllProjectMembers(projectId);
};
const inviteUserToProject = async ({ actorId, actor, projectId, emails }: TInviteUserToProjectDTO) => {
const inviteUserToProject = async ({ actorId, actor, projectId, email }: TInviteUserToProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const invitees: TUsers[] = [];
for (const email of emails) {
const invitee = await userDAL.findOne({ email });
if (!invitee || !invitee.isAccepted)
throw new BadRequestError({
message: "Failed to validate invitee",
name: "Invite user to project"
});
const inviteeMembership = await projectMembershipDAL.findOne({
userId: invitee.id,
projectId
});
if (inviteeMembership)
throw new BadRequestError({
message: "Existing member of project",
name: "Invite user to project"
});
const project = await projectDAL.findById(projectId);
const inviteeMembershipOrg = await orgDAL.findMembership({
userId: invitee.id,
orgId: project.orgId,
status: OrgMembershipStatus.Accepted
});
if (!inviteeMembershipOrg)
throw new BadRequestError({
message: "Failed to validate invitee org membership",
name: "Invite user to project"
});
await projectMembershipDAL.create({
userId: invitee.id,
projectId,
role: ProjectMembershipRole.Member
const invitee = await userDAL.findOne({ email });
if (!invitee || !invitee.isAccepted)
throw new BadRequestError({
message: "Faield to validate invitee",
name: "Invite user to project"
});
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: [invitee.email],
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
const inviteeMembership = await projectMembershipDAL.findOne({
userId: invitee.id,
projectId
});
if (inviteeMembership)
throw new BadRequestError({
message: "Existing member of project",
name: "Invite user to project"
});
invitees.push(invitee);
}
const project = await projectDAL.findById(projectId);
const inviteeMembershipOrg = await orgDAL.findMembership({
userId: invitee.id,
orgId: project.orgId,
status: OrgMembershipStatus.Accepted
});
if (!inviteeMembershipOrg)
throw new BadRequestError({
message: "Failed to validate invitee org membership",
name: "Invite user to project"
});
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
await projectMembershipDAL.create({
userId: invitee.id,
projectId,
role: ProjectMembershipRole.Member
});
return { invitees, latestKey };
const sender = await userDAL.findById(actorId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: [invitee.email],
substitutions: {
inviterFirstName: sender.firstName,
inviterEmail: sender.email,
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
return { invitee, latestKey };
};
const addUsersToProject = async ({
projectId,
actorId,
actor,
members,
sendEmails = true
}: TAddUsersToWorkspaceDTO) => {
const addUsersToProject = async ({ projectId, actorId, actor, members }: TAddUsersToWorkspaceDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
@@ -158,16 +134,11 @@ export const projectMembershipServiceFactory = ({
await projectMembershipDAL.transaction(async (tx) => {
await projectMembershipDAL.insertMany(
orgMembers.map(({ userId, id: membershipId }) => {
const role =
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
return {
projectId,
userId: userId as string,
role
};
}),
orgMembers.map(({ userId }) => ({
projectId,
userId: userId as string,
role: ProjectMembershipRole.Member
})),
tx
);
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
@@ -182,133 +153,22 @@ export const projectMembershipServiceFactory = ({
tx
);
});
if (sendEmails) {
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
const sender = await userDAL.findById(actorId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
substitutions: {
inviterFirstName: sender.firstName,
inviterEmail: sender.email,
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
return orgMembers;
};
const addUsersToProjectNonE2EE = async ({
projectId,
actorId,
actor,
emails,
sendEmails = true
}: TAddUsersToWorkspaceNonE2EEDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails);
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" });
const existingMembers = await projectMembershipDAL.find({
projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find ghost user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find ghost user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = createWsMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Member,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
await projectMembershipDAL.transaction(async (tx) => {
const result = await projectMembershipDAL.insertMany(
orgMembers.map(({ user, id: membershipId }) => {
const role =
orgMembers.find((membership) => membership.id === membershipId)?.role || ProjectMembershipRole.Member;
return {
projectId,
userId: user.id,
role
};
}),
tx
);
members.push(...result);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
});
if (sendEmails) {
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
return members;
};
const updateProjectMembership = async ({
actorId,
actor,
@@ -319,15 +179,6 @@ export const projectMembershipServiceFactory = ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
if (membershipUser?.ghost) {
throw new BadRequestError({
message: "Unauthorized member update",
name: "Update project membership"
});
}
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
if (isCustomRole) {
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
@@ -357,15 +208,6 @@ export const projectMembershipServiceFactory = ({
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
if (membershipUser?.ghost) {
throw new BadRequestError({
message: "Cannot delete ghost",
name: "Delete project membership"
});
}
const membership = await projectMembershipDAL.transaction(async (tx) => {
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
@@ -378,7 +220,6 @@ export const projectMembershipServiceFactory = ({
getProjectMemberships,
inviteUserToProject,
updateProjectMembership,
addUsersToProjectNonE2EE,
deleteProjectMembership,
addUsersToProject
};

View File

@@ -1,10 +1,9 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export type TInviteUserToProjectDTO = {
emails: string[];
email: string;
} & TProjectPermission;
export type TUpdateProjectMembershipDTO = {
@@ -17,16 +16,9 @@ export type TDeleteProjectMembershipDTO = {
} & TProjectPermission;
export type TAddUsersToWorkspaceDTO = {
sendEmails?: boolean;
members: {
orgMembershipId: string;
workspaceEncryptedKey: string;
workspaceEncryptedNonce: string;
projectRole: ProjectMembershipRole;
}[];
} & TProjectPermission;
export type TAddUsersToWorkspaceNonE2EEDTO = {
sendEmails?: boolean;
emails: string[];
} & TProjectPermission;

View File

@@ -21,7 +21,11 @@ export const projectDALFactory = (db: TDbClient) => {
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.orderBy("createdAt", "asc", "last");
.orderBy([
{ column: `${TableName.Project}.name`, order: "asc" },
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const nestedWorkspaces = sqlNestRelationships({
data: workspaces,
key: "id",
@@ -48,20 +52,6 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findProjectGhostUser = async (projectId: string) => {
try {
const ghostUser = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users))
.where({ ghost: true })
.first();
return ghostUser;
} catch (error) {
throw new DatabaseError({ error, name: "Find project ghost user" });
}
};
const findAllProjectsByIdentity = async (identityId: string) => {
try {
const workspaces = await db(TableName.IdentityProjectMembership)
@@ -116,7 +106,11 @@ export const projectDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
);
)
.orderBy([
{ column: `${TableName.Project}.name`, order: "asc" },
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
return sqlNestRelationships({
data: workspaces,
key: "id",
@@ -142,7 +136,6 @@ export const projectDALFactory = (db: TDbClient) => {
...projectOrm,
findAllProjects,
findAllProjectsByIdentity,
findProjectGhostUser,
findProjectById
};
};

View File

@@ -1,46 +1,22 @@
/* eslint-disable no-console */
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TSecrets } from "@app/db/schemas";
import { ProjectMembershipRole } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
import {
decryptAsymmetric,
encryptSymmetric128BitHexKeyUTF8,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { createProjectKey, createWsMembers } from "@app/lib/project";
import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, TUpgradeProjectDTO } from "./project-types";
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
@@ -50,22 +26,11 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
userDAL: TUserDALFactory;
folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
secretVersionDAL: TSecretVersionDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
orgDAL: TOrgDALFactory;
secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
secretApprovalSecretDAL: TSecretApprovalRequestSecretDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
secretDAL: TSecretDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@@ -73,19 +38,8 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
export const projectServiceFactory = ({
projectDAL,
projectKeyDAL,
secretApprovalRequestDAL,
secretApprovalSecretDAL,
permissionService,
userDAL,
folderDAL,
orgService,
orgDAL,
identityProjectDAL,
secretVersionDAL,
projectBotDAL,
identityOrgMembershipDAL,
secretDAL,
secretBlindIndexDAL,
projectMembershipDAL,
projectEnvDAL,
@@ -95,7 +49,7 @@ export const projectServiceFactory = ({
* Create workspace. Make user the admin
* */
const createProject = async ({ orgId, actor, actorId, workspaceName }: TCreateProjectDTO) => {
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(actor, actorId, orgId);
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
const appCfg = getConfig();
@@ -110,28 +64,20 @@ export const projectServiceFactory = ({
});
}
const results = await projectDAL.transaction(async (tx) => {
const ghostUser = await orgService.addGhostUser(orgId, tx);
const newProject = projectDAL.transaction(async (tx) => {
const project = await projectDAL.create(
{
name: workspaceName,
orgId,
slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
version: ProjectVersion.V2
},
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
tx
);
// set ghost user as admin of project
// set user as admin member for proeject
await projectMembershipDAL.create(
{
userId: ghostUser.user.id,
userId: actorId,
role: ProjectMembershipRole.Admin,
projectId: project.id
},
tx
);
// generate the blind index for project
await secretBlindIndexDAL.create(
{
@@ -153,154 +99,11 @@ export const projectServiceFactory = ({
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
// 3. Create a random key that we'll use as the project key.
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: ghostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
// 5. Create & a bot for the project
await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
// Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
// If the project is being created by a user, add the user to the project as an admin
if (actor === ActorType.USER) {
// Find public key of user
const user = await userDAL.findUserEncKeyByUserId(actorId);
if (!user) {
throw new Error("User not found");
}
const [projectAdmin] = createWsMembers({
decryptKey: latestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id,
projectMembershipRole: ProjectMembershipRole.Admin
}
]
});
// Create a membership for the user
await projectMembershipDAL.create(
{
projectId: project.id,
userId: user.id,
role: projectAdmin.projectRole
},
tx
);
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.user.id,
receiverId: user.id,
projectId: project.id
},
tx
);
}
// If the project is being created by an identity, add the identity to the project as an admin
else if (actor === ActorType.IDENTITY) {
// Find identity org membership
const identityOrgMembership = await identityOrgMembershipDAL.findOne(
{
identityId: actorId,
orgId: project.orgId
},
tx
);
// If identity org membership not found, throw error
if (!identityOrgMembership) {
throw new BadRequestError({
message: `Failed to find identity with id ${actorId}`
});
}
// Get the role permission for the identity
// IS THIS CORRECT?
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
ProjectMembershipRole.Admin,
orgId
);
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
await identityProjectDAL.create(
{
identityId: actorId,
projectId: project.id,
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
roleId: customRole?.id
},
tx
);
}
return {
...project,
environments: envs,
_id: project.id
};
// _id for backward compat
return { ...project, environments: envs, _id: project.id };
});
return results;
};
const findProjectGhostUser = async (projectId: string) => {
const user = await projectMembershipDAL.findProjectGhostUser(projectId);
return user;
return newProject;
};
const deleteProject = async ({ actor, actorId, projectId }: TDeleteProjectDTO) => {
@@ -342,361 +145,12 @@ export const projectServiceFactory = ({
return updatedProject;
};
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
if (membership?.role !== ProjectMembershipRole.Admin) {
throw new ForbiddenRequestError({
message: "User must be admin"
});
}
/*
1. Get the existing project
2. Get the existing project keys
4. Get all the project envs & folders
5. Get ALL secrets within the project
6. Create a new ghost user
7. Create a project membership for the ghost user
8. Get the existing bot, and the existing project keys for the members of the project
9. IF a bot already exists for the project, delete it!
10. Delete all the existing project keys
11. Create a project key for the ghost user
12. Find the newly created ghost user's latest key
FOR EACH OF THE OLD PROJECT KEYS (loop):
13. Find the user based on the key.receiverId.
14. Find the org membership for the user.
15. Create a new project key for the user.
16. Encrypt the ghost user's private key
17. Create a new bot, and set the public/private key of the bot, to the ghost user's public/private key.
18. Add the workspace key to the bot
19. Decrypt the secrets with the old project key
20. Get the newly created bot's private key, and workspace key (we do it this way to test as many steps of the bot process as possible)
21. Get the workspace key from the bot
FOR EACH DECRYPTED SECRET (loop):
22. Re-encrypt the secret value, secret key, and secret comment with the NEW project key from the bot.
23. Update the secret in the database with the new encrypted values.
24. Transaction ends. If there were no errors. All changes are applied.
25. API route returns 200 OK.
*/
const project = await projectDAL.findOne({ id: projectId, version: ProjectVersion.V1 });
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
if (!project || !oldProjectKey) {
throw new BadRequestError({
message: "Project or project key not found"
});
}
const projectEnvs = await projectEnvDAL.find({
projectId: project.id
});
console.log(
"projectEnvs",
projectEnvs.map((e) => e.name)
);
const projectFolders = await folderDAL.find({
$in: {
envId: projectEnvs.map((env) => env.id)
}
});
// Get all the secrets within the project (as encrypted)
const secrets: TPartialSecret[] = [];
for (const folder of projectFolders) {
const folderSecrets = await secretDAL.find({ folderId: folder.id });
const folderSecretVersions = await secretVersionDAL.find({
folderId: folder.id
});
const approvalRequests = await secretApprovalRequestDAL.find({
status: RequestState.Open,
folderId: folder.id
});
const approvalSecrets = await secretApprovalSecretDAL.find({
$in: {
requestId: approvalRequests.map((el) => el.id)
}
});
secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret })));
secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion })));
secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret })));
}
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
if (secrets.length !== decryptedSecrets.length) {
throw new Error("Failed to decrypt some secret versions");
}
// Get the existing bot and the existing project keys for the members of the project
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
// TRANSACTION START
await projectDAL.transaction(async (tx) => {
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
// Create a ghost user
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
// Create a project key
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
console.log("Creating new project key for ghost user");
// Create a new project key for the GHOST
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: ghostUser.user.id,
encryptedKey: newEncryptedProjectKey,
nonce: newEncryptedProjectKeyIv,
senderId: ghostUser.user.id
},
tx
);
// Create a membership for the ghost user
await projectMembershipDAL.create(
{
projectId: project.id,
userId: ghostUser.user.id,
role: ProjectMembershipRole.Admin
},
tx
);
// If a bot already exists, delete it
if (existingBot) {
console.log("Deleting existing bot");
await projectBotDAL.delete({ id: existingBot.id }, tx);
}
console.log("Deleting old project keys");
// Delete all the existing project keys
await projectKeyDAL.delete(
{
projectId: project.id,
$in: {
id: existingProjectKeys.map((key) => key.id)
}
},
tx
);
console.log("Finding latest key for ghost user");
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
if (!ghostUserLatestKey) {
throw new Error("User latest key not found (V2 Upgrade)");
}
console.log("Creating new project keys for old members");
const newProjectMembers: {
encryptedKey: string;
nonce: string;
senderId: string;
receiverId: string;
projectId: string;
}[] = [];
for (const key of existingProjectKeys) {
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
if (!user || !orgMembership) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
}
const [newMember] = createWsMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id,
projectMembershipRole: ProjectMembershipRole.Admin
}
]
});
newProjectMembers.push({
encryptedKey: newMember.workspaceEncryptedKey,
nonce: newMember.workspaceEncryptedNonce,
senderId: ghostUser.user.id,
receiverId: user.id,
projectId: project.id
});
}
// Create project keys for all the old members
await projectKeyDAL.insertMany(newProjectMembers, tx);
// Encrypt the bot private key (which is the same as the ghost user)
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
// 5. Create a bot for the project
const newBot = await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
encryptedProjectKey: newEncryptedProjectKey,
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
algorithm,
keyEncoding: encoding
},
tx
);
console.log("Updating secrets with new project key");
console.log("Got decrypted secrets");
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
iv: newBot.iv,
tag: newBot.tag,
ciphertext: newBot.encryptedPrivateKey
});
const botKey = decryptAsymmetric({
ciphertext: newBot.encryptedProjectKey!,
privateKey: botPrivateKey,
nonce: newBot.encryptedProjectKeyNonce!,
publicKey: ghostUser.keys.publicKey
});
type TPartialSecret = Pick<
TSecrets,
| "id"
| "secretKeyCiphertext"
| "secretKeyIV"
| "secretKeyTag"
| "secretValueCiphertext"
| "secretValueIV"
| "secretValueTag"
| "secretCommentCiphertext"
| "secretCommentIV"
| "secretCommentTag"
>;
const updatedSecrets: TPartialSecret[] = [];
const updatedSecretVersions: TPartialSecret[] = [];
const updatedSecretApprovals: TPartialSecret[] = [];
for (const rawSecret of decryptedSecrets) {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey);
const payload = {
id: rawSecret.id,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
} as const;
if (rawSecret.docType === SecretDocType.Secret) {
updatedSecrets.push(payload);
} else if (rawSecret.docType === SecretDocType.SecretVersion) {
updatedSecretVersions.push(payload);
} else if (rawSecret.docType === SecretDocType.ApprovalSecret) {
updatedSecretApprovals.push(payload);
} else {
throw new Error("Unknown secret type");
}
}
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(
[
...updatedSecrets.map((secret) => ({
filter: { id: secret.id },
data: {
...secret,
id: undefined
}
}))
],
tx
);
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(
[
...updatedSecretVersions.map((version) => ({
filter: { id: version.id },
data: {
...version,
id: undefined
}
}))
],
tx
);
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
[
...updatedSecretApprovals.map((approval) => ({
filter: {
id: approval.id
},
data: {
...approval,
id: undefined
}
}))
],
tx
);
if (secretUpdates.length !== updatedSecrets.length) {
throw new Error("Failed to update some secrets");
}
if (secretVersionUpdates.length !== updatedSecretVersions.length) {
throw new Error("Failed to update some secret versions");
}
if (secretApprovalUpdates.length !== updatedSecretApprovals.length) {
throw new Error("Failed to update some secret approvals");
}
throw new Error("Transaction was successful");
});
};
return {
createProject,
deleteProject,
getProjects,
findProjectGhostUser,
getAProject,
toggleAutoCapitalization,
updateName,
upgradeProject
updateName
};
};

View File

@@ -1,5 +1,3 @@
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
export type TCreateProjectDTO = {
@@ -20,7 +18,3 @@ export type TGetProjectDTO = {
actorId: string;
projectId: string;
};
export type TUpgradeProjectDTO = {
userPrivateKey: string;
} & TProjectPermission;

View File

@@ -22,11 +22,7 @@ export const secretDALFactory = (db: TDbClient) => {
// the idea is to use postgres specific function
// insert with id this will cause a conflict then merge the data
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
tx?: Knex
) => {
const bulkUpdate = async (data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>, tx?: Knex) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
@@ -45,24 +41,6 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
const bulkUpdateNoVersionIncrement = async (
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.Secret).where(filter).update(updateData).returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const deleteMany = async (
data: Array<{ blindIndex: string; type: SecretType }>,
folderId: string,
@@ -161,13 +139,5 @@ export const secretDALFactory = (db: TDbClient) => {
}
};
return {
...secretOrm,
update,
bulkUpdate,
deleteMany,
bulkUpdateNoVersionIncrement,
findByFolderId,
findByBlindIndexes
};
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
};

View File

@@ -537,18 +537,30 @@ export const secretServiceFactory = ({
const secretBlindIndex = await interalGenSecBlindIndexByName(projectId, secretName);
const secret = await (typeof version !== undefined
// Case: The old python SDK uses incorrect logic https://github.com/Infisical/infisical-python/blob/main/infisical/client/infisicalclient.py#L89.
// Fetch secrets using service tokens cannot fetch personal secrets, only shared.
// The mongo backend used to correct this mistake, this line also adds it to current backend
// Mongo backend check: https://github.com/Infisical/infisical-mongo/blob/main/backend/src/helpers/secrets.ts#L658
let secretType = type;
if (actor === ActorType.SERVICE) {
logger.info(
`secretServiceFactory: overriding secret type for service token [projectId=${projectId}] [factoryFunctionName=getSecretByName]`
);
secretType = SecretType.Shared;
}
const secret = await (typeof version === undefined
? secretDAL.findOne({
folderId,
type,
userId: type === SecretType.Personal ? actorId : null,
type: secretType,
userId: secretType === SecretType.Personal ? actorId : null,
secretBlindIndex
})
: secretVersionDAL
.findOne({
folderId,
type,
userId: type === SecretType.Personal ? actorId : null,
type: secretType,
userId: secretType === SecretType.Personal ? actorId : null,
secretBlindIndex
})
.then((el) => SecretsSchema.parse({ ...el, id: el.secretId })));

View File

@@ -1,8 +1,8 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { TableName, TSecretVersions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
@@ -36,46 +36,6 @@ export const secretVersionDALFactory = (db: TDbClient) => {
}
};
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.SecretVersion)
.where(filter)
.update(updateData)
// .increment("version", 1) // TODO: Is this really needed?
.returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const bulkUpdateNoVersionIncrement = async (
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.SecretVersion).where(filter).update(updateData).returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
try {
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
@@ -99,11 +59,5 @@ export const secretVersionDALFactory = (db: TDbClient) => {
}
};
return {
...secretVersionOrm,
findLatestVersionMany,
bulkUpdate,
findLatestVersionByFolderId,
bulkUpdateNoVersionIncrement
};
return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
};

View File

@@ -1,15 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Project Invitation</title>
</head>
<body>
</head>
<body>
<h2>Join your team on Infisical</h2>
<p>You have been invited to a new Infisical project — {{workspaceName}}</p>
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
</body>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body>
</html>

View File

@@ -1,4 +1,5 @@
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
@@ -17,11 +18,8 @@ type TSuperAdminServiceFactoryDep = {
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
let serverCfg: Readonly<TSuperAdmin>;
export const getServerCfg = () => {
if (!serverCfg) throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" });
return serverCfg;
};
// eslint-disable-next-line
export let getServerCfg: () => Promise<TSuperAdmin>;
export const superAdminServiceFactory = ({
serverCfgDAL,
@@ -30,19 +28,18 @@ export const superAdminServiceFactory = ({
orgService
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
serverCfg = await serverCfgDAL.findOne({});
if (!serverCfg) {
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true });
serverCfg = newCfg;
return newCfg;
}
return serverCfg;
// TODO(akhilmhdh): bad pattern time less change this later to me itself
getServerCfg = () => serverCfgDAL.findOne({});
const serverCfg = await serverCfgDAL.findOne({});
if (serverCfg) return;
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true });
return newCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate) => {
const serverCfg = await getServerCfg();
const cfg = await serverCfgDAL.updateById(serverCfg.id, data);
serverCfg = cfg;
Object.freeze(serverCfg);
return cfg;
};
@@ -62,6 +59,7 @@ export const superAdminServiceFactory = ({
ip,
userAgent
}: TAdminSignUpDTO) => {
const appCfg = getConfig();
const existingUser = await userDAL.findOne({ email });
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
@@ -95,7 +93,10 @@ export const superAdminServiceFactory = ({
);
return { user: newUser, enc: userEnc };
});
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, "Admin Org");
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName);
await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens(userInfo.user, ip, userAgent);

View File

@@ -47,17 +47,6 @@ export const userDALFactory = (db: TDbClient) => {
}
};
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
try {
return await db(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.first();
} catch (error) {
throw new DatabaseError({ error, name: "Find user by project membership id" });
}
};
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
try {
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
@@ -122,7 +111,6 @@ export const userDALFactory = (db: TDbClient) => {
findUserEncKeyByEmail,
findUserEncKeyByUserId,
updateUserEncryptionByUserId,
findUserByProjectMembershipId,
upsertUserEncryptionKey,
createUserEncryption,
findOneUserAction,

View File

@@ -22,11 +22,9 @@
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@app/*": ["./src/*"],
"@lib/*": ["./src/lib/*"],
"@server/*": ["./src/server/*"]
"@app/*": ["./src/*"]
}
},
"include": ["src/**/*", "scripts/**/*", "e2e-test/**/*","./.eslintrc.js"],
"include": ["src/**/*", "scripts/**/*", "e2e-test/**/*", "./.eslintrc.js", "./tsup.config.js"],
"exclude": ["node_modules"]
}

View File

@@ -1,14 +1,73 @@
/* eslint-disable */
import path from "node:path";
import fs from "fs/promises";
import { replaceTscAliasPaths } from "tsc-alias";
import { defineConfig } from "tsup";
// Instead of using tsx or tsc for building, consider using tsup.
// TSX serves as an alternative to Node.js, allowing you to build directly on the Node.js runtime.
// Its functionality mirrors Node.js, with the only difference being the absence of a final build step. Production should ideally be launched with TSX.
// TSC is effective for creating a final build, but it requires manual copying of all static files such as handlebars, emails, etc.
// A significant challenge is the shift towards ESM, as more packages are adopting ESM. If the output is in CommonJS, it may lead to errors.
// The suggested configuration offers a balance, accommodating both ESM and CommonJS requirements.
export default defineConfig({
shims: true,
clean: true,
minify: false,
keepNames: true,
splitting: false,
format: "esm",
// copy the files to output
loader: {
".handlebars": "copy",
".md": "copy"
".md": "copy",
".txt": "copy"
},
external: ["../../../frontend/node_modules/next/dist/server/next-server.js"],
outDir: "dist",
tsconfig: "./tsconfig.json",
entry: ["./src"],
sourceMap: "inline"
sourceMap: true,
skipNodeModulesBundle: true,
esbuildPlugins: [
{
// esm directory import are not allowed
// /folder1 should be explicitly imported as /folder1/index.ts
// this plugin will append it automatically on build time to all imports
name: "commonjs-esm-directory-import",
setup(build) {
build.onResolve({ filter: /.*/ }, async (args) => {
if (args.importer) {
if (args.kind === "import-statement") {
const isRelativePath = args.path.startsWith(".");
const absPath = isRelativePath
? path.join(args.resolveDir, args.path)
: path.join(args.path.replace("@app", "./src"));
const isFile = await fs
.stat(`${absPath}.ts`)
.then((el) => el.isFile)
.catch((err) => err.code === "ENOTDIR");
return {
path: isFile ? `${args.path}.mjs` : `${args.path}/index.mjs`,
external: true
};
}
}
return undefined;
});
}
}
],
async onSuccess() {
// this will replace all tsconfig paths
await replaceTscAliasPaths({
configFile: "tsconfig.json",
watch: false,
outDir: "dist"
});
}
});

View File

@@ -1,6 +1,6 @@
---
title: "Role-based Access Controls"
description: "Infisical's Role-based Acccess Controls enable creating permissions for user and machine identities to restrict access to resources and the range of actions that can performed."
description: "Infisical's Role-based Access Controls enable creating permissions for user and machine identities to restrict access to resources and the range of actions that can be performed."
---
### General access controls

View File

@@ -13,12 +13,12 @@ nacl.util = require("tweetnacl-util");
*/
const generateKeyPair = () => {
const pair = nacl.box.keyPair();
return {
publicKey: nacl.util.encodeBase64(pair.publicKey),
privateKey: nacl.util.encodeBase64(pair.secretKey)
};
};
return ({
publicKey: nacl.util.encodeBase64(pair.publicKey),
privateKey: nacl.util.encodeBase64(pair.secretKey)
});
}
type EncryptAsymmetricProps = {
plaintext: string;
@@ -29,19 +29,27 @@ type EncryptAsymmetricProps = {
/**
* Verify that private key [privateKey] is the one that corresponds to
* the public key [publicKey]
* @param {Object}
* @param {Object}
* @param {String} - base64-encoded Nacl private key
* @param {String} - base64-encoded Nacl public key
*/
const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
const verifyPrivateKey = ({
privateKey,
publicKey
}: {
privateKey: string;
publicKey: string;
}) => {
const derivedPublicKey = nacl.util.encodeBase64(
nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey
nacl.box.keyPair.fromSecretKey(
nacl.util.decodeBase64(privateKey)
).publicKey
);
if (derivedPublicKey !== publicKey) {
throw new Error("Failed to verify private key");
}
};
}
/**
* Derive a key from password [password] and salt [salt] using Argon2id
@@ -221,8 +229,7 @@ export {
decryptAssymmetric,
decryptSymmetric,
deriveArgonKey,
encryptAssymmetric,
encryptAssymmetric,
encryptSymmetric,
generateKeyPair,
verifyPrivateKey
};
verifyPrivateKey};

View File

@@ -56,6 +56,7 @@ const encryptSecrets = async ({
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
} else {
// case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex");
@@ -115,6 +116,7 @@ const encryptSecrets = async ({
return result;
});
} catch (error) {
console.log("Error while encrypting secrets");
}

View File

@@ -8,9 +8,9 @@ const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
};
export const fetchUserWsKey = async (projectId: string) => {
export const fetchUserWsKey = async (workspaceID: string) => {
const { data } = await apiRequest.get<UserWsKeyPair>(
`/api/v2/workspace/${projectId}/encrypted-key`
`/api/v2/workspace/${workspaceID}/encrypted-key`
);
return data;

View File

@@ -22,7 +22,7 @@ export * from "./secrets/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
export type { SubscriptionPlan } from "./subscriptions/types";
export type { WsTag } from "./tags/types";
export type { AddUserToWsDTOE2EE, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
export type { AddUserToWsDTO, OrgUser, TWorkspaceUser, User, UserEnc } from "./users/types";
export type { TWebhook } from "./webhooks/types";
export type {
CreateEnvironmentDTO,

View File

@@ -1,4 +1,4 @@
export { useAddUserToWsE2EE, useAddUserToWsNonE2EE } from "./mutation";
export { useAddUserToWs } from "./mutation";
export {
fetchOrgUsers,
useAddUserToOrg,

View File

@@ -7,12 +7,12 @@ import {
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
import { AddUserToWsDTO } from "./types";
export const useAddUserToWsE2EE = () => {
export const useAddUserToWs = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToWsDTOE2EE>({
return useMutation<{}, {}, AddUserToWsDTO>({
mutationFn: async ({ workspaceId, members, decryptKey, userPrivateKey }) => {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
@@ -45,19 +45,3 @@ export const useAddUserToWsE2EE = () => {
}
});
};
export const useAddUserToWsNonE2EE = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToWsDTONonE2EE>({
mutationFn: async ({ projectId, emails }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${projectId}/memberships`, {
emails
});
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId));
}
});
};

View File

@@ -63,7 +63,7 @@ export type TProjectMembership = {
export type TWorkspaceUser = OrgUser;
export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTO = {
workspaceId: string;
decryptKey: UserWsKeyPair;
userPrivateKey: string;
@@ -73,11 +73,6 @@ export type AddUserToWsDTOE2EE = {
}[];
};
export type AddUserToWsDTONonE2EE = {
projectId: string;
emails: string[];
};
export type UpdateOrgUserRoleDTO = {
organizationId: string;
membershipId: string;

View File

@@ -21,6 +21,5 @@ export {
useToggleAutoCapitalization,
useUpdateIdentityWorkspaceRole,
useUpdateUserWorkspaceRole,
useUpdateWsEnvironment,
useUpgradeProject
useUpdateWsEnvironment
} from "./queries";

View File

@@ -61,21 +61,6 @@ export const fetchWorkspaceSecrets = async (workspaceId: string) => {
return secrets;
};
export const useUpgradeProject = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { projectId: string; privateKey: string }>({
mutationFn: ({ projectId, privateKey }) => {
return apiRequest.post(`/api/v2/workspace/${projectId}/upgrade`, {
userPrivateKey: privateKey
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
return data.workspaces;
@@ -173,19 +158,19 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
export const createWorkspace = ({
organizationId,
projectName
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName, organizationId });
workspaceName
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ organizationId, projectName }) =>
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ organizationId, workspaceName }) =>
createWorkspace({
organizationId,
projectName
workspaceName
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
@@ -300,13 +285,11 @@ export const useAddUserToWorkspace = () => {
return useMutation({
mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => {
const {
data: { invitees, latestKey }
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, {
emails: [email]
});
data: { invitee, latestKey }
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
return {
invitees,
invitee,
latestKey
};
},

View File

@@ -3,10 +3,8 @@ export type Workspace = {
id: string;
name: string;
orgId: string;
version: "v1" | "v2";
autoCapitalization: boolean;
environments: WorkspaceEnv[];
slug: string;
};
export type WorkspaceEnv = {
@@ -27,7 +25,7 @@ export type NameWorkspaceSecretsDTO = {
// mutation dto
export type CreateWorkspaceDTO = {
projectName: string;
workspaceName: string;
organizationId: string;
};

View File

@@ -4,6 +4,7 @@
/* eslint-disable vars-on-top */
/* eslint-disable no-var */
/* eslint-disable func-names */
import crypto from "crypto";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
@@ -33,6 +34,7 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
@@ -60,14 +62,16 @@ import {
import { usePopUp } from "@app/hooks";
import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useAddUserToWs,
useCreateWorkspace,
useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useGetUserAction,
useLogoutUser,
useRegisterUserAction
useRegisterUserAction,
useUploadWsKey
} from "@app/hooks/api";
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { CreateOrgModal } from "@app/views/Org/components";
interface LayoutProps {
@@ -125,8 +129,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
: true;
const createWs = useCreateWorkspace();
const addUsersToProject = useAddUserToWsNonE2EE();
const uploadWsKey = useUploadWsKey();
const addWsUser = useAddUserToWs();
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -216,32 +220,53 @@ export const AppLayout = ({ children }: LayoutProps) => {
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
if (!currentOrg?.id) return;
try {
const {
data: {
project: { id: newProjectId }
workspace: { id: newWorkspaceId }
}
} = await createWs.mutateAsync({
organizationId: currentOrg.id,
projectName: name
organizationId: currentOrg?.id,
workspaceName: name
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
encryptedKey: ciphertext,
nonce,
userId: user?.id,
workspaceId: newWorkspaceId
});
if (addMembers) {
// not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg.id);
await addUsersToProject.mutateAsync({
emails: orgUsers
.map((member) => member.user.email)
.filter((email) => email !== user.email),
projectId: newProjectId
const decryptKey = await fetchUserWsKey(newWorkspaceId);
await addWsUser.mutateAsync({
workspaceId: newWorkspaceId,
decryptKey,
userPrivateKey: PRIVATE_KEY,
members: orgUsers
.filter(
({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
)
.map(({ user: orgUser, id: orgMembershipId }) => ({
userPublicKey: orgUser.publicKey,
orgMembershipId
}))
});
}
createNotification({ text: "Workspace created", type: "success" });
handlePopUpClose("addNewWs");
router.push(`/project/${newProjectId}/secrets/overview`);
router.push(`/project/${newWorkspaceId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" });

View File

@@ -1,5 +1,7 @@
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
import crypto from "crypto";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -21,7 +23,7 @@ import {
faNetworkWired,
faPlug,
faPlus,
faUserPlus
faUserPlus,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
@@ -31,6 +33,7 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
@@ -51,11 +54,12 @@ import {
import { withPermission } from "@app/hoc";
import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useAddUserToWs,
useCreateWorkspace,
useRegisterUserAction
useRegisterUserAction,
useUploadWsKey
} from "@app/hooks/api";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -471,7 +475,7 @@ const OrganizationPage = withPermission(
const currentOrg = String(router.query.id);
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === currentOrg) || [];
const { createNotification } = useNotificationContext();
const addUsersToProject = useAddUserToWsNonE2EE();
const addWsUser = useAddUserToWs();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addNewWs",
@@ -493,36 +497,61 @@ const OrganizationPage = withPermission(
const [searchFilter, setSearchFilter] = useState("");
const createWs = useCreateWorkspace();
const { user } = useUser();
const uploadWsKey = useUploadWsKey();
const { data: serverDetails } = useFetchServerStatus();
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
try {
const {
data: {
project: { id: newProjectId }
workspace: { id: newWorkspaceId }
}
} = await createWs.mutateAsync({
organizationId: currentOrg,
projectName: name
workspaceName: name
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
encryptedKey: ciphertext,
nonce,
userId: user?.id,
workspaceId: newWorkspaceId
});
if (addMembers) {
// not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg);
await addUsersToProject.mutateAsync({
emails: orgUsers
.map((member) => member.user.email)
.filter((email) => email !== user.email),
projectId: newProjectId
});
const decryptKey = await fetchUserWsKey(newWorkspaceId);
const members = orgUsers
.filter(
({ status, user: orgUser }) => status === "accepted" && user.email !== orgUser.email
)
.map(({ user: orgUser, id: orgMembershipId }) => ({
userPublicKey: orgUser.publicKey,
orgMembershipId
}));
if (members.length) {
await addWsUser.mutateAsync({
workspaceId: newWorkspaceId,
decryptKey,
userPrivateKey: PRIVATE_KEY,
members
});
}
}
createNotification({ text: "Workspace created", type: "success" });
handlePopUpClose("addNewWs");
router.push(`/project/${newProjectId}/secrets/overview`);
router.push(`/project/${newWorkspaceId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" });
@@ -706,7 +735,7 @@ const OrganizationPage = withPermission(
new Date().getTime() - new Date(user?.createdAt).getTime() <
30 * 24 * 60 * 60 * 1000
) && (
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<LearningItemSquare

View File

@@ -44,8 +44,7 @@ import {
} from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useAddUserToWs,
useDeleteUserFromWorkspace,
useGetOrgUsers,
useGetProjectRoles,
@@ -96,14 +95,12 @@ export const MemberListTab = () => {
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE();
const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
const { mutateAsync: addUserToWorkspace } = useAddUserToWs();
const { mutateAsync: uploadWsKey } = useUploadWsKey();
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
if (!currentWorkspace) return;
if (!currentOrg?.id) return;
// TODO(akhilmhdh): Move to memory storage
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
@@ -117,24 +114,12 @@ export const MemberListTab = () => {
if (!orgUser) return;
try {
if (currentWorkspace.version === "v1") {
await addUserToWorkspace({
workspaceId,
userPrivateKey,
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
});
} else if (currentWorkspace.version === "v2") {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
emails: [orgUser.user.email]
});
} else {
createNotification({
text: "Failed to add user to project, unknown project type",
type: "error"
});
}
await addUserToWorkspace({
workspaceId,
userPrivateKey,
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
});
createNotification({
text: "Successfully added user to the project",
type: "success"

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -14,8 +14,6 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import {
Alert,
AlertDescription,
Button,
EmptyState,
IconButton,
@@ -39,8 +37,7 @@ import {
useGetFoldersByEnv,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
useUpdateSecretV3,
useUpgradeProject
useUpdateSecretV3
} from "@app/hooks/api";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
@@ -107,7 +104,6 @@ export const SecretOverviewPage = () => {
environments: userAvailableEnvs.map(({ slug }) => slug)
});
const upgradeProject = useUpgradeProject();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
@@ -201,24 +197,6 @@ export const SecretOverviewPage = () => {
}
};
const onUpgradeProject = useCallback(async () => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
createNotification({
type: "error",
text: "Private key not found"
});
return;
}
await upgradeProject.mutateAsync({
projectId: workspaceId,
privateKey: PRIVATE_KEY
});
}, []);
const handleResetSearch = () => setSearchFilter("");
const handleFolderClick = (path: string) => {
@@ -337,23 +315,6 @@ export const SecretOverviewPage = () => {
.
</p>
</div>
{currentWorkspace?.version === "v1" && (
<div className="mt-8">
<Alert variant="danger">
<AlertDescription className="prose">
Upgrade your project. More filler text More filler text More filler text More filler
text More filler text More filler text More filler text More filler text More filler
text More filler text More filler text More filler text{" "}
</AlertDescription>
<div className="mt-2">
<Button isLoading={upgradeProject.isLoading} onClick={onUpgradeProject}>
Upgrade
</Button>
</div>
</Alert>
</div>
)}
<div className="mt-8 flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="w-80">

View File

@@ -30,35 +30,35 @@ export const AdminDashboardPage = () => {
return (
<div className="container mx-auto max-w-7xl pb-12 text-white dark:[color-scheme:dark]">
<div className="mb-8">
<div className="mb-4 mt-6 flex flex-col items-start justify-between text-xl">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<div className="mb-8 flex flex-col items-start justify-between text-xl">
<h1 className="text-3xl font-semibold">Admin Dashboard</h1>
<p className="text-base text-bunker-300">Manage your Infisical</p>
<p className="text-base text-bunker-300">Manage your Infisical instance.</p>
</div>
{isUserLoading || isNotAllowed ? (
<ContentLoader text={isNotAllowed ? "Redirecting to org page..." : undefined} />
) : (
<div>
<Tabs defaultValue={TabSections.Settings}>
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>
<div className="flex items-center space-x-4">
<Switch
id="disable-invite"
isChecked={Boolean(config?.allowSignUp)}
onCheckedChange={(isChecked) => updateServerConfig({ allowSignUp: isChecked })}
/>
<div className="flex-grow">Enable signup or invite</div>
</div>
</TabPanel>
</Tabs>
</div>
)}
</div>
{isUserLoading || isNotAllowed ? (
<ContentLoader text={isNotAllowed ? "Redirecting to org page..." : undefined} />
) : (
<div>
<Tabs defaultValue={TabSections.Settings}>
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>
<div className="flex items-center space-x-4">
<Switch
id="disable-invite"
isChecked={Boolean(config?.allowSignUp)}
onCheckedChange={(isChecked) => updateServerConfig({ allowSignUp: isChecked })}
/>
<div className="flex-grow">Enable signup or invite</div>
</div>
</TabPanel>
</Tabs>
</div>
)}
</div>
);
};

View File

@@ -61,7 +61,7 @@ export const SignUpPage = () => {
router.push("/login");
}
}
}, [config?.initialized]);
}, []);
const { mutateAsync: createAdminUser } = useCreateAdminUser();