Merge branch 'main' into org-name-constraint

This commit is contained in:
Scott Wilson
2025-03-12 17:07:12 -07:00
258 changed files with 9274 additions and 3566 deletions
+3
View File
@@ -0,0 +1,3 @@
# Learn more at https://direnv.net
# We instruct direnv to use our Nix flake for a consistent development environment.
use flake
@@ -92,7 +92,7 @@ jobs:
exit 1 exit 1
fi fi
- name: Install openapi-diff - name: Install openapi-diff
run: go install github.com/tufin/oasdiff@latest run: go install github.com/oasdiff/oasdiff@latest
- name: Running OpenAPI Spec diff action - name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup - name: cleanup
+5 -1
View File
@@ -34,7 +34,10 @@ jobs:
working-directory: backend working-directory: backend
- name: Start postgres and redis - name: Start postgres and redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test - name: Run unit test
run: npm run test:unit
working-directory: backend
- name: Run integration test
run: npm run test:e2e run: npm run test:e2e
working-directory: backend working-directory: backend
env: env:
@@ -45,3 +48,4 @@ jobs:
- name: cleanup - name: cleanup
run: | run: |
docker compose -f "docker-compose.dev.yml" down docker compose -f "docker-compose.dev.yml" down
@@ -120,4 +120,3 @@ export default {
}; };
} }
}; };
+2
View File
@@ -40,6 +40,7 @@
"type:check": "tsc --noEmit", "type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src", "lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'", "lint": "eslint 'src/**/*.ts'",
"test:unit": "vitest run -c vitest.unit.config.ts",
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
@@ -70,6 +71,7 @@
"migrate:org": "tsx ./scripts/migrate-organization.ts", "migrate:org": "tsx ./scripts/migrate-organization.ts",
"seed:new": "tsx ./scripts/create-seed-file.ts", "seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest" "db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
}, },
"keywords": [], "keywords": [],
@@ -0,0 +1,45 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
if (!hasSecretVersionV2UserActorId) {
t.uuid("userActorId");
t.foreign("userActorId").references("id").inTable(TableName.Users);
}
if (!hasSecretVersionV2IdentityActorId) {
t.uuid("identityActorId");
t.foreign("identityActorId").references("id").inTable(TableName.Identity);
}
if (!hasSecretVersionV2ActorType) {
t.string("actorType");
}
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretVersionV2)) {
const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId");
const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId");
const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType");
await knex.schema.alterTable(TableName.SecretVersionV2, (t) => {
if (hasSecretVersionV2UserActorId) {
t.dropColumn("userActorId");
}
if (hasSecretVersionV2IdentityActorId) {
t.dropColumn("identityActorId");
}
if (hasSecretVersionV2ActorType) {
t.dropColumn("actorType");
}
});
}
}
+4 -1
View File
@@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({
folderId: z.string().uuid(), folderId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
userActorId: z.string().uuid().nullable().optional(),
identityActorId: z.string().uuid().nullable().optional(),
actorType: z.string().nullable().optional()
}); });
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>; export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
@@ -1,16 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import { import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
SecretApprovalRequestsReviewersSchema,
SecretApprovalRequestsSchema,
SecretTagsSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
@@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
} }
}); });
const tagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.array()
.optional();
server.route({ server.route({
method: "GET", method: "GET",
url: "/:id", url: "/:id",
@@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .omit({ _id: true, environment: true, workspace: true, type: true, version: true })
.extend({ .extend({
op: z.string(), op: z.string(),
tags: tagSchema, tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish(), secretMetadata: ResourceMetadataSchema.nullish(),
secret: z secret: z
.object({ .object({
@@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretComment: z.string().optional(), secretComment: z.string().optional(),
tags: tagSchema, tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish() secretMetadata: ResourceMetadataSchema.nullish()
}) })
.optional() .optional()
+2 -2
View File
@@ -1,6 +1,6 @@
import z from "zod"; import z from "zod";
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { RAW_SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
@@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const AccessListEntrySchema = z const AccessListEntrySchema = z
.object({ .object({
allowedActions: z.nativeEnum(ProjectPermissionActions).array(), allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
id: z.string(), id: z.string(),
membershipId: z.string(), membershipId: z.string(),
name: z.string() name: z.string()
@@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
secretVersions: secretRawSchema.array() secretVersions: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
}) })
} }
}, },
@@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
offset: req.query.offset, offset: req.query.offset,
secretId: req.params.secretId secretId: req.params.secretId
}); });
return { secretVersions }; return { secretVersions };
} }
}); });
+5 -7
View File
@@ -1,10 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; import { SecretSnapshotsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => { export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
secretVersions: secretRawSchema secretVersions: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true }) .omit({ _id: true, environment: true, workspace: true, type: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretId: z.string(), secretId: z.string(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
}).array()
}) })
.array(), .array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
@@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId id: req.params.secretSnapshotId
}); });
return { secretSnapshot }; return { secretSnapshot };
} }
}); });
@@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
@@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
body: z.object({ body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
isTemporary: z.literal(false) isTemporary: z.literal(false)
@@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }), z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
z.object({ z.object({
@@ -3,6 +3,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), permissions: ProjectPermissionV2Schema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
isTemporary: z.literal(false) isTemporary: z.literal(false)
@@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
z.object({ z.object({
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.CREATE.slug), .describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.UPDATE.slug), .describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.UPDATE.permissions)
.optional()
.superRefine(checkForInvalidPermissionCombination)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1,5 +1,16 @@
import { z } from "zod"; import { z } from "zod";
export type PasswordRequirements = {
length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
allowedSymbols?: string;
};
export enum SqlProviders { export enum SqlProviders {
Postgres = "postgres", Postgres = "postgres",
MySQL = "mysql2", MySQL = "mysql2",
@@ -100,6 +111,28 @@ export const DynamicSecretSqlDBSchema = z.object({
database: z.string().trim(), database: z.string().trim(),
username: z.string().trim(), username: z.string().trim(),
password: z.string().trim(), password: z.string().trim(),
passwordRequirements: z
.object({
length: z.number().min(1).max(250),
required: z
.object({
lowercase: z.number().min(0),
uppercase: z.number().min(0),
digits: z.number().min(0),
symbols: z.number().min(0)
})
.refine((data) => {
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 250;
}, "Sum of required characters cannot exceed 250"),
allowedSymbols: z.string().optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.optional()
.describe("Password generation requirements"),
creationStatement: z.string().trim(), creationStatement: z.string().trim(),
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
@@ -1,6 +1,6 @@
import { randomInt } from "crypto";
import handlebars from "handlebars"; import handlebars from "handlebars";
import knex from "knex"; import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { withGatewayProxy } from "@app/lib/gateway"; import { withGatewayProxy } from "@app/lib/gateway";
@@ -8,16 +8,99 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGatewayServiceFactory } from "../../gateway/gateway-service"; import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models"; import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const generatePassword = (provider: SqlProviders) => { const DEFAULT_PASSWORD_REQUIREMENTS = {
// oracle has limit of 48 password length length: 48,
const size = provider === SqlProviders.Oracle ? 30 : 48; required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*"; const ORACLE_PASSWORD_REQUIREMENTS = {
return customAlphabet(charset, 48)(size); ...DEFAULT_PASSWORD_REQUIREMENTS,
length: 30
};
const generatePassword = (provider: SqlProviders, requirements?: PasswordRequirements) => {
const defaultReqs = provider === SqlProviders.Oracle ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
const finalReqs = requirements || defaultReqs;
try {
const { length, required, allowedSymbols } = finalReqs;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
}; };
const generateUsername = (provider: SqlProviders) => { const generateUsername = (provider: SqlProviders) => {
@@ -115,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const create = async (inputs: unknown, expireAt: number) => { const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs); const providerInputs = await validateProviderInputs(inputs);
const username = generateUsername(providerInputs.client); const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client); const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => { const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host }); const db = await $getClient({ ...providerInputs, port, host });
try { try {
+30 -13
View File
@@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
@@ -87,9 +87,14 @@ export const groupServiceFactory = ({
actorOrgId actorOrgId
); );
const isCustomRole = Boolean(customRole); const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to create a more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const group = await groupDAL.transaction(async (tx) => { const group = await groupDAL.transaction(async (tx) => {
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx); const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
@@ -156,9 +161,13 @@ export const groupServiceFactory = ({
); );
const isCustomRole = Boolean(customOrgRole); const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!hasRequiredNewRolePermission) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update a more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
} }
@@ -329,9 +338,13 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission); const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
if (!hasRequiredPrivileges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to add user to more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
@@ -396,9 +409,13 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission); const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
if (!hasRequiredPrivileges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to delete user from more privileged group",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
@@ -3,7 +3,7 @@ import { packRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
@@ -79,9 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -161,9 +165,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (data?.slug) { if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
@@ -239,9 +247,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
return { return {
@@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
@@ -88,9 +88,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -172,9 +176,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -268,9 +276,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to edit more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -1,7 +1,109 @@
/* eslint-disable no-nested-ternary */
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
import { z } from "zod";
import { TOrganizations } from "@app/db/schemas"; import { TOrganizations } from "@app/db/schemas";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
import {
ProjectPermissionSecretActions,
ProjectPermissionSet,
ProjectPermissionSub,
ProjectPermissionV2Schema,
SecretSubjectFields
} from "./project-permission";
export function throwIfMissingSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet> | PureAbility,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
>,
subjectFields?: SecretSubjectFields
) {
try {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
} catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
}
}
export function hasSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet>,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
>,
subjectFields?: SecretSubjectFields
) {
let canNewPermission = false;
let canOldPermission = false;
if (subjectFields) {
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
return canNewPermission || canOldPermission;
}
const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional();
export function checkForInvalidPermissionCombination(permissions: z.infer<typeof OptionalArrayPermissionSchema>) {
if (!permissions) return;
for (const permission of permissions) {
if (permission.subject === ProjectPermissionSub.Secrets) {
if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) {
const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue);
const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret);
// eslint-disable-next-line no-continue
if (!hasReadValue && !hasDescribeSecret) continue;
const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret;
throw new BadRequestError({
message: `You have selected Read, and ${
hasBothDescribeAndReadValue
? "both Read Value and Describe Secret"
: hasReadValue
? "Read Value"
: hasDescribeSecret
? "Describe Secret"
: ""
}. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.`
});
}
}
}
return true;
}
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
if (!actorAuthMethod) return false; if (!actorAuthMethod) return false;
@@ -5,22 +5,6 @@ import { PermissionConditionOperators } from "@app/lib/casl";
export const PermissionConditionSchema = { export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(), [PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
[PermissionConditionOperators.$REGEX]: z
.string()
.min(1)
.refine(
(el) => {
try {
// eslint-disable-next-line no-new
new RegExp(el);
return true;
} catch {
return false;
}
},
{ message: "Invalid regex pattern" }
),
[PermissionConditionOperators.$EQ]: z.string().min(1), [PermissionConditionOperators.$EQ]: z.string().min(1),
[PermissionConditionOperators.$NEQ]: z.string().min(1), [PermissionConditionOperators.$NEQ]: z.string().min(1),
[PermissionConditionOperators.$GLOB]: z [PermissionConditionOperators.$GLOB]: z
@@ -17,6 +17,15 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionCmekActions { export enum ProjectPermissionCmekActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -115,7 +124,7 @@ export type IdentityManagementSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionSecretActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
] ]
| [ | [
@@ -429,6 +438,7 @@ const GeneralPermissionSchema = [
}) })
]; ];
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
@@ -460,7 +470,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
), ),
conditions: SecretConditionV2Schema.describe( conditions: SecretConditionV2Schema.describe(
@@ -517,7 +527,6 @@ const buildAdminPermissionRules = () => {
// Admins get full access to everything // Admins get full access to everything
[ [
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretApproval,
@@ -550,10 +559,22 @@ const buildAdminPermissionRules = () => {
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionActions.Delete ProjectPermissionActions.Delete
], ],
el as ProjectPermissionSub el
); );
}); });
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can( can(
[ [
ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionDynamicSecretActions.ReadRootCredential,
@@ -613,10 +634,12 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionActions.Create, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionActions.Delete ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
], ],
ProjectPermissionSub.Secrets ProjectPermissionSub.Secrets
); );
@@ -788,7 +811,9 @@ export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => { const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
@@ -837,7 +862,6 @@ export const buildServiceTokenProjectPermission = (
(subject) => { (subject) => {
if (canWrite) { if (canWrite) {
can(ProjectPermissionActions.Edit, subject, { can(ProjectPermissionActions.Edit, subject, {
// TODO: @Akhi
// @ts-expect-error type // @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
@@ -916,7 +940,17 @@ export const backfillPermissionV1SchemaToV2Schema = (
subject: ProjectPermissionSub.SecretImports as const subject: ProjectPermissionSub.SecretImports as const
})); }));
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
subject: ProjectPermissionSub.Secrets as const,
...el,
action:
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
: el.action
}));
const secretFolderPolicies = secretSubjects const secretFolderPolicies = secretSubjects
.map(({ subject, ...el }) => ({ .map(({ subject, ...el }) => ({
...el, ...el,
// read permission is not needed anymore // read permission is not needed anymore
@@ -958,6 +992,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts // @ts-ignore-error this is valid ts
secretImportPolicies, secretImportPolicies,
secretPolicies,
dynamicSecretPolicies, dynamicSecretPolicies,
hasReadOnlyFolder.length ? [] : secretFolderPolicies hasReadOnlyFolder.length ? [] : secretFolderPolicies
); );
@@ -3,7 +3,7 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
@@ -76,9 +76,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged user",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -163,9 +167,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
// we need to validate that the privilege given is not higher than the assigning users permission // we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (dto?.slug) { if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
@@ -6,6 +6,7 @@ import {
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretType, SecretType,
TableName,
TSecretApprovalRequestsSecretsInsert, TSecretApprovalRequestsSecretsInsert,
TSecretApprovalRequestsSecretsV2Insert TSecretApprovalRequestsSecretsV2Insert
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -57,8 +58,9 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
@@ -88,7 +90,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: Pick< secretTagDAL: Pick<
TSecretTagDALFactory, TSecretTagDALFactory,
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "findManyTagsById"
| "saveTagsToSecret"
| "deleteTagsManySecret"
| "saveTagsToSecretV2"
| "deleteTagsToSecretV2"
| "find"
>; >;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@@ -106,7 +113,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -503,7 +510,7 @@ export const secretApprovalRequestServiceFactory = ({
if (!hasMinApproval && !isSoftEnforcement) if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
let mergeStatus; let mergeStatus;
if (shouldUseSecretV2Bridge) { if (shouldUseSecretV2Bridge) {
// this cycle if for bridged secrets // this cycle if for bridged secrets
@@ -861,7 +868,6 @@ export const secretApprovalRequestServiceFactory = ({
if (isSoftEnforcement) { if (isSoftEnforcement) {
const cfg = getConfig(); const cfg = getConfig();
const project = await projectDAL.findProjectById(projectId);
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
const requestedByUser = await userDAL.findOne({ id: actorId }); const requestedByUser = await userDAL.findOne({ id: actorId });
const approverUsers = await userDAL.find({ const approverUsers = await userDAL.find({
@@ -913,10 +919,11 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) environment,
); secretPath
});
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -1001,6 +1008,7 @@ export const secretApprovalRequestServiceFactory = ({
: keyName2BlindIndex[secretName]; : keyName2BlindIndex[secretName];
// add tags // add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
return { return {
...latestSecretVersions[secretId], ...latestSecretVersions[secretId],
...el, ...el,
@@ -1156,7 +1164,8 @@ export const secretApprovalRequestServiceFactory = ({
environment: env.name, environment: env.name,
secretPath, secretPath,
projectId, projectId,
requestId: secretApprovalRequest.id requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretName) ?? []))]
} }
} }
}); });
@@ -1327,17 +1336,48 @@ export const secretApprovalRequestServiceFactory = ({
// deleted secrets // deleted secrets
const deletedSecrets = data[SecretOperations.Delete]; const deletedSecrets = data[SecretOperations.Delete];
if (deletedSecrets && deletedSecrets.length) { if (deletedSecrets && deletedSecrets.length) {
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys( const secretsToDeleteInDB = await secretV2BridgeDAL.find({
folderId, folderId,
deletedSecrets.map((el) => ({ $complex: {
key: el.secretKey, operator: "and",
type: SecretType.Shared value: [
})) {
); operator: "or",
value: deletedSecrets.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
});
if (secretsToDeleteInDB.length !== deletedSecrets.length) if (secretsToDeleteInDB.length !== deletedSecrets.length)
throw new NotFoundError({ throw new NotFoundError({
message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}` message: `Secret does not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
}); });
secretsToDeleteInDB.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags?.map((i) => i.slug)
})
);
});
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key); const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id); const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds); const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
@@ -1363,9 +1403,9 @@ export const secretApprovalRequestServiceFactory = ({
const tagsGroupById = groupBy(tags, (i) => i.id); const tagsGroupById = groupBy(tags, (i) => i.id);
commits.forEach((commit) => { commits.forEach((commit) => {
let action = ProjectPermissionActions.Create; let action = ProjectPermissionSecretActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit;
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; if (commit.op === SecretOperations.Delete) return; // we do the validation on top
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
action, action,
@@ -1456,7 +1496,8 @@ export const secretApprovalRequestServiceFactory = ({
environment: env.name, environment: env.name,
secretPath, secretPath,
projectId, projectId,
requestId: secretApprovalRequest.id requestId: secretApprovalRequest.id,
secretKeys: [...new Set(Object.values(data).flatMap((arr) => arr?.map((item) => item.secretKey) ?? []))]
} }
} }
}); });
@@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
viewSecretValue: true,
hasSecretAccess: () => true hasSecretAccess: () => true
}); });
// secrets that gets replicated across imports // secrets that gets replicated across imports
@@ -13,6 +13,7 @@ import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -332,6 +333,7 @@ export const secretRotationQueueFactory = ({
await secretVersionV2BridgeDAL.insertMany( await secretVersionV2BridgeDAL.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el, ...el,
actorType: ActorType.PLATFORM,
secretId: id secretId: id
})), })),
tx tx
@@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationDALFactory } from "./secret-rotation-dal";
import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
@@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({
}); });
} }
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -1,16 +1,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error // akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -21,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { import {
TGetSnapshotDataDTO, TGetSnapshotDataDTO,
TProjectSnapshotCountDTO, TProjectSnapshotCountDTO,
@@ -96,10 +106,10 @@ export const secretSnapshotServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) { if (!folder) {
@@ -133,10 +143,10 @@ export const secretSnapshotServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) if (!folder)
@@ -161,6 +171,7 @@ export const secretSnapshotServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails; let snapshotDetails;
if (shouldUseBridge) { if (shouldUseBridge) {
@@ -169,68 +180,112 @@ export const secretSnapshotServiceFactory = ({
projectId: snapshot.projectId projectId: snapshot.projectId
}); });
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
...el, const canReadValue = hasSecretReadValueOrDescribePermission(
secretKey: el.key, permission,
secretValue: el.encryptedValue ProjectPermissionSecretActions.ReadValue,
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() {
: "", environment: encryptedSnapshotDetails.environment.slug,
secretComment: el.encryptedComment secretPath: fullFolderPath,
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() secretName: el.key,
: "" secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
})) }
);
let secretValue = "";
if (canReadValue) {
secretValue = el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "";
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey: el.key,
secretValueHidden: !canReadValue,
secretValue,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
};
})
}; };
} else { } else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
const { botKey } = await projectBotService.getBotKey(snapshot.projectId); const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey) if (!botKey)
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
...el, const secretKey = decryptSymmetric128BitHexKeyUTF8({
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext, ciphertext: el.secretKeyCiphertext,
iv: el.secretKeyIV, iv: el.secretKeyIV,
tag: el.secretKeyTag, tag: el.secretKeyTag,
key: botKey key: botKey
}), });
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext, const canReadValue = hasSecretReadValueOrDescribePermission(
iv: el.secretValueIV, permission,
tag: el.secretValueTag, ProjectPermissionSecretActions.ReadValue,
key: botKey {
}), environment: encryptedSnapshotDetails.environment.slug,
secretComment: secretPath: fullFolderPath,
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext secretName: secretKey,
? decryptSymmetric128BitHexKeyUTF8({ secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
ciphertext: el.secretCommentCiphertext, }
iv: el.secretCommentIV, );
tag: el.secretCommentTag,
key: botKey let secretValue = "";
})
: "" if (canReadValue) {
})) secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
});
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey,
secretValueHidden: !canReadValue,
secretValue,
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
};
})
}; };
} }
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshotDetails.folderId,
envId: snapshotDetails.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath
})
);
return snapshotDetails; return snapshotDetails;
}; };
@@ -370,7 +425,21 @@ export const secretSnapshotServiceFactory = ({
const secrets = await secretV2BridgeDAL.insertMany( const secrets = await secretV2BridgeDAL.insertMany(
rollbackSnaps.flatMap(({ secretVersions, folderId }) => rollbackSnaps.flatMap(({ secretVersions, folderId }) =>
secretVersions.map( secretVersions.map(
({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({ ({
latestSecretVersion,
version,
updatedAt,
createdAt,
secretId,
envId,
id,
tags,
// exclude the bottom fields from the secret - they are for versioning only.
userActorId,
identityActorId,
actorType,
...el
}) => ({
...el, ...el,
id: secretId, id: secretId,
version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion,
@@ -401,8 +470,18 @@ export const secretSnapshotServiceFactory = ({
})), })),
tx tx
); );
const userActorId = actor === ActorType.USER ? actorId : undefined;
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
const actorType = actor || ActorType.PLATFORM;
const secretVersions = await secretVersionV2BridgeDAL.insertMany( const secretVersions = await secretVersionV2BridgeDAL.insertMany(
secrets.map(({ id, updatedAt, createdAt, ...el }) => ({ ...el, secretId: id })), secrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el,
secretId: id,
userActorId,
identityActorId,
actorType
})),
tx tx
); );
await secretVersionV2TagBridgeDAL.insertMany( await secretVersionV2TagBridgeDAL.insertMany(
+5 -1
View File
@@ -459,7 +459,8 @@ export const PROJECTS = {
workspaceId: "The ID of the project to update.", workspaceId: "The ID of the project to update.",
name: "The new name of the project.", name: "The new name of the project.",
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project." autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)"
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
@@ -666,6 +667,7 @@ export const SECRETS = {
secretPath: "The path of the secret to attach tags to.", secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)", type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located", environment: "The slug of the environment where the secret is located",
viewSecretValue: "Whether or not to retrieve the secret value.",
projectSlug: "The slug of the project where the secret is located.", projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to attach to the secret." tagSlugs: "An array of existing tag slugs to attach to the secret."
}, },
@@ -689,6 +691,7 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.", "The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.", secretPath: "The secret path to list secrets from.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not.", includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets.", tagSlugs: "The comma separated tag slugs to filter secrets.",
metadataFilter: metadataFilter:
@@ -717,6 +720,7 @@ export const RAW_SECRETS = {
secretPath: "The path of the secret to get.", secretPath: "The path of the secret to get.",
version: "The version of the secret to get.", version: "The version of the secret to get.",
type: "The type of the secret to get.", type: "The type of the secret to get.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not." includeImports: "Weather to include imported secrets or not."
}, },
UPDATE: { UPDATE: {
+669
View File
@@ -0,0 +1,669 @@
import { createMongoAbility } from "@casl/ability";
import { PermissionConditionOperators } from ".";
import { validatePermissionBoundary } from "./boundary";
describe("Validate Permission Boundary Function", () => {
test.each([
{
title: "child with equal privilege",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets"
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets"
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "child with less privilege",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets"
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit"],
subject: "secrets"
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "child with more privilege",
parentPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets"
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit"],
subject: "secrets"
}
]),
expectValid: false,
missingPermissions: [{ action: "edit", subject: "secrets" }]
},
{
title: "parent with multiple and child with multiple",
parentPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets"
},
{
action: ["create", "edit"],
subject: "members"
}
]),
childPermission: createMongoAbility([
{
action: ["create"],
subject: "members"
},
{
action: ["create"],
subject: "secrets"
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "Child with no access",
parentPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets"
},
{
action: ["create", "edit"],
subject: "members"
}
]),
childPermission: createMongoAbility([]),
expectValid: true,
missingPermissions: []
},
{
title: "Parent and child disjoint set",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
]),
childPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
]),
expectValid: false,
missingPermissions: ["create", "edit", "delete", "read"].map((el) => ({
action: el,
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "dev" }
}
}))
},
{
title: "Parent with inverted rules",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
},
{
action: "read",
subject: "secrets",
inverted: true,
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
}
}
]),
childPermission: createMongoAbility([
{
action: "read",
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
}
}
]),
expectValid: true,
missingPermissions: []
},
{
title: "Parent with inverted rules - child accessing invalid one",
parentPermission: createMongoAbility([
{
action: ["create", "edit", "delete", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
},
{
action: "read",
subject: "secrets",
inverted: true,
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
}
}
]),
childPermission: createMongoAbility([
{
action: "read",
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
}
}
]),
expectValid: false,
missingPermissions: [
{
action: "read",
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" },
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
}
}
]
}
])("Check permission: $title", ({ parentPermission, childPermission, expectValid, missingPermissions }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
if (expectValid) {
expect(permissionBoundary.isValid).toBeTruthy();
} else {
expect(permissionBoundary.isValid).toBeFalsy();
expect(permissionBoundary.missingPermissions).toEqual(expect.arrayContaining(missingPermissions));
}
});
});
describe("Validate Permission Boundary: Checking Parent $eq operator", () => {
const parentPermission = createMongoAbility([
{
action: ["create", "read"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "prod" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "staging" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
describe("Validate Permission Boundary: Checking Parent $neq operator", () => {
const parentPermission = createMongoAbility([
{
action: ["create", "read"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/staging"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/dev**" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/hello" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
describe("Validate Permission Boundary: Checking Parent $IN operator", () => {
const parentPermission = createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "dev" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev"] }
}
}
])
},
{
operator: `${PermissionConditionOperators.$IN} - 2`,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "staging"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$EQ]: "prod" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$NEQ]: "dev" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$IN]: ["dev", "prod"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["edit"],
subject: "secrets",
conditions: {
environment: { [PermissionConditionOperators.$GLOB]: "dev**" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
describe("Validate Permission Boundary: Checking Parent $GLOB operator", () => {
const parentPermission = createMongoAbility([
{
action: ["create", "read"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**" }
}
}
]);
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/hello/world" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/hello/world", "/hello/world2"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello/**/world" }
}
}
])
}
])("Child $operator truthy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeTruthy();
});
test.each([
{
operator: PermissionConditionOperators.$EQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$EQ]: "/print" }
}
}
])
},
{
operator: PermissionConditionOperators.$NEQ,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$NEQ]: "/hello/world" }
}
}
])
},
{
operator: PermissionConditionOperators.$IN,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$IN]: ["/", "/hello"] }
}
}
])
},
{
operator: PermissionConditionOperators.$GLOB,
childPermission: createMongoAbility([
{
action: ["create"],
subject: "secrets",
conditions: {
secretPath: { [PermissionConditionOperators.$GLOB]: "/hello**" }
}
}
])
}
])("Child $operator falsy cases", ({ childPermission }) => {
const permissionBoundary = validatePermissionBoundary(parentPermission, childPermission);
expect(permissionBoundary.isValid).toBeFalsy();
});
});
+249
View File
@@ -0,0 +1,249 @@
import { MongoAbility } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { PermissionConditionOperators } from "./index";
type TMissingPermission = {
action: string;
subject: string;
conditions?: MongoQuery;
};
type TPermissionConditionShape = {
[PermissionConditionOperators.$EQ]: string;
[PermissionConditionOperators.$NEQ]: string;
[PermissionConditionOperators.$GLOB]: string;
[PermissionConditionOperators.$IN]: string[];
};
const getPermissionSetID = (action: string, subject: string) => `${action}:${subject}`;
const invertTheOperation = (shouldInvert: boolean, operation: boolean) => (shouldInvert ? !operation : operation);
const formatConditionOperator = (condition: TPermissionConditionShape | string) => {
return (
typeof condition === "string" ? { [PermissionConditionOperators.$EQ]: condition } : condition
) as TPermissionConditionShape;
};
const isOperatorsASubset = (parentSet: TPermissionConditionShape, subset: TPermissionConditionShape) => {
// we compute each operator against each other in left hand side and right hand side
if (subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ]) {
const subsetOperatorValue = subset[PermissionConditionOperators.$EQ] || subset[PermissionConditionOperators.$NEQ];
const isInverted = !subset[PermissionConditionOperators.$EQ];
if (
parentSet[PermissionConditionOperators.$EQ] &&
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$NEQ] &&
invertTheOperation(isInverted, parentSet[PermissionConditionOperators.$NEQ] === subsetOperatorValue)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$IN] &&
invertTheOperation(isInverted, !parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue))
) {
return false;
}
// ne and glob cannot match each other
if (parentSet[PermissionConditionOperators.$GLOB] && isInverted) {
return false;
}
if (
parentSet[PermissionConditionOperators.$GLOB] &&
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], { strictSlashes: false })
) {
return false;
}
}
if (subset[PermissionConditionOperators.$IN]) {
const subsetOperatorValue = subset[PermissionConditionOperators.$IN];
if (
parentSet[PermissionConditionOperators.$EQ] &&
(subsetOperatorValue.length !== 1 || subsetOperatorValue[0] !== parentSet[PermissionConditionOperators.$EQ])
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$NEQ] &&
subsetOperatorValue.includes(parentSet[PermissionConditionOperators.$NEQ])
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$IN] &&
!subsetOperatorValue.every((el) => parentSet[PermissionConditionOperators.$IN].includes(el))
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$GLOB] &&
!subsetOperatorValue.every((el) =>
picomatch.isMatch(el, parentSet[PermissionConditionOperators.$GLOB], {
strictSlashes: false
})
)
) {
return false;
}
}
if (subset[PermissionConditionOperators.$GLOB]) {
const subsetOperatorValue = subset[PermissionConditionOperators.$GLOB];
const { isGlob } = picomatch.scan(subsetOperatorValue);
// if it's glob, all other fixed operators would make this superset because glob is powerful. like eq
// example: $in [dev, prod] => glob: dev** could mean anything starting with dev: thus is bigger
if (
isGlob &&
Object.keys(parentSet).some(
(el) => el !== PermissionConditionOperators.$GLOB && el !== PermissionConditionOperators.$NEQ
)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$EQ] &&
parentSet[PermissionConditionOperators.$EQ] !== subsetOperatorValue
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$NEQ] &&
picomatch.isMatch(parentSet[PermissionConditionOperators.$NEQ], subsetOperatorValue, {
strictSlashes: false
})
) {
return false;
}
// if parent set is IN, glob cannot be used for children - It's a bigger scope
if (
parentSet[PermissionConditionOperators.$IN] &&
!parentSet[PermissionConditionOperators.$IN].includes(subsetOperatorValue)
) {
return false;
}
if (
parentSet[PermissionConditionOperators.$GLOB] &&
!picomatch.isMatch(subsetOperatorValue, parentSet[PermissionConditionOperators.$GLOB], {
strictSlashes: false
})
) {
return false;
}
}
return true;
};
const isSubsetForSamePermissionSubjectAction = (
parentSetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
subsetRules: ReturnType<MongoAbility["possibleRulesFor"]>,
appendToMissingPermission: (condition?: MongoQuery) => void
) => {
const isMissingConditionInParent = parentSetRules.every((el) => !el.conditions);
if (isMissingConditionInParent) return true;
// all subset rules must pass in comparison to parent rul
return subsetRules.every((subsetRule) => {
const subsetRuleConditions = subsetRule.conditions as Record<string, TPermissionConditionShape | string>;
// compare subset rule with all parent rules
const isSubsetOfNonInvertedParentSet = parentSetRules
.filter((el) => !el.inverted)
.some((parentSetRule) => {
// get conditions and iterate
const parentSetRuleConditions = parentSetRule?.conditions as Record<string, TPermissionConditionShape | string>;
if (!parentSetRuleConditions) return true;
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
// if parent condition is missing then it's never a subset
if (!subsetRuleConditions?.[parentConditionField]) return false;
// standardize the conditions plain string operator => $eq function
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
});
});
const invertedParentSetRules = parentSetRules.filter((el) => el.inverted);
const isNotSubsetOfInvertedParentSet = invertedParentSetRules.length
? !invertedParentSetRules.some((parentSetRule) => {
// get conditions and iterate
const parentSetRuleConditions = parentSetRule?.conditions as Record<
string,
TPermissionConditionShape | string
>;
if (!parentSetRuleConditions) return true;
return Object.keys(parentSetRuleConditions).every((parentConditionField) => {
// if parent condition is missing then it's never a subset
if (!subsetRuleConditions?.[parentConditionField]) return false;
// standardize the conditions plain string operator => $eq function
const parentRuleConditionOperators = formatConditionOperator(parentSetRuleConditions[parentConditionField]);
const selectedSubsetRuleCondition = subsetRuleConditions?.[parentConditionField];
const subsetRuleConditionOperators = formatConditionOperator(selectedSubsetRuleCondition);
return isOperatorsASubset(parentRuleConditionOperators, subsetRuleConditionOperators);
});
})
: true;
const isSubset = isSubsetOfNonInvertedParentSet && isNotSubsetOfInvertedParentSet;
if (!isSubset) {
appendToMissingPermission(subsetRule.conditions);
}
return isSubset;
});
};
export const validatePermissionBoundary = (parentSetPermissions: MongoAbility, subsetPermissions: MongoAbility) => {
const checkedPermissionRules = new Set<string>();
const missingPermissions: TMissingPermission[] = [];
subsetPermissions.rules.forEach((subsetPermissionRules) => {
const subsetPermissionSubject = subsetPermissionRules.subject.toString();
let subsetPermissionActions: string[] = [];
// actions can be string or string[]
if (typeof subsetPermissionRules.action === "string") {
subsetPermissionActions.push(subsetPermissionRules.action);
} else {
subsetPermissionRules.action.forEach((subsetPermissionAction) => {
subsetPermissionActions.push(subsetPermissionAction);
});
}
// if action is already processed ignore
subsetPermissionActions = subsetPermissionActions.filter(
(el) => !checkedPermissionRules.has(getPermissionSetID(el, subsetPermissionSubject))
);
if (!subsetPermissionActions.length) return;
subsetPermissionActions.forEach((subsetPermissionAction) => {
const parentSetRulesOfSubset = parentSetPermissions.possibleRulesFor(
subsetPermissionAction,
subsetPermissionSubject
);
const nonInveretedOnes = parentSetRulesOfSubset.filter((el) => !el.inverted);
if (!nonInveretedOnes.length) {
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject });
return;
}
const subsetRules = subsetPermissions.possibleRulesFor(subsetPermissionAction, subsetPermissionSubject);
isSubsetForSamePermissionSubjectAction(parentSetRulesOfSubset, subsetRules, (conditions) => {
missingPermissions.push({ action: subsetPermissionAction, subject: subsetPermissionSubject, conditions });
});
});
subsetPermissionActions.forEach((el) =>
checkedPermissionRules.add(getPermissionSetID(el, subsetPermissionSubject))
);
});
if (missingPermissions.length) {
return { isValid: false as const, missingPermissions };
}
return { isValid: true };
};
+1 -38
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability"; import { buildMongoQueryMatcher } from "@casl/ability";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch"; import picomatch from "picomatch";
@@ -20,45 +20,8 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
*/
const extractPermissions = (ability: MongoAbility) => {
const permissions: string[] = [];
ability.rules.forEach((permission) => {
if (typeof permission.action === "string") {
permissions.push(`${permission.action}_${permission.subject as string}`);
} else {
permission.action.forEach((permissionAction) => {
permissions.push(`${permissionAction}_${permission.subject as string}`);
});
}
});
return permissions;
};
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
};
export enum PermissionConditionOperators { export enum PermissionConditionOperators {
$IN = "$in", $IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq", $EQ = "$eq",
$NEQ = "$ne", $NEQ = "$ne",
$GLOB = "$glob" $GLOB = "$glob"
+10 -1
View File
@@ -1,4 +1,5 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
export class DatabaseError extends Error { export class DatabaseError extends Error {
name: string; name: string;
@@ -52,10 +53,18 @@ export class ForbiddenRequestError extends Error {
error: unknown; error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) { details?: unknown;
constructor({
name,
error,
message,
details
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
super(message ?? "You are not allowed to access this resource"); super(message ?? "You are not allowed to access this resource");
this.name = name || "ForbiddenError"; this.name = name || "ForbiddenError";
this.error = error; this.error = error;
this.details = details;
} }
} }
+52 -38
View File
@@ -96,6 +96,7 @@ export const pingGatewayAndVerify = async ({
error: err as Error error: err as Error
}); });
}); });
for (let attempt = 1; attempt <= maxRetries; attempt += 1) { for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try { try {
const stream = quicClient.connection.newStream("bidi"); const stream = quicClient.connection.newStream("bidi");
@@ -108,17 +109,13 @@ export const pingGatewayAndVerify = async ({
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) { if (done) {
throw new BadRequestError({ throw new Error("Gateway closed before receiving PONG");
message: "Gateway closed before receiving PONG"
});
} }
const response = Buffer.from(value).toString(); const response = Buffer.from(value).toString();
if (response !== "PONG\n" && response !== "PONG") { if (response !== "PONG\n" && response !== "PONG") {
throw new BadRequestError({ throw new Error(`Failed to Ping. Unexpected response: ${response}`);
message: `Failed to Ping. Unexpected response: ${response}`
});
} }
reader.releaseLock(); reader.releaseLock();
@@ -146,6 +143,7 @@ interface TProxyServer {
server: net.Server; server: net.Server;
port: number; port: number;
cleanup: () => Promise<void>; cleanup: () => Promise<void>;
getProxyError: () => string;
} }
const setupProxyServer = async ({ const setupProxyServer = async ({
@@ -170,6 +168,7 @@ const setupProxyServer = async ({
error: err as Error error: err as Error
}); });
}); });
const proxyErrorMsg = [""];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = net.createServer(); const server = net.createServer();
@@ -185,31 +184,33 @@ const setupProxyServer = async ({
const forwardWriter = stream.writable.getWriter(); const forwardWriter = stream.writable.getWriter();
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`)); await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
forwardWriter.releaseLock(); forwardWriter.releaseLock();
/* eslint-disable @typescript-eslint/no-misused-promises */
// Set up bidirectional copy // Set up bidirectional copy
const setupCopy = async () => { const setupCopy = () => {
// Client to QUIC // Client to QUIC
// eslint-disable-next-line // eslint-disable-next-line
(async () => { (async () => {
try { const writer = stream.writable.getWriter();
const writer = stream.writable.getWriter();
// Create a handler for client data // Create a handler for client data
clientConn.on("data", async (chunk) => { clientConn.on("data", (chunk) => {
await writer.write(chunk); writer.write(chunk).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
}); });
});
// Handle client connection close // Handle client connection close
clientConn.on("end", async () => { clientConn.on("end", () => {
await writer.close(); writer.close().catch((err) => {
logger.error(err);
}); });
});
clientConn.on("error", async (err) => { clientConn.on("error", (clientConnErr) => {
await writer.abort(err); writer.abort(clientConnErr?.message).catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
}); });
} catch (err) { });
clientConn.destroy();
}
})(); })();
// QUIC to Client // QUIC to Client
@@ -238,15 +239,18 @@ const setupProxyServer = async ({
} }
} }
} catch (err) { } catch (err) {
proxyErrorMsg.push((err as Error)?.message);
clientConn.destroy(); clientConn.destroy();
} }
})(); })();
}; };
await setupCopy();
// setupCopy();
// Handle connection closure // Handle connection closure
clientConn.on("close", async () => { clientConn.on("close", () => {
await stream.destroy(); stream.destroy().catch((err) => {
proxyErrorMsg.push((err as Error)?.message);
});
}); });
const cleanup = async () => { const cleanup = async () => {
@@ -254,13 +258,18 @@ const setupProxyServer = async ({
await stream.destroy(); await stream.destroy();
}; };
clientConn.on("error", (err) => { clientConn.on("error", (clientConnErr) => {
logger.error(err, "Client socket error"); logger.error(clientConnErr, "Client socket error");
void cleanup(); cleanup().catch((err) => {
reject(err); logger.error(err, "Client conn cleanup");
});
}); });
clientConn.on("end", cleanup); clientConn.on("end", () => {
cleanup().catch((err) => {
logger.error(err, "Client conn end");
});
});
} catch (err) { } catch (err) {
logger.error(err, "Failed to establish target connection:"); logger.error(err, "Failed to establish target connection:");
clientConn.end(); clientConn.end();
@@ -272,12 +281,12 @@ const setupProxyServer = async ({
reject(err); reject(err);
}); });
server.on("close", async () => { server.on("close", () => {
await quicClient?.destroy(); quicClient?.destroy().catch((err) => {
logger.error(err, "Failed to destroy quic client");
});
}); });
/* eslint-enable */
server.listen(0, () => { server.listen(0, () => {
const address = server.address(); const address = server.address();
if (!address || typeof address === "string") { if (!address || typeof address === "string") {
@@ -293,7 +302,8 @@ const setupProxyServer = async ({
cleanup: async () => { cleanup: async () => {
server.close(); server.close();
await quicClient?.destroy(); await quicClient?.destroy();
} },
getProxyError: () => proxyErrorMsg.join(",")
}); });
}); });
}); });
@@ -316,7 +326,7 @@ export const withGatewayProxy = async (
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server // Setup the proxy server
const { port, cleanup } = await setupProxyServer({ const { port, cleanup, getProxyError } = await setupProxyServer({
targetHost, targetHost,
targetPort, targetPort,
relayPort, relayPort,
@@ -330,8 +340,12 @@ export const withGatewayProxy = async (
// Execute the callback with the allocated port // Execute the callback with the allocated port
await callback(port); await callback(port);
} catch (err) { } catch (err) {
logger.error(err, "Failed to proxy"); const proxyErrorMessage = getProxyError();
throw new BadRequestError({ message: (err as Error)?.message }); if (proxyErrorMessage) {
logger.error(new Error(proxyErrorMessage), "Failed to proxy");
}
logger.error(err, "Failed to do gateway");
throw new BadRequestError({ message: proxyErrorMessage || (err as Error)?.message });
} finally { } finally {
// Ensure cleanup happens regardless of success or failure // Ensure cleanup happens regardless of success or failure
await cleanup(); await cleanup();
+1 -1
View File
@@ -1,6 +1,6 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds const TURN_TOKEN_TTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => { export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
const timestamp = Math.floor((Date.now() + ttl) / 1000); const timestamp = Math.floor((Date.now() + ttl) / 1000);
const username = `${timestamp}:${id}`; const username = `${timestamp}:${id}`;
+8
View File
@@ -83,6 +83,14 @@ const run = async () => {
process.exit(0); process.exit(0);
}); });
process.on("uncaughtException", (error) => {
logger.error(error, "CRITICAL ERROR: Uncaught Exception");
});
process.on("unhandledRejection", (error) => {
logger.error(error, "CRITICAL ERROR: Unhandled Promise Rejection");
});
await server.listen({ await server.listen({
port: envConfig.PORT, port: envConfig.PORT,
host: envConfig.HOST, host: envConfig.HOST,
+2 -1
View File
@@ -21,6 +21,7 @@ import {
TQueueSecretSyncSyncSecretsByIdDTO, TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types"; } from "@app/services/secret-sync/secret-sync-types";
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
export enum QueueName { export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@@ -107,7 +108,7 @@ export type TQueueJobTypes = {
}; };
[QueueName.SecretWebhook]: { [QueueName.SecretWebhook]: {
name: QueueJobs.SecWebhook; name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string; depth?: number }; payload: TWebhookPayloads;
}; };
[QueueName.AccessTokenStatusUpdate]: [QueueName.AccessTokenStatusUpdate]:
+2 -1
View File
@@ -122,7 +122,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
reqId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.Forbidden, statusCode: HttpStatusCodes.Forbidden,
message: error.message, message: error.message,
error: error.name error: error.name,
details: error?.details
}); });
} else if (error instanceof RateLimitError) { } else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({ void res.status(HttpStatusCodes.TooManyRequests).send({
+1
View File
@@ -635,6 +635,7 @@ export const registerRoutes = async (
}); });
const superAdminService = superAdminServiceFactory({ const superAdminService = superAdminServiceFactory({
userDAL, userDAL,
identityDAL,
userAliasDAL, userAliasDAL,
authService: loginService, authService: loginService,
serverCfgDAL: superAdminDAL, serverCfgDAL: superAdminDAL,
+19 -1
View File
@@ -7,6 +7,7 @@ import {
ProjectRolesSchema, ProjectRolesSchema,
ProjectsSchema, ProjectsSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
SecretTagsSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -111,7 +112,16 @@ export const secretRawSchema = z.object({
secretReminderRepeatDays: z.number().nullable().optional(), secretReminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
actor: z
.object({
actorId: z.string().nullable().optional(),
actorType: z.string().nullable().optional(),
name: z.string().nullable().optional(),
membershipId: z.string().nullable().optional()
})
.optional()
.nullable()
}); });
export const ProjectPermissionSchema = z.object({ export const ProjectPermissionSchema = z.object({
@@ -232,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
kmsCertificateKeyId: true, kmsCertificateKeyId: true,
auditLogsRetentionDays: true auditLogsRetentionDays: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).extend({
name: z.string()
});
+44 -2
View File
@@ -1,7 +1,7 @@
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
import { z } from "zod"; import { z } from "zod";
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
querystring: z.object({ querystring: z.object({
searchTerm: z.string().default(""), searchTerm: z.string().default(""),
offset: z.coerce.number().default(0), offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20) limit: z.coerce.number().max(100).default(20),
// TODO: remove this once z.coerce.boolean() is supported
adminsOnly: z
.string()
.transform((val) => val === "true")
.default("false")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -149,6 +154,43 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/identity-management/identities",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
searchTerm: z.string().default(""),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20)
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({
name: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const identities = await server.services.superAdmin.getIdentities({
...req.query
});
return {
identities
};
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/integrations/slack/config", url: "/integrations/slack/config",
@@ -1,10 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionSecretActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
@@ -15,7 +16,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional(), .optional(),
@@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({ secrets = await server.services.secret.getSecretsRawMultiEnv({
viewSecretValue: true,
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
viewSecretValue: booleanSchema.default(true),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
@@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional(), .optional(),
@@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
const secretsRaw = await server.services.secret.getSecretsRaw({ secrets = (
actorId: req.permission.id, await server.services.secret.getSecretsRaw({
actor: req.permission.type, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actor: req.permission.type,
environment, viewSecretValue: req.query.viewSecretValue,
actorAuthMethod: req.permission.authMethod, throwOnMissingReadValuePermission: false,
projectId, actorOrgId: req.permission.orgId,
path: secretPath, environment,
orderBy, actorAuthMethod: req.permission.authMethod,
orderDirection, projectId,
search, path: secretPath,
limit: remainingLimit, orderBy,
offset: adjustedOffset, orderDirection,
tagSlugs: tags search,
}); limit: remainingLimit,
offset: adjustedOffset,
secrets = secretsRaw.secrets; tagSlugs: tags
})
).secrets;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
@@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional() .optional()
@@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.getSecretsRawByFolderMappings( const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{ {
filterByAction: ProjectPermissionSecretActions.DescribeSecret,
projectId, projectId,
folderMappings, folderMappings,
filters: { filters: {
@@ -846,6 +834,52 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/accessible-secrets",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
filterByAction: z
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
.default(ProjectPermissionSecretActions.ReadValue)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretValueHidden: z.boolean()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId, environment, secretPath, filterByAction } = req.query;
const { secrets } = await server.services.secret.getAccessibleSecrets({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
secretPath,
projectId,
filterByAction
});
return { secrets };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/secrets-by-keys", url: "/secrets-by-keys",
@@ -862,22 +896,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(), projectId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
keys: z.string().trim().transform(decodeURIComponent) keys: z.string().trim().transform(decodeURIComponent),
viewSecretValue: booleanSchema.default(false)
}), }),
response: { response: {
200: z.object({ 200: z.object({
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional() .optional()
@@ -886,7 +915,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { secretPath, projectId, environment } = req.query; const { secretPath, projectId, environment, viewSecretValue } = req.query;
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
@@ -895,6 +924,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
viewSecretValue,
environment, environment,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId, projectId,
-1
View File
@@ -91,7 +91,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await projectRouter.register(registerProjectMembershipRouter); await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerSecretTagRouter); await projectRouter.register(registerSecretTagRouter);
}, },
{ prefix: "/workspace" } { prefix: "/workspace" }
); );
@@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => { export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
message: z.string(),
user: UsersSchema, user: UsersSchema,
token: z.string() token: z.string(),
userEncryptionVersion: z.nativeEnum(UserEncryption)
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code); const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
return { return passwordReset;
message: "Successfully verified email",
user,
token
};
} }
}); });
+42 -2
View File
@@ -2,10 +2,12 @@ import { z } from "zod";
import { import {
IntegrationsSchema, IntegrationsSchema,
ProjectEnvironmentsSchema,
ProjectMembershipsSchema, ProjectMembershipsSchema,
ProjectRolesSchema, ProjectRolesSchema,
ProjectSlackConfigsSchema, ProjectSlackConfigsSchema,
ProjectType, ProjectType,
SecretFoldersSchema,
UserEncryptionKeysSchema, UserEncryptionKeysSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -307,7 +309,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.max(256, { message: "Description must be 256 or fewer characters" }) .max(256, { message: "Description must be 256 or fewer characters" })
.optional() .optional()
.describe(PROJECTS.UPDATE.projectDescription), .describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
slug: z
.string()
.trim()
.regex(
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
)
.max(64, { message: "Slug must be 64 characters or fewer" })
.optional()
.describe(PROJECTS.UPDATE.slug)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -325,7 +337,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
update: { update: {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization autoCapitalization: req.body.autoCapitalization,
slug: req.body.slug
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -664,4 +677,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return slackConfig; return slackConfig;
} }
}); });
server.route({
method: "GET",
url: "/:workspaceId/environment-folder-tree",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.record(
ProjectEnvironmentsSchema.extend({ folders: SecretFoldersSchema.extend({ path: z.string() }).array() })
)
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const environmentsFolders = await server.services.folder.getProjectEnvironmentsFolders(
req.params.workspaceId,
req.permission
);
return environmentsFolders;
}
});
}; };
+2
View File
@@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router"; import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router"; import { registerServiceTokenRouter } from "./service-token-router";
@@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" }); await server.register(registerMfaRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register( await server.register(
async (orgRouter) => { async (orgRouter) => {
await orgRouter.register(registerOrgRouter); await orgRouter.register(registerOrgRouter);
@@ -0,0 +1,53 @@
import { z } from "zod";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/password-reset",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
newPassword: z.string().trim()
})
},
handler: async (req) => {
const token = validatePasswordResetAuthorization(req.headers.authorization);
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.Recovery,
newPassword: req.body.newPassword,
userId: token.userId
});
}
});
server.route({
method: "POST",
url: "/user/password-reset",
schema: {
body: z.object({
oldPassword: z.string().trim(),
newPassword: z.string().trim()
})
},
config: {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.LoggedInReset,
userId: req.permission.id,
newPassword: req.body.newPassword,
oldPassword: req.body.oldPassword
});
}
});
};
+92 -91
View File
@@ -1,13 +1,7 @@
import picomatch from "picomatch"; import picomatch from "picomatch";
import { z } from "zod"; import { z } from "zod";
import { import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
SecretApprovalRequestsSchema,
SecretsSchema,
SecretTagsSchema,
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({ const SecretReferenceNode = z.object({
key: z.string(), key: z.string(),
@@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({
environment: z.string(), environment: z.string(),
secretPath: z.string() secretPath: z.string()
}); });
const convertStringBoolean = (defaultValue: boolean = false) => {
return z
.enum(["true", "false"])
.default(defaultValue ? "true" : "false")
.transform((value) => value === "true");
};
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] }; type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
@@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ tags: SanitizedTagSchema.array()
tags: SecretTagsSchema.pick({ })
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
})
)
}) })
} }
}, },
@@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
}) })
} }
@@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
expandSecretReferences: z viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue),
.enum(["true", "false"]) expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand),
.default("false") recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive),
.transform((value) => value === "true") include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports),
.describe(RAW_SECRETS.LIST.expand),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z tagSlugs: z
.string() .string()
.describe(RAW_SECRETS.LIST.tagSlugs) .describe(RAW_SECRETS.LIST.tagSlugs)
@@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array(), .array(),
imports: z imports: z
@@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true }) .omit({ createdAt: true, updatedAt: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
.array() .array()
@@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences, expandSecretReferences: req.query.expandSecretReferences,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
path: secretPath, path: secretPath,
metadataFilter: req.query.metadataFilter, metadataFilter: req.query.metadataFilter,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
@@ -376,10 +349,46 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
} }
return { secrets, imports }; return { secrets, imports };
} }
}); });
server.route({
method: "GET",
url: "/raw/id/:secretId",
config: {
rateLimit: secretsLimit
},
schema: {
params: z.object({
secretId: z.string()
}),
response: {
200: z.object({
secret: secretRawSchema.extend({
secretPath: z.string(),
tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { secretId } = req.params;
const secret = await server.services.secret.getSecretByIdRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretId
});
return { secret };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/raw/:secretName", url: "/raw/:secretName",
@@ -403,28 +412,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
expandSecretReferences: z viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue),
.enum(["true", "false"]) expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand),
.default("false") include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports)
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.expand),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.includeImports)
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: secretRawSchema.extend({ secret: secretRawSchema.extend({
tags: SecretTagsSchema.pick({ secretValueHidden: z.boolean(),
id: true, tags: SanitizedTagSchema.array().optional(),
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
}) })
@@ -456,6 +452,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences, expandSecretReferences: req.query.expandSecretReferences,
environment, environment,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
projectSlug: workspaceSlug, projectSlug: workspaceSlug,
path: secretPath, path: secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
@@ -662,7 +659,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -758,7 +757,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -780,6 +781,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
if (secretOperation.type === SecretProtectionType.Approval) { if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval }; return { approval: secretOperation.approval };
} }
const { secret } = secretOperation; const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -842,13 +844,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspace: z.string(), workspace: z.string(),
environment: z.string(), environment: z.string(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
.array(), .array(),
imports: z imports: z
@@ -944,10 +940,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
type: z.nativeEnum(SecretType).default(SecretType.Shared), type: z.nativeEnum(SecretType).default(SecretType.Shared),
version: z.coerce.number().optional(), version: z.coerce.number().optional(),
include_imports: z include_imports: convertStringBoolean()
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1218,6 +1211,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
z.object({ z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({ z.object({
secretValueHidden: z.boolean(),
_id: z.string(), _id: z.string(),
workspace: z.string(), workspace: z.string(),
environment: z.string() environment: z.string()
@@ -1387,13 +1381,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ _id: z.string(),
_id: z.string(), secretValueHidden: z.boolean(),
workspace: z.string(), workspace: z.string(),
environment: z.string() environment: z.string()
}) })
)
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -1705,7 +1698,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -1820,7 +1813,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() secrets: SecretsSchema.omit({ secretBlindIndex: true })
.extend({
secretValueHidden: z.boolean()
})
.array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -2082,7 +2079,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: secretRawSchema.array() secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -2204,7 +2201,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: secretRawSchema.array() secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
+30
View File
@@ -45,6 +45,36 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
if (decodedToken.userId !== userId) throw new UnauthorizedError(); if (decodedToken.userId !== userId) throw new UnauthorizedError();
}; };
export const validatePasswordResetAuthorization = (token?: string) => {
if (!token) throw new UnauthorizedError();
const appCfg = getConfig();
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw new UnauthorizedError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw new UnauthorizedError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) {
throw new UnauthorizedError({
message: `The provided authentication token type is not supported.`
});
}
return decodedToken;
};
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => { export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
if (isLocked) { if (isLocked) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -4,7 +4,10 @@ import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
@@ -12,10 +15,13 @@ import { TokenType } from "../auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
import { import {
ResetPasswordV2Type,
TChangePasswordDTO, TChangePasswordDTO,
TCreateBackupPrivateKeyDTO, TCreateBackupPrivateKeyDTO,
TResetPasswordV2DTO,
TResetPasswordViaBackupKeyDTO, TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type"; } from "./auth-password-type";
@@ -114,26 +120,31 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email * Email password reset flow via email. Step 1 send email
*/ */
const sendPasswordResetEmail = async (email: string) => { const sendPasswordResetEmail = async (email: string) => {
const user = await userDAL.findUserByUsername(email); const sendEmail = async () => {
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts const user = await userDAL.findUserByUsername(email);
if (!user || (user && !user.isAccepted)) return;
const cfg = getConfig(); if (user && user.isAccepted) {
const token = await tokenService.createTokenForUser({ const cfg = getConfig();
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, const token = await tokenService.createTokenForUser({
userId: user.id type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
}); userId: user.id
});
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.ResetPassword, template: SmtpTemplates.ResetPassword,
recipients: [email], recipients: [email],
subjectLine: "Infisical password reset", subjectLine: "Infisical password reset",
substitutions: { substitutions: {
email, email,
token, token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
}
});
} }
}); };
// note(daniel): run in background to prevent timing attacks
void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
}; };
/* /*
@@ -142,6 +153,11 @@ export const authPaswordServiceFactory = ({
const verifyPasswordResetEmail = async (email: string, code: string) => { const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig(); const cfg = getConfig();
const user = await userDAL.findUserByUsername(email); const user = await userDAL.findUserByUsername(email);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset"); throw new Error("Failed email verification for pass reset");
@@ -162,8 +178,91 @@ export const authPaswordServiceFactory = ({
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME } { expiresIn: cfg.JWT_SIGNUP_LIFETIME }
); );
return { token, user }; return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
}; };
const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
const cfg = getConfig();
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) {
throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
}
if (!user.hashedPassword) {
throw new BadRequestError({ message: "Unable to reset password, no password is set" });
}
if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
}
// we check the old password if the user is resetting their password while logged in
if (type === ResetPasswordV2Type.LoggedInReset) {
if (!oldPassword) {
throw new BadRequestError({ message: "Current password is required." });
}
const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
if (!isValid) {
throw new BadRequestError({ message: "Incorrect current password." });
}
}
const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
// we need to get the original private key first for v2
let privateKey: string;
if (
user.serverEncryptedPrivateKey &&
user.serverEncryptedPrivateKeyTag &&
user.serverEncryptedPrivateKeyIV &&
user.serverEncryptedPrivateKeyEncoding &&
user.encryptionVersion === UserEncryption.V2
) {
privateKey = infisicalSymmetricDecrypt({
iv: user.serverEncryptedPrivateKeyIV,
tag: user.serverEncryptedPrivateKeyTag,
ciphertext: user.serverEncryptedPrivateKey,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
} else {
throw new BadRequestError({
message: "Cannot reset password without current credentials or recovery method",
name: "Reset password"
});
}
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
publicKey: user.publicKey,
privateKey
});
const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
await userDAL.updateUserEncryptionByUserId(userId, {
hashedPassword: newHashedPassword,
// srp params
salt: encKeys.salt,
verifier: encKeys.verifier,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
});
await tokenService.revokeAllMySessions(userId);
};
/* /*
* Reset password of a user via backup key * Reset password of a user via backup key
* */ * */
@@ -391,6 +490,7 @@ export const authPaswordServiceFactory = ({
createBackupPrivateKey, createBackupPrivateKey,
getBackupPrivateKeyOfUser, getBackupPrivateKeyOfUser,
sendPasswordSetupEmail, sendPasswordSetupEmail,
setupPassword setupPassword,
resetPasswordV2
}; };
}; };
@@ -13,6 +13,18 @@ export type TChangePasswordDTO = {
password: string; password: string;
}; };
export enum ResetPasswordV2Type {
Recovery = "recovery",
LoggedInReset = "logged-in-reset"
}
export type TResetPasswordV2DTO = {
type: ResetPasswordV2Type;
userId: string;
newPassword: string;
oldPassword?: string;
};
export type TResetPasswordViaBackupKeyDTO = { export type TResetPasswordViaBackupKeyDTO = {
userId: string; userId: string;
protectedKey: string; protectedKey: string;
@@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
@@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor: {
type: actor,
actorId
},
tx tx
}); });
} }
@@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">; folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;
@@ -4,7 +4,7 @@ import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@@ -102,11 +102,13 @@ export const groupProjectServiceFactory = ({
project.id project.id
); );
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
if (!hasRequiredPrivileges) { throw new ForbiddenRequestError({
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); name: "PermissionBoundaryError",
} message: "Failed to assign group to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
} }
// validate custom roles input // validate custom roles input
@@ -267,12 +269,13 @@ export const groupProjectServiceFactory = ({
requestedRoleChange, requestedRoleChange,
project.id project.id
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
if (!hasRequiredPrivileges) { name: "PermissionBoundaryError",
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); message: "Failed to assign group to a more privileged role",
} details: { missingPermissions: permissionBoundary.missingPermissions }
});
} }
// validate custom roles input // validate custom roles input
@@ -7,7 +7,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -339,9 +339,12 @@ export const identityAwsAuthServiceFactory = ({
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke aws auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke aws auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
@@ -5,7 +5,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -312,9 +312,12 @@ export const identityAzureAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke azure auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke azure auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
@@ -5,7 +5,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -358,9 +358,12 @@ export const identityGcpAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke gcp auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke gcp auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
@@ -7,7 +7,7 @@ import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -78,14 +78,22 @@ export const identityJwtAuthServiceFactory = ({
let tokenData: Record<string, string | boolean | number> = {}; let tokenData: Record<string, string | boolean | number> = {};
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) { if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
const decryptedJwksCaCert = orgDataKeyDecryptor({ let client: JwksClient;
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert if (identityJwtAuth.jwksUrl.includes("https:")) {
}).toString(); const decryptedJwksCaCert = orgDataKeyDecryptor({
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
const client = new JwksClient({ }).toString();
jwksUri: identityJwtAuth.jwksUrl,
requestAgent const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
}); client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl,
requestAgent
});
} else {
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl
});
}
const { kid } = decodedToken.header; const { kid } = decodedToken.header;
const jwtSigningKey = await client.getSigningKey(kid); const jwtSigningKey = await client.getSigningKey(kid);
@@ -508,11 +516,13 @@ export const identityJwtAuthServiceFactory = ({
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) { const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke JWT auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke jwt auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
}
const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => {
const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx); const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx);
@@ -7,7 +7,7 @@ import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/sche
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -487,9 +487,12 @@ export const identityKubernetesAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke kubernetes auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke kubernetes auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
@@ -8,7 +8,7 @@ import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -428,11 +428,13 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) { const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke OIDC auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke oidc auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
}
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
@@ -4,7 +4,7 @@ import ms from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
@@ -91,11 +91,13 @@ export const identityProjectServiceFactory = ({
projectId projectId
); );
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
if (!hasRequiredPriviledges) { throw new ForbiddenRequestError({
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); name: "PermissionBoundaryError",
} message: "Failed to assign to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
} }
// validate custom roles input // validate custom roles input
@@ -185,9 +187,13 @@ export const identityProjectServiceFactory = ({
projectId projectId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) { const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); if (!permissionBoundary.isValid)
} throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to change to a more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
} }
// validate custom roles input // validate custom roles input
@@ -277,8 +283,13 @@ export const identityProjectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
if (!isAtLeastAsPrivileged(permission, identityRolePermission)) const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to remove more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
return deletedIdentity; return deletedIdentity;
@@ -5,7 +5,7 @@ import { IdentityAuthMethod, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
@@ -245,11 +245,13 @@ export const identityTokenAuthServiceFactory = ({
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) { const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke Token Auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke token auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
}
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
@@ -295,10 +297,12 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!hasPriviledge) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to create token for identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to create token for identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
@@ -415,10 +419,12 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!hasPriviledge) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to update token for identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to update token for identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const [token] = await identityAccessTokenDAL.update( const [token] = await identityAccessTokenDAL.update(
@@ -8,7 +8,7 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
@@ -367,9 +367,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke universal auth of identity with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke universal auth of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
@@ -414,10 +417,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!hasPriviledge) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role" name: "PermissionBoundaryError",
message: "Failed to create client secret for a more privileged identity.",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const appCfg = getConfig(); const appCfg = getConfig();
@@ -475,9 +480,12 @@ export const identityUaServiceFactory = ({
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role" name: "PermissionBoundaryError",
message: "Failed to get identity client secret with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const identityUniversalAuth = await identityUaDAL.findOne({ const identityUniversalAuth = await identityUaDAL.findOne({
@@ -524,9 +532,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
if (!isAtLeastAsPrivileged(permission, rolePermission)) const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to read identity client secret of project with more privileged role" name: "PermissionBoundaryError",
message: "Failed to read identity client secret of identity with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId); const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
@@ -566,10 +577,12 @@ export const identityUaServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!isAtLeastAsPrivileged(permission, rolePermission)) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Failed to revoke identity client secret with more privileged role" name: "PermissionBoundaryError",
message: "Failed to revoke identity client secret with more privileged role",
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
+35 -3
View File
@@ -1,10 +1,42 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName, TIdentities } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { DatabaseError } from "@app/lib/errors";
export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>; export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
export const identityDALFactory = (db: TDbClient) => { export const identityDALFactory = (db: TDbClient) => {
const identityOrm = ormify(db, TableName.Identity); const identityOrm = ormify(db, TableName.Identity);
return identityOrm;
const getIdentitiesByFilter = async ({
limit,
offset,
searchTerm,
sortBy
}: {
limit: number;
offset: number;
searchTerm: string;
sortBy?: keyof TIdentities;
}) => {
try {
let query = db.replicaNode()(TableName.Identity);
if (searchTerm) {
query = query.where((qb) => {
void qb.whereILike("name", `%${searchTerm}%`);
});
}
if (sortBy) {
query = query.orderBy(sortBy);
}
return await query.limit(limit).offset(offset).select(selectAllTableCols(TableName.Identity));
} catch (error) {
throw new DatabaseError({ error, name: "Get identities by filter" });
}
};
return { ...identityOrm, getIdentitiesByFilter };
}; };
@@ -4,7 +4,7 @@ import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@@ -58,9 +58,13 @@ export const identityServiceFactory = ({
orgId orgId
); );
const isCustomRole = Boolean(customRole); const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to create a more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -129,9 +133,13 @@ export const identityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to update a more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
let customRole: TOrgRoles | undefined; let customRole: TOrgRoles | undefined;
if (role) { if (role) {
@@ -141,9 +149,13 @@ export const identityServiceFactory = ({
); );
const isCustomRole = Boolean(customOrgRole); const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); const appliedRolePermissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!hasRequiredNewRolePermission) if (!appliedRolePermissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to create a more privileged identity",
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
});
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = customOrgRole;
} }
@@ -216,9 +228,13 @@ export const identityServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
if (!hasRequiredPriviledges) if (!permissionBoundary.isValid)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); throw new ForbiddenRequestError({
name: "PermissionBoundaryError",
message: "Failed to delete more privileged identity",
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const deletedIdentity = await identityDAL.deleteById(id); const deletedIdentity = await identityDAL.deleteById(id);
@@ -114,20 +114,27 @@ export const integrationAuthServiceFactory = ({
const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => { const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => {
const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string); const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string);
return Promise.all( const filteredAuthorizations = await Promise.all(
authorizations.filter(async (auth) => { authorizations.map(async (auth) => {
const { permission } = await permissionService.getProjectPermission({ try {
actor, const { permission } = await permissionService.getProjectPermission({
actorId, actor,
projectId: auth.projectId, actorId,
actorAuthMethod, projectId: auth.projectId,
actorOrgId, actorAuthMethod,
actionProjectType: ActionProjectType.SecretManager actorOrgId,
}); actionProjectType: ActionProjectType.SecretManager
});
return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); return permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations) ? auth : null;
} catch (error) {
// user does not belong to the project that the integration auth belongs to
return null;
}
}) })
); );
return filteredAuthorizations.filter((auth): auth is NonNullable<typeof auth> => auth !== null);
}; };
const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => { const getIntegrationAuth = async ({ actor, id, actorId, actorAuthMethod, actorOrgId }: TGetIntegrationAuthDTO) => {
@@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async (
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true hasSecretAccess: () => true,
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -1,8 +1,13 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -91,13 +96,10 @@ export const integrationServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: sourceEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath
environment: sourceEnvironment, });
secretPath
})
);
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
if (!folder) { if (!folder) {
@@ -174,13 +176,10 @@ export const integrationServiceFactory = ({
const newSecretPath = secretPath || integration.secretPath; const newSecretPath = secretPath || integration.secretPath;
if (environment || secretPath) { if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: newEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath: newSecretPath
environment: newEnvironment, });
secretPath: newSecretPath
})
);
} }
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath); const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
@@ -7,7 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
@@ -274,13 +274,13 @@ export const projectMembershipServiceFactory = ({
projectId projectId
); );
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
if (!permissionBoundary.isValid)
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `Failed to change to a more privileged role ${requestedRoleChange}` name: "PermissionBoundaryError",
message: `Failed to change to a more privileged role ${requestedRoleChange}`,
details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
}
} }
// validate custom roles input // validate custom roles input
@@ -10,8 +10,13 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
@@ -563,11 +568,24 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (update.slug) {
const existingProject = await projectDAL.findOne({
slug: update.slug,
orgId: actorOrgId
});
if (existingProject && existingProject.id !== project.id) {
throw new BadRequestError({
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
});
}
}
const updatedProject = await projectDAL.updateById(project.id, { const updatedProject = await projectDAL.updateById(project.id, {
name: update.name, name: update.name,
description: update.description, description: update.description,
autoCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization enforceCapitalization: update.autoCapitalization,
slug: update.slug
}); });
return updatedProject; return updatedProject;
@@ -747,7 +765,7 @@ export const projectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
const project = await projectDAL.findProjectById(projectId); const project = await projectDAL.findProjectById(projectId);
@@ -82,6 +82,7 @@ export type TUpdateProjectDTO = {
name?: string; name?: string;
description?: string; description?: string;
autoCapitalization?: boolean; autoCapitalization?: boolean;
slug?: string;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -0,0 +1,17 @@
import { TSecretFolders } from "@app/db/schemas";
import { InternalServerError } from "@app/lib/errors";
export const buildFolderPath = (
folder: TSecretFolders,
foldersMap: Record<string, TSecretFolders>,
depth: number = 0
): string => {
if (depth > 20) {
throw new InternalServerError({ message: "Maximum folder depth of 20 exceeded" });
}
if (!folder.parentId) {
return depth === 0 ? "/" : "";
}
return `${buildFolderPath(foldersMap[folder.parentId], foldersMap, depth + 1)}/${folder.name}`;
};
@@ -8,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -27,7 +28,7 @@ type TSecretFolderServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
folderDAL: TSecretFolderDALFactory; folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
folderVersionDAL: TSecretFolderVersionDALFactory; folderVersionDAL: TSecretFolderVersionDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
}; };
@@ -580,6 +581,44 @@ export const secretFolderServiceFactory = ({
return folders; return folders;
}; };
const getProjectEnvironmentsFolders = async (projectId: string, actor: OrgServiceActor) => {
// folder list is allowed to be read by anyone
// permission is to check if user has access
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager
});
const environments = await projectEnvDAL.find({ projectId });
const folders = await folderDAL.find({
$in: {
envId: environments.map((env) => env.id)
},
isReserved: false
});
const environmentFolders = Object.fromEntries(
environments.map((env) => {
const relevantFolders = folders.filter((folder) => folder.envId === env.id);
const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder]));
const foldersWithPath = relevantFolders.map((folder) => ({
...folder,
path: buildFolderPath(folder, foldersMap)
}));
return [env.slug, { ...env, folders: foldersWithPath }];
})
);
return environmentFolders;
};
return { return {
createFolder, createFolder,
updateFolder, updateFolder,
@@ -589,6 +628,7 @@ export const secretFolderServiceFactory = ({
getFolderById, getFolderById,
getProjectFolderCount, getProjectFolderCount,
getFoldersMultiEnv, getFoldersMultiEnv,
getFoldersDeepByEnvs getFoldersDeepByEnvs,
getProjectEnvironmentsFolders
}; };
}; };
@@ -3,6 +3,7 @@ import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretImportDALFactory } from "./secret-import-dal"; import { TSecretImportDALFactory } from "./secret-import-dal";
@@ -32,6 +33,12 @@ type TSecretImportSecretsV2 = {
folderId: string | undefined; folderId: string | undefined;
importFolderId: string; importFolderId: string;
secrets: (TSecretsV2 & { secrets: (TSecretsV2 & {
secretTags: {
slug: string;
name: string;
color?: string | null;
id: string;
}[];
workspace: string; workspace: string;
environment: string; environment: string;
_id: string; _id: string;
@@ -39,6 +46,7 @@ type TSecretImportSecretsV2 = {
// akhilmhdh: yes i know you can put ?. // akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things // But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string; secretValue: string;
secretValueHidden: boolean;
secretComment: string; secretComment: string;
secretMetadata?: ResourceMetadataDTO; secretMetadata?: ResourceMetadataDTO;
})[]; })[];
@@ -150,12 +158,14 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL, secretImportDAL,
decryptor, decryptor,
expandSecretReferences, expandSecretReferences,
hasSecretAccess hasSecretAccess,
viewSecretValue
}: { }: {
secretImports: (Omit<TSecretImports, "importEnv"> & { secretImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string }; importEnv: { id: string; slug: string; name: string };
})[]; })[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
viewSecretValue: boolean;
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
decryptor: (value?: Buffer | null) => string; decryptor: (value?: Buffer | null) => string;
@@ -168,9 +178,14 @@ export const fnSecretsV2FromImports = async ({
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => { }) => {
const cyclicDetector = new Set(); const cyclicDetector = new Set();
const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ const stack: {
{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } secretImports: typeof rootSecretImports;
]; depth: number;
parentImportedSecrets: (TSecretsV2 & {
secretValueHidden: boolean;
secretTags: { slug: string; name: string; id: string; color?: string | null }[];
})[];
}[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }];
const processedImports: TSecretImportSecretsV2[] = []; const processedImports: TSecretImportSecretsV2[] = [];
@@ -229,7 +244,9 @@ export const fnSecretsV2FromImports = async ({
.map((item) => ({ .map((item) => ({
...item, ...item,
secretKey: item.key, secretKey: item.key,
secretValue: decryptor(item.encryptedValue), secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: !viewSecretValue,
secretTags: item.tags,
secretComment: decryptor(item.encryptedComment), secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug, environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
@@ -267,6 +284,8 @@ export const fnSecretsV2FromImports = async ({
processedImport.secrets = unique(processedImport.secrets, (i) => i.key); processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
return Promise.allSettled( return Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => { processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecretReferences({ const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath, secretPath: processedImport.secretPath,
@@ -4,8 +4,16 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -89,13 +97,11 @@ export const secretImportServiceFactory = ({
); );
// check if user has permission to import from target path // check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment: data.environment,
subject(ProjectPermissionSub.Secrets, { secretPath: data.path
environment: data.environment, });
secretPath: data.path
})
);
if (isReplication) { if (isReplication) {
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) { if (!plan.secretApproval) {
@@ -401,13 +407,10 @@ export const secretImportServiceFactory = ({
if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" });
// check if user has permission to import from target path // check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment: secretImportDoc.importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: secretImportDoc.importPath
environment: secretImportDoc.importEnv.slug, });
secretPath: secretImportDoc.importPath
})
);
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -595,14 +598,12 @@ export const secretImportServiceFactory = ({
// so anything based on this order will also be in right position // so anything based on this order will also be in right position
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
const allowedImports = secretImports.filter((el) => const allowedImports = secretImports.filter((el) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: el.importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: el.importPath
environment: el.importEnv.slug, })
secretPath: el.importPath
})
)
); );
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
}; };
@@ -642,20 +643,19 @@ export const secretImportServiceFactory = ({
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
secretImports, secretImports,
folderDAL, folderDAL,
viewSecretValue: true,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: expandEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath: expandSecretPath,
environment: expandEnvironment, secretName: expandSecretKey,
secretPath: expandSecretPath, secretTags: expandSecretTags
secretName: expandSecretKey, })
secretTags: expandSecretTags
})
)
}); });
return importedSecrets; return importedSecrets;
} }
@@ -666,13 +666,10 @@ export const secretImportServiceFactory = ({
}); });
const allowedImports = secretImports.filter((el) => const allowedImports = secretImports.filter((el) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: el.importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: el.importPath
environment: el.importEnv.slug, })
secretPath: el.importPath
})
)
); );
const importedSecrets = await fnSecretsFromImports({ const importedSecrets = await fnSecretsFromImports({
allowedImports, allowedImports,
@@ -683,7 +680,10 @@ export const secretImportServiceFactory = ({
return importedSecrets.map((el) => ({ return importedSecrets.map((el) => ({
...el, ...el,
secrets: el.secrets.map((encryptedSecret) => secrets: el.secrets.map((encryptedSecret) =>
decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) decryptSecretRaw(
{ ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false },
botKey
)
) )
})); }));
}; };
@@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({
expandSecretReferences, expandSecretReferences,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true hasSecretAccess: () => true,
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -1,9 +1,10 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
ProjectPermissionActions, ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
@@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({
ProjectPermissionSub.SecretSyncs ProjectPermissionSub.SecretSyncs
); );
ForbiddenError.from(projectPermission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { secretPath
environment, });
secretPath
})
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@@ -269,13 +267,10 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath) if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" }); throw new BadRequestError({ message: "Must specify both source environment and secret path" });
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: updatedEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath: updatedSecretPath
environment: updatedEnvironment, });
secretPath: updatedSecretPath
})
);
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath); const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
@@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
throw new DatabaseError({ error, name: "Find all by ids" }); throw new DatabaseError({ error, name: "Find all by ids" });
} }
}; };
return { return {
...secretTagOrm, ...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany, saveTagsToSecret: secretJnTagOrm.insertMany,
@@ -613,6 +613,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id` `${TableName.SecretTag}.id`
) )
.leftJoin(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2)) .select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
@@ -622,12 +625,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
); )
.select(db.ref("projectId").withSchema(TableName.Environment).as("projectId"));
const docs = sqlNestRelationships({ const docs = sqlNestRelationships({
data: rawDocs, data: rawDocs,
key: "id", key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }), parentMapper: (el) => ({ _id: el.id, projectId: el.projectId, ...SecretsV2Schema.parse(el) }),
childrenMapper: [ childrenMapper: [
{ {
key: "tagId", key: "tagId",
@@ -5,8 +5,10 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -62,6 +64,7 @@ export const fnSecretBulkInsert = async ({
resourceMetadataDAL, resourceMetadataDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
actor,
tx tx
}: TFnSecretBulkInsert) => { }: TFnSecretBulkInsert) => {
const sanitizedInputSecrets = inputSecrets.map( const sanitizedInputSecrets = inputSecrets.map(
@@ -90,6 +93,10 @@ export const fnSecretBulkInsert = async ({
}) })
); );
const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined;
const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined;
const actorType = actor?.type || ActorType.PLATFORM;
const newSecrets = await secretDAL.insertMany( const newSecrets = await secretDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ ...el, folderId })), sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx tx
@@ -102,10 +109,14 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
})) }))
); );
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,
folderId, folderId,
userActorId,
identityActorId,
actorType,
secretId: newSecretGroupedByKeyName[el.key][0].id secretId: newSecretGroupedByKeyName[el.key][0].id
})), })),
tx tx
@@ -137,6 +148,7 @@ export const fnSecretBulkInsert = async ({
if (newSecretTags.length) { if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId [`${TableName.SecretTag}Id` as const]: secret_tagsId
@@ -145,7 +157,16 @@ export const fnSecretBulkInsert = async ({
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
} }
return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); const secretsWithTags = await secretDAL.find(
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const fnSecretBulkUpdate = async ({ export const fnSecretBulkUpdate = async ({
@@ -157,8 +178,13 @@ export const fnSecretBulkUpdate = async ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
resourceMetadataDAL resourceMetadataDAL,
actor
}: TFnSecretBulkUpdate) => { }: TFnSecretBulkUpdate) => {
const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined;
const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined;
const actorType = actor?.type || ActorType.PLATFORM;
const sanitizedInputSecrets = inputSecrets.map( const sanitizedInputSecrets = inputSecrets.map(
({ ({
filter, filter,
@@ -216,7 +242,10 @@ export const fnSecretBulkUpdate = async ({
encryptedValue, encryptedValue,
reminderRepeatDays, reminderRepeatDays,
folderId, folderId,
secretId secretId,
userActorId,
identityActorId,
actorType
}) })
), ),
tx tx
@@ -283,7 +312,15 @@ export const fnSecretBulkUpdate = async ({
tx tx
); );
return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); const secretsWithTags = await secretDAL.find(
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const fnSecretBulkDelete = async ({ export const fnSecretBulkDelete = async ({
@@ -516,7 +553,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey); const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
}); });
const cacheKey = getCacheUniqueKey(environment, secretPath); const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -535,7 +572,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
}); });
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -616,6 +653,12 @@ export const reshapeBridgeSecret = (
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & { secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
value: string; value: string;
comment: string; comment: string;
userActorName?: string | null;
identityActorName?: string | null;
userActorId?: string | null;
identityActorId?: string | null;
membershipId?: string | null;
actorType?: string | null;
tags?: { tags?: {
id: string; id: string;
slug: string; slug: string;
@@ -623,19 +666,27 @@ export const reshapeBridgeSecret = (
name: string; name: string;
}[]; }[];
secretMetadata?: ResourceMetadataDTO; secretMetadata?: ResourceMetadataDTO;
} },
secretValueHidden: boolean
) => ({ ) => ({
secretKey: secret.key, secretKey: secret.key,
secretPath, secretPath,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
secretValue: secret.value || "",
secretComment: secret.comment || "", secretComment: secret.comment || "",
version: secret.version, version: secret.version,
type: secret.type, type: secret.type,
_id: secret.id, _id: secret.id,
id: secret.id, id: secret.id,
user: secret.userId, user: secret.userId,
actor: secret.actorType
? {
actorType: secret.actorType,
actorId: secret.userActorId || secret.identityActorId,
name: secret.identityActorName || secret.userActorName,
membershipId: secret.membershipId
}
: undefined,
tags: secret.tags, tags: secret.tags,
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.reminderRepeatDays, secretReminderRepeatDays: secret.reminderRepeatDays,
@@ -643,5 +694,15 @@ export const reshapeBridgeSecret = (
metadata: secret.metadata, metadata: secret.metadata,
secretMetadata: secret.secretMetadata, secretMetadata: secret.secretMetadata,
createdAt: secret.createdAt, createdAt: secret.createdAt,
updatedAt: secret.updatedAt updatedAt: secret.updatedAt,
...(secretValueHidden
? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true
}
: {
secretValue: secret.value || "",
secretValueHidden: false
})
}); });
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -36,6 +37,8 @@ export type TGetSecretsDTO = {
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
metadataFilter?: { metadataFilter?: {
key?: string; key?: string;
value?: string; value?: string;
@@ -48,6 +51,11 @@ export type TGetSecretsDTO = {
keys?: string[]; keys?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
TGetSecretsDTO,
"viewSecretValue" | "recursive" | "expandSecretReferences"
>;
export type TGetASecretDTO = { export type TGetASecretDTO = {
secretName: string; secretName: string;
path: string; path: string;
@@ -57,6 +65,7 @@ export type TGetASecretDTO = {
includeImports?: boolean; includeImports?: boolean;
version?: number; version?: number;
projectId: string; projectId: string;
viewSecretValue: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateSecretDTO = TProjectPermission & { export type TCreateSecretDTO = TProjectPermission & {
@@ -164,10 +173,14 @@ export type TFnSecretBulkInsert = {
} }
>; >;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
actorId: string;
};
}; };
type TRequireReferenceIfValue = type TRequireReferenceIfValue =
@@ -188,10 +201,14 @@ export type TFnSecretBulkUpdate = {
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[]; }[];
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">; secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: {
type: string;
actorId: string;
};
tx?: Knex; tx?: Knex;
}; };
@@ -332,4 +349,12 @@ export type TGetSecretsRawByFolderMappingsDTO = {
folderMappings: { folderId: string; path: string; environment: string }[]; folderMappings: { folderId: string; path: string; environment: string }[];
userId: string; userId: string;
filters: TFindSecretsByFolderIdsFilter; filters: TFindSecretsByFolderIdsFilter;
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
}; };
export type TGetAccessibleSecretsDTO = {
environment: string;
projectId: string;
secretPath: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -12,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2Bridge
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersionsV2> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersionV2)
.where(`${TableName.SecretVersionV2}.secretId`, secretId)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder // This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try { try {
@@ -119,11 +172,101 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`); logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
}; };
const findVersionsBySecretIdWithActors = async (
secretId: string,
projectId: string,
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
tx?: Knex
) => {
try {
const query = (tx || db)(TableName.SecretVersionV2)
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
.leftJoin(
TableName.ProjectMembership,
`${TableName.ProjectMembership}.userId`,
`${TableName.SecretVersionV2}.userActorId`
)
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.where((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
})
.orWhere((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
})
.select(
selectAllTableCols(TableName.SecretVersionV2),
db.ref("username").withSchema(TableName.Users).as("userActorName"),
db.ref("name").withSchema(TableName.Identity).as("identityActorName"),
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
);
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(
sort.map(([column, order, nulls]) => ({
column: `${TableName.SecretVersionV2}.${column as string}`,
order,
nulls
}))
);
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({
_id: el.id,
...SecretVersionsV2Schema.parse(el),
userActorName: el.userActorName,
identityActorName: el.identityActorName,
membershipId: el.membershipId
}),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
}
};
return { return {
...secretVersionV2Orm, ...secretVersionV2Orm,
pruneExcessVersions, pruneExcessVersions,
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId findLatestVersionByFolderId,
findVersionsBySecretIdWithActors,
findBySecretId
}; };
}; };
+44 -1
View File
@@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const findManySecretsWithTags = async (
filter: {
secretIds: string[];
type: SecretType;
},
tx?: Knex
) => {
try {
const secrets = await (tx || db.replicaNode())(TableName.Secret)
.whereIn(`${TableName.Secret}.id` as "id", filter.secretIds)
.where("type", filter.type)
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
const data = sqlNestRelationships({
data: secrets,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get many secrets with tags" });
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try { try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
@@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => {
upsertSecretReferences, upsertSecretReferences,
findReferencedSecretReferences, findReferencedSecretReferences,
findAllProjectSecretValues, findAllProjectSecretValues,
pruneSecretReminders pruneSecretReminders,
findManySecretsWithTags
}; };
}; };
+39 -15
View File
@@ -1,5 +1,4 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path"; import path from "path";
import { import {
@@ -12,8 +11,9 @@ import {
TSecretFolders, TSecretFolders,
TSecrets TSecrets
} from "@app/db/schemas"; } from "@app/db/schemas";
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { import {
buildSecretBlindIndexFromName, buildSecretBlindIndexFromName,
@@ -51,6 +51,8 @@ import {
TUpdateManySecretsRawFnFactory TUpdateManySecretsRawFnFactory
} from "./secret-types"; } from "./secret-types";
export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = "<hidden-by-infisical>";
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig(); const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({ const secretBlindIndex = await buildSecretBlindIndexFromName({
@@ -189,13 +191,10 @@ export const recursivelyGetSecretPaths = ({
// Filter out paths that the user does not have permission to access, and paths that are not in the current path // Filter out paths that the user does not have permission to access, and paths that are not in the current path
const allowedPaths = paths.filter( const allowedPaths = paths.filter(
(folder) => (folder) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { secretPath: folder.path
environment, }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
secretPath: folder.path
})
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
); );
return allowedPaths; return allowedPaths;
@@ -344,6 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
export const decryptSecretRaw = ( export const decryptSecretRaw = (
secret: TSecrets & { secret: TSecrets & {
secretValueHidden: boolean;
workspace: string; workspace: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -362,12 +362,14 @@ export const decryptSecretRaw = (
key key
}); });
const secretValue = decryptSymmetric128BitHexKeyUTF8({ const secretValue = !secret.secretValueHidden
ciphertext: secret.secretValueCiphertext, ? decryptSymmetric128BitHexKeyUTF8({
iv: secret.secretValueIV, ciphertext: secret.secretValueCiphertext,
tag: secret.secretValueTag, iv: secret.secretValueIV,
key tag: secret.secretValueTag,
}); key
})
: INFISICAL_SECRET_VALUE_HIDDEN_MASK;
let secretComment = ""; let secretComment = "";
@@ -385,6 +387,7 @@ export const decryptSecretRaw = (
secretPath: secret.secretPath, secretPath: secret.secretPath,
workspace: secret.workspace, workspace: secret.workspace,
environment: secret.environment, environment: secret.environment,
secretValueHidden: secret.secretValueHidden,
secretValue, secretValue,
secretComment, secretComment,
version: secret.version, version: secret.version,
@@ -579,6 +582,7 @@ export const fnSecretBulkInsert = async ({
[`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id [`${TableName.Secret}Id` as const]: newSecretGroupByBlindIndex[secretBlindIndex as string][0].id
})) }))
); );
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,
@@ -1197,3 +1201,23 @@ export const fnDeleteProjectSecretReminders = async (
} }
} }
}; };
export const conditionallyHideSecretValue = (
shouldHideValue: boolean,
{
secretValueCiphertext,
secretValueIV,
secretValueTag
}: {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
) => {
return {
secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext,
secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV,
secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag,
secretValueHidden: shouldHideValue
};
};
+32 -5
View File
@@ -61,6 +61,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { WebhookEvents } from "../webhook/webhook-types";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { interpolateSecrets } from "./secret-fns"; import { interpolateSecrets } from "./secret-fns";
import { import {
@@ -402,7 +403,8 @@ export const secretQueueFactory = ({
expandSecretReferences, expandSecretReferences,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true hasSecretAccess: () => true,
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -623,7 +625,14 @@ export const secretQueueFactory = ({
await queueService.queue( await queueService.queue(
QueueName.SecretWebhook, QueueName.SecretWebhook,
QueueJobs.SecWebhook, QueueJobs.SecWebhook,
{ environment, projectId, secretPath }, {
type: WebhookEvents.SecretModified,
payload: {
environment,
projectId,
secretPath
}
},
{ {
jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`, jobId: `secret-webhook-${environment}-${projectId}-${secretPath}`,
removeOnFail: { count: 5 }, removeOnFail: { count: 5 },
@@ -1055,6 +1064,8 @@ export const secretQueueFactory = ({
const organization = await orgDAL.findOrgByProjectId(projectId); const organization = await orgDAL.findOrgByProjectId(projectId);
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
const secret = await secretV2BridgeDAL.findById(data.secretId);
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
if (!organization) { if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`); logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
@@ -1083,6 +1094,19 @@ export const secretQueueFactory = ({
organizationName: organization.name organizationName: organization.name
} }
}); });
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, {
type: WebhookEvents.SecretReminderExpired,
payload: {
projectName: project.name,
projectId: project.id,
secretPath: folder?.path,
environment: folder?.environmentSlug || "",
reminderNote: data.note,
secretName: secret?.key,
secretId: data.secretId
}
});
}); });
const startSecretV2Migration = async (projectId: string) => { const startSecretV2Migration = async (projectId: string) => {
@@ -1490,14 +1514,17 @@ export const secretQueueFactory = ({
queueService.start(QueueName.SecretWebhook, async (job) => { queueService.start(QueueName.SecretWebhook, async (job) => {
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId: job.data.projectId projectId: job.data.payload.projectId
}); });
await fnTriggerWebhook({ await fnTriggerWebhook({
...job.data, projectId: job.data.payload.projectId,
environment: job.data.payload.environment,
secretPath: job.data.payload.secretPath || "/",
projectEnvDAL, projectEnvDAL,
webhookDAL,
projectDAL, projectDAL,
webhookDAL,
event: job.data,
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString() secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
}); });
}); });
+308 -100
View File
@@ -6,14 +6,23 @@ import {
ActionProjectType, ActionProjectType,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUpgradeStatus, ProjectUpgradeStatus,
ProjectVersion,
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretsSchema, SecretsSchema,
SecretType SecretType
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; 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 { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
@@ -48,6 +57,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { import {
conditionallyHideSecretValue,
decryptSecretRaw, decryptSecretRaw,
fnSecretBlindIndexCheck, fnSecretBlindIndexCheck,
fnSecretBulkDelete, fnSecretBulkDelete,
@@ -71,6 +81,8 @@ import {
TDeleteManySecretRawDTO, TDeleteManySecretRawDTO,
TDeleteSecretDTO, TDeleteSecretDTO,
TDeleteSecretRawDTO, TDeleteSecretRawDTO,
TGetAccessibleSecretsDTO,
TGetASecretByIdRawDTO,
TGetASecretDTO, TGetASecretDTO,
TGetASecretRawDTO, TGetASecretRawDTO,
TGetSecretAccessListDTO, TGetSecretAccessListDTO,
@@ -95,7 +107,7 @@ type TSecretServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findSecretPathByFolderIds"
>; >;
secretV2BridgeService: TSecretV2BridgeServiceFactory; secretV2BridgeService: TSecretV2BridgeServiceFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
@@ -204,7 +216,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -322,7 +334,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -444,7 +456,23 @@ export const secretServiceFactory = ({
environmentSlug: folder.environment.slug environmentSlug: folder.environment.slug
}); });
} }
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return {
...updatedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]),
workspace: projectId,
environment,
secretPath: path
};
}; };
const deleteSecret = async ({ const deleteSecret = async ({
@@ -467,7 +495,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -540,7 +568,23 @@ export const secretServiceFactory = ({
}); });
} }
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return {
...deletedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]),
_id: deletedSecret[0].id,
workspace: projectId,
environment,
secretPath: path
};
}; };
const getSecrets = async ({ const getSecrets = async ({
@@ -588,10 +632,10 @@ export const secretServiceFactory = ({
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else { } else {
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] }; if (!folder) return { secrets: [], imports: [] };
@@ -613,13 +657,10 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: importPath
environment: importEnv.slug, })
secretPath: importPath
})
)
); );
const importedSecrets = await fnSecretsFromImports({ const importedSecrets = await fnSecretsFromImports({
allowedImports, allowedImports,
@@ -670,10 +711,11 @@ export const secretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) if (!folder)
throw new NotFoundError({ throw new NotFoundError({
@@ -720,14 +762,12 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: importPath
environment: importEnv.slug, })
secretPath: importPath
})
)
); );
const importedSecrets = await fnSecretsFromImports({ const importedSecrets = await fnSecretsFromImports({
allowedImports, allowedImports,
secretDAL, secretDAL,
@@ -739,6 +779,7 @@ export const secretServiceFactory = ({
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
return { return {
...importedSecrets[i].secrets[j], ...importedSecrets[i].secrets[j],
secretValueHidden: false,
workspace: projectId, workspace: projectId,
environment: importedSecrets[i].environment, environment: importedSecrets[i].environment,
secretPath: importedSecrets[i].secretPath secretPath: importedSecrets[i].secretPath
@@ -749,7 +790,13 @@ export const secretServiceFactory = ({
} }
if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
return { ...secret, workspace: projectId, environment, secretPath: path }; return {
...secret,
secretValueHidden: false, // Always false because we check permission at the beginning of the function
workspace: projectId,
environment,
secretPath: path
};
}; };
const createManySecret = async ({ const createManySecret = async ({
@@ -771,7 +818,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -859,7 +906,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -901,8 +948,8 @@ export const secretServiceFactory = ({
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
const references = await getSecretReference(projectId); const references = await getSecretReference(projectId);
const secrets = await secretDAL.transaction(async (tx) => const secrets = await secretDAL.transaction(async (tx) => {
fnSecretBulkUpdate({ const updatedSecrets = await fnSecretBulkUpdate({
folderId, folderId,
projectId, projectId,
tx, tx,
@@ -932,8 +979,22 @@ export const secretServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL
}) });
);
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return updatedSecrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
});
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -967,7 +1028,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -1018,8 +1079,19 @@ export const secretServiceFactory = ({
}); });
} }
} }
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return secrets; return secrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
}); });
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
@@ -1180,6 +1252,7 @@ export const secretServiceFactory = ({
secretName, secretName,
path: secretPath, path: secretPath,
environment, environment,
viewSecretValue: false,
type: "shared" type: "shared"
}); });
@@ -1194,12 +1267,25 @@ export const secretServiceFactory = ({
| (typeof groupPermissions)[number] | (typeof groupPermissions)[number]
) => { ) => {
const allowedActions = [ const allowedActions = [
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Delete,
ProjectPermissionActions.Edit ProjectPermissionSecretActions.Create,
].filter((action) => ProjectPermissionSecretActions.Edit
entityPermission.permission.can( ].filter((action) => {
if (
action === ProjectPermissionSecretActions.DescribeSecret ||
action === ProjectPermissionSecretActions.ReadValue
) {
return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, {
environment,
secretPath,
secretName,
secretTags: secret?.tags?.map((el) => el.slug)
});
}
return entityPermission.permission.can(
action, action,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
@@ -1207,8 +1293,8 @@ export const secretServiceFactory = ({
secretName, secretName,
secretTags: secret?.tags?.map((el) => el.slug) secretTags: secret?.tags?.map((el) => el.slug)
}) })
) );
); });
return { return {
...entityPermission, ...entityPermission,
@@ -1227,6 +1313,39 @@ export const secretServiceFactory = ({
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
}; };
const getAccessibleSecrets = async ({
projectId,
secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge) {
throw new BadRequestError({
message: "Project version does not support this endpoint.",
name: "ProjectVersionNotSupported"
});
}
const secrets = await secretV2BridgeService.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction,
actor,
actorId,
actorOrgId,
actorAuthMethod
});
return secrets;
};
const getSecretsRaw = async ({ const getSecretsRaw = async ({
projectId, projectId,
path, path,
@@ -1234,11 +1353,13 @@ export const secretServiceFactory = ({
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
viewSecretValue,
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive, recursive,
tagSlugs = [], tagSlugs = [],
throwOnMissingReadValuePermission = true,
...paramsV2 ...paramsV2
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@@ -1249,6 +1370,8 @@ export const secretServiceFactory = ({
actorId, actorId,
actor, actor,
actorOrgId, actorOrgId,
viewSecretValue,
throwOnMissingReadValuePermission,
environment, environment,
path, path,
recursive, recursive,
@@ -1257,6 +1380,7 @@ export const secretServiceFactory = ({
tagSlugs, tagSlugs,
...paramsV2 ...paramsV2
}); });
return { secrets, imports }; return { secrets, imports };
} }
@@ -1285,14 +1409,20 @@ export const secretServiceFactory = ({
recursive recursive
}); });
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey));
const filteredSecrets = tagSlugs.length const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets; : decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) => const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw( decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, {
...sec,
environment: el.environment,
workspace: projectId,
secretPath: el.secretPath,
secretValueHidden: false
},
botKey botKey
) )
); );
@@ -1303,6 +1433,7 @@ export const secretServiceFactory = ({
const importedEntries = decryptedImportSecrets.reduce( const importedEntries = decryptedImportSecrets.reduce(
( (
accum: { accum: {
secretValueHidden: boolean;
secretKey: string; secretKey: string;
secretPath: string; secretPath: string;
workspace: string; workspace: string;
@@ -1346,6 +1477,7 @@ export const secretServiceFactory = ({
Object.keys(secretsGroupByPath).map((groupedPath) => Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled( Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({ const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: groupedPath, secretPath: groupedPath,
@@ -1362,6 +1494,7 @@ export const secretServiceFactory = ({
processedImports.map((processedImport) => processedImports.map((processedImport) =>
Promise.allSettled( Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => { processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({ const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: path, secretPath: path,
@@ -1382,11 +1515,24 @@ export const secretServiceFactory = ({
}; };
}; };
const getSecretByIdRaw = async ({ secretId, actorId, actor, actorOrgId, actorAuthMethod }: TGetASecretByIdRawDTO) => {
const secret = await secretV2BridgeService.getSecretById({
secretId,
actorId,
actor,
actorOrgId,
actorAuthMethod
});
return secret;
};
const getSecretByNameRaw = async ({ const getSecretByNameRaw = async ({
type, type,
path, path,
actor, actor,
environment, environment,
viewSecretValue,
projectId: workspaceId, projectId: workspaceId,
expandSecretReferences, expandSecretReferences,
projectSlug, projectSlug,
@@ -1406,6 +1552,7 @@ export const secretServiceFactory = ({
includeImports, includeImports,
actorAuthMethod, actorAuthMethod,
path, path,
viewSecretValue,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
@@ -1436,6 +1583,7 @@ export const secretServiceFactory = ({
message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`,
name: "bot_not_found_error" name: "bot_not_found_error"
}); });
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) { if (expandSecretReferences) {
@@ -1454,7 +1602,10 @@ export const secretServiceFactory = ({
decryptedSecret.secretValue = expandedSecretValue || ""; decryptedSecret.secretValue = expandedSecretValue || "";
} }
return { secretMetadata: undefined, ...decryptedSecret }; return {
secretMetadata: undefined,
...decryptedSecret
};
}; };
const createSecretRaw = async ({ const createSecretRaw = async ({
@@ -1605,7 +1756,16 @@ export const secretServiceFactory = ({
tags: tagIds tags: tagIds
}); });
return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; return {
type: SecretProtectionType.Direct as const,
secret: decryptSecretRaw(
{
...secret,
secretValueHidden: false
},
botKey
)
};
}; };
const updateSecretRaw = async ({ const updateSecretRaw = async ({
@@ -2001,7 +2161,7 @@ export const secretServiceFactory = ({
return { return {
type: SecretProtectionType.Direct as const, type: SecretProtectionType.Direct as const,
secrets: secrets.map((secret) => secrets: secrets.map((secret) =>
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey)
) )
}; };
}; };
@@ -2290,6 +2450,12 @@ export const secretServiceFactory = ({
const folder = await folderDAL.findById(secret.folderId); const folder = await folderDAL.findById(secret.folderId);
if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` });
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
if (!folderWithPath) {
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
}
const { botKey } = await projectBotService.getBotKey(folder.projectId); const { botKey } = await projectBotService.getBotKey(folder.projectId);
if (!botKey) if (!botKey)
throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` });
@@ -2303,18 +2469,43 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); const secretVersions = await secretVersionDAL.findBySecretId(secretId, {
return secretVersions.map((el) => offset,
decryptSecretRaw( limit,
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
});
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{ {
environment: folder.environment.envSlug,
secretPath: folderWithPath.path,
secretName: secretKey,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
}
);
return decryptSecretRaw(
{
secretValueHidden,
...el, ...el,
workspace: folder.projectId, workspace: folder.projectId,
environment: folder.environment.envSlug, environment: folder.environment.envSlug,
secretPath: "/" secretPath: folderWithPath.path
}, },
botKey botKey
) );
); });
}; };
const attachTags = async ({ const attachTags = async ({
@@ -2340,7 +2531,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2446,7 +2637,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2612,7 +2803,7 @@ export const secretServiceFactory = ({
message: `Project with slug '${projectSlug}' not found` message: `Project with slug '${projectSlug}' not found`
}); });
} }
if (project.version === 3) { if (project.version === ProjectVersion.V3) {
return secretV2BridgeService.moveSecrets({ return secretV2BridgeService.moveSecrets({
sourceEnvironment, sourceEnvironment,
sourceSecretPath, sourceSecretPath,
@@ -2637,30 +2828,6 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
const { botKey } = await projectBotService.getBotKey(project.id); const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) { if (!botKey) {
throw new NotFoundError({ throw new NotFoundError({
@@ -2688,11 +2855,9 @@ export const secretServiceFactory = ({
}); });
} }
const sourceSecrets = await secretDAL.find({ const sourceSecrets = await secretDAL.findManySecretsWithTags({
type: SecretType.Shared, type: SecretType.Shared,
$in: { secretIds
id: secretIds
}
}); });
if (sourceSecrets.length !== secretIds.length) { if (sourceSecrets.length !== secretIds.length) {
@@ -2701,21 +2866,62 @@ export const secretServiceFactory = ({
}); });
} }
const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ const sourceActions = [
...secret, ProjectPermissionSecretActions.Delete,
secretKey: decryptSymmetric128BitHexKeyUTF8({ ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue
] as const;
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
const decryptedSourceSecrets = sourceSecrets.map((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key: botKey key: botKey
}), });
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext, for (const destinationAction of destinationActions) {
iv: secret.secretValueIV, ForbiddenError.from(permission).throwUnlessCan(
tag: secret.secretValueTag, destinationAction,
key: botKey subject(ProjectPermissionSub.Secrets, {
}) environment: destinationEnvironment,
})); secretPath: destinationSecretPath
})
);
}
for (const sourceAction of sourceActions) {
if (
sourceAction === ProjectPermissionSecretActions.ReadValue ||
sourceAction === ProjectPermissionSecretActions.DescribeSecret
) {
throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
});
} else {
ForbiddenError.from(permission).throwUnlessCan(
sourceAction,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
}
}
return {
...secret,
secretKey,
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
};
});
let isSourceUpdated = false; let isSourceUpdated = false;
let isDestinationUpdated = false; let isDestinationUpdated = false;
@@ -3088,6 +3294,8 @@ export const secretServiceFactory = ({
getSecretsRawMultiEnv, getSecretsRawMultiEnv,
getSecretReferenceTree, getSecretReferenceTree,
getSecretsRawByFolderMappings, getSecretsRawByFolderMappings,
getSecretAccessList getSecretAccessList,
getSecretByIdRaw,
getAccessibleSecrets
}; };
}; };
+20 -2
View File
@@ -2,6 +2,7 @@ import { Knex } from "knex";
import { z } from "zod"; import { z } from "zod";
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -121,6 +122,10 @@ export type TGetASecretDTO = {
version?: number; version?: number;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretByIdDTO = {
secretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateBulkSecretDTO = { export type TCreateBulkSecretDTO = {
path: string; path: string;
environment: string; environment: string;
@@ -176,10 +181,18 @@ export enum SecretsOrderBy {
Name = "name" // "key" for secrets but using name for use across resources Name = "name" // "key" for secrets but using name for use across resources
} }
export type TGetAccessibleSecretsDTO = {
secretPath: string;
environment: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
export type TGetSecretsRawDTO = { export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;
environment: string; environment: string;
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
@@ -205,6 +218,7 @@ export type TGetASecretRawDTO = {
secretName: string; secretName: string;
path: string; path: string;
environment: string; environment: string;
viewSecretValue: boolean;
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
type: "shared" | "personal"; type: "shared" | "personal";
includeImports?: boolean; includeImports?: boolean;
@@ -213,6 +227,10 @@ export type TGetASecretRawDTO = {
projectId?: string; projectId?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetASecretByIdRawDTO = {
secretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateSecretRawDTO = TProjectPermission & { export type TCreateSecretRawDTO = TProjectPermission & {
secretName: string; secretName: string;
secretPath: string; secretPath: string;
@@ -409,7 +427,7 @@ export type TCreateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -446,7 +464,7 @@ export type TUpdateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -1,9 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory
export const secretVersionDALFactory = (db: TDbClient) => { export const secretVersionDALFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion); const secretVersionOrm = ormify(db, TableName.SecretVersion);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersions> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersion)
.where(`${TableName.SecretVersion}.secretId`, secretId)
.leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`)
.leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`)
.leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.select(selectAllTableCols(TableName.SecretVersion))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder // This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try { try {
@@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId, findLatestVersionByFolderId,
findBySecretId,
bulkUpdateNoVersionIncrement bulkUpdateNoVersionIncrement
}; };
}; };
@@ -5,7 +5,11 @@ import bcrypt from "bcrypt";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
@@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({
scopes.forEach(({ environment, secretPath }) => { scopes.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
}); });
+1
View File
@@ -50,6 +50,7 @@ const buildSlackPayload = (notification: TSlackNotification) => {
const messageBody = `A secret approval request has been opened by ${payload.userEmail}. const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment} *Environment*: ${payload.environment}
*Secret path*: ${payload.secretPath || "/"} *Secret path*: ${payload.secretPath || "/"}
*Secret Key${payload.secretKeys.length > 1 ? "s" : ""}*: ${payload.secretKeys.join(", ")}
View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId}/approval?requestId=${
payload.requestId payload.requestId
@@ -62,6 +62,7 @@ export type TSlackNotification =
secretPath: string; secretPath: string;
requestId: string; requestId: string;
projectId: string; projectId: string;
secretKeys: string[];
}; };
} }
| { | {
@@ -19,9 +19,11 @@ import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types"; import { UserAliasType } from "../user-alias/user-alias-types";
import { TSuperAdminDALFactory } from "./super-admin-dal"; import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types"; import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
type TSuperAdminServiceFactoryDep = { type TSuperAdminServiceFactoryDep = {
identityDAL: Pick<TIdentityDALFactory, "getIdentitiesByFilter">;
serverCfgDAL: TSuperAdminDALFactory; serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne">; userAliasDAL: Pick<TUserAliasDALFactory, "findOne">;
@@ -51,6 +53,7 @@ const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const superAdminServiceFactory = ({ export const superAdminServiceFactory = ({
serverCfgDAL, serverCfgDAL,
userDAL, userDAL,
identityDAL,
userAliasDAL, userAliasDAL,
authService, authService,
orgService, orgService,
@@ -271,26 +274,30 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization }; return { token, user: userInfo, organization };
}; };
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => { const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({ return userDAL.getUsersByFilter({
limit, limit,
offset, offset,
searchTerm, searchTerm,
sortBy: "username" sortBy: "username",
adminsOnly
}); });
}; };
const deleteUser = async (userId: string) => { const deleteUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({
message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan."
});
}
const user = await userDAL.deleteById(userId); const user = await userDAL.deleteById(userId);
return user; return user;
}; };
const getIdentities = ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
return identityDAL.getIdentitiesByFilter({
limit,
offset,
searchTerm,
sortBy: "name"
});
};
const grantServerAdminAccessToUser = async (userId: string) => { const grantServerAdminAccessToUser = async (userId: string) => {
if (!licenseService.onPremFeatures?.instanceUserManagement) { if (!licenseService.onPremFeatures?.instanceUserManagement) {
throw new BadRequestError({ throw new BadRequestError({
@@ -388,6 +395,7 @@ export const superAdminServiceFactory = ({
adminSignUp, adminSignUp,
getUsers, getUsers,
deleteUser, deleteUser,
getIdentities,
getAdminSlackConfig, getAdminSlackConfig,
updateRootEncryptionStrategy, updateRootEncryptionStrategy,
getConfiguredEncryptionStrategies, getConfiguredEncryptionStrategies,
@@ -20,6 +20,13 @@ export type TAdminGetUsersDTO = {
offset: number; offset: number;
limit: number; limit: number;
searchTerm: string; searchTerm: string;
adminsOnly: boolean;
};
export type TAdminGetIdentitiesDTO = {
offset: number;
limit: number;
searchTerm: string;
}; };
export enum LoginMethod { export enum LoginMethod {
+8 -1
View File
@@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => {
limit, limit,
offset, offset,
searchTerm, searchTerm,
sortBy sortBy,
adminsOnly
}: { }: {
limit: number; limit: number;
offset: number; offset: number;
searchTerm: string; searchTerm: string;
sortBy?: keyof TUsers; sortBy?: keyof TUsers;
adminsOnly: boolean;
}) => { }) => {
try { try {
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false); let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
if (searchTerm) { if (searchTerm) {
query = query.where((qb) => { query = query.where((qb) => {
void qb void qb
@@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => {
}); });
} }
if (adminsOnly) {
query = query.where("superAdmin", true);
}
if (sortBy) { if (sortBy) {
query = query.orderBy(sortBy); query = query.orderBy(sortBy);
} }
+81 -32
View File
@@ -11,7 +11,7 @@ import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal"; import { TWebhookDALFactory } from "./webhook-dal";
import { WebhookType } from "./webhook-types"; import { TWebhookPayloads, WebhookEvents, WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
@@ -54,29 +54,64 @@ export const triggerWebhookRequest = async (
return req; return req;
}; };
export const getWebhookPayload = ( export const getWebhookPayload = (event: TWebhookPayloads) => {
eventName: string, if (event.type === WebhookEvents.SecretModified) {
details: { const { projectName, projectId, environment, secretPath, type } = event.payload;
workspaceName: string;
workspaceId: string; switch (type) {
environment: string; case WebhookType.SLACK:
secretPath?: string; return {
type?: string | null; text: "A secret value has been added or modified.",
attachments: [
{
color: "#E7F256",
fields: [
{
title: "Project",
value: projectName,
short: false
},
{
title: "Environment",
value: environment,
short: false
},
{
title: "Secret Path",
value: secretPath,
short: false
}
]
}
]
};
case WebhookType.GENERAL:
default:
return {
event: event.type,
project: {
workspaceId: projectId,
projectName,
environment,
secretPath
}
};
}
} }
) => {
const { workspaceName, workspaceId, environment, secretPath, type } = details; const { projectName, projectId, environment, secretPath, type, reminderNote, secretName } = event.payload;
switch (type) { switch (type) {
case WebhookType.SLACK: case WebhookType.SLACK:
return { return {
text: "A secret value has been added or modified.", text: "You have a secret reminder",
attachments: [ attachments: [
{ {
color: "#E7F256", color: "#E7F256",
fields: [ fields: [
{ {
title: "Project", title: "Project",
value: workspaceName, value: projectName,
short: false short: false
}, },
{ {
@@ -88,6 +123,16 @@ export const getWebhookPayload = (
title: "Secret Path", title: "Secret Path",
value: secretPath, value: secretPath,
short: false short: false
},
{
title: "Secret Name",
value: secretName,
short: false
},
{
title: "Reminder Note",
value: reminderNote,
short: false
} }
] ]
} }
@@ -96,11 +141,14 @@ export const getWebhookPayload = (
case WebhookType.GENERAL: case WebhookType.GENERAL:
default: default:
return { return {
event: eventName, event: event.type,
project: { project: {
workspaceId, workspaceId: projectId,
projectName,
environment, environment,
secretPath secretPath,
secretName,
reminderNote
} }
}; };
} }
@@ -110,6 +158,7 @@ export type TFnTriggerWebhookDTO = {
projectId: string; projectId: string;
secretPath: string; secretPath: string;
environment: string; environment: string;
event: TWebhookPayloads;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">; webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
@@ -124,8 +173,9 @@ export const fnTriggerWebhook = async ({
projectId, projectId,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL, event,
secretManagerDecryptor secretManagerDecryptor,
projectDAL
}: TFnTriggerWebhookDTO) => { }: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter( const toBeTriggeredHooks = webhooks.filter(
@@ -134,21 +184,20 @@ export const fnTriggerWebhook = async ({
); );
if (!toBeTriggeredHooks.length) return; if (!toBeTriggeredHooks.length) return;
logger.info({ environment, secretPath, projectId }, "Secret webhook job started"); logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
const project = await projectDAL.findById(projectId); let { projectName } = event.payload;
if (!projectName) {
const project = await projectDAL.findById(event.payload.projectId);
projectName = project.name;
}
const webhooksTriggered = await Promise.allSettled( const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) => toBeTriggeredHooks.map((hook) => {
triggerWebhookRequest( const formattedEvent = {
hook, type: event.type,
secretManagerDecryptor, payload: { ...event.payload, type: hook.type, projectName }
getWebhookPayload("secrets.modified", { } as TWebhookPayloads;
workspaceName: project.name, return triggerWebhookRequest(hook, secretManagerDecryptor, getWebhookPayload(formattedEvent));
workspaceId: projectId, })
environment,
secretPath,
type: hook.type
})
)
)
); );
// filter hooks by status // filter hooks by status
@@ -16,7 +16,8 @@ import {
TDeleteWebhookDTO, TDeleteWebhookDTO,
TListWebhookDTO, TListWebhookDTO,
TTestWebhookDTO, TTestWebhookDTO,
TUpdateWebhookDTO TUpdateWebhookDTO,
WebhookEvents
} from "./webhook-types"; } from "./webhook-types";
type TWebhookServiceFactoryDep = { type TWebhookServiceFactoryDep = {
@@ -144,12 +145,15 @@ export const webhookServiceFactory = ({
await triggerWebhookRequest( await triggerWebhookRequest(
webhook, webhook,
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(), (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
getWebhookPayload("test", { getWebhookPayload({
workspaceName: project.name, type: "test" as WebhookEvents.SecretModified,
workspaceId: webhook.projectId, payload: {
environment: webhook.environment.slug, projectName: project.name,
secretPath: webhook.secretPath, projectId: webhook.projectId,
type: webhook.type environment: webhook.environment.slug,
secretPath: webhook.secretPath,
type: webhook.type
}
}) })
); );
} catch (err) { } catch (err) {
@@ -30,3 +30,36 @@ export enum WebhookType {
GENERAL = "general", GENERAL = "general",
SLACK = "slack" SLACK = "slack"
} }
export enum WebhookEvents {
SecretModified = "secrets.modified",
SecretReminderExpired = "secrets.reminder-expired",
TestEvent = "test"
}
type TWebhookSecretModifiedEventPayload = {
type: WebhookEvents.SecretModified;
payload: {
projectName?: string;
projectId: string;
environment: string;
secretPath?: string;
type?: string | null;
};
};
type TWebhookSecretReminderEventPayload = {
type: WebhookEvents.SecretReminderExpired;
payload: {
projectName?: string;
projectId: string;
environment: string;
secretPath?: string;
type?: string | null;
secretName: string;
secretId: string;
reminderNote?: string | null;
};
};
export type TWebhookPayloads = TWebhookSecretModifiedEventPayload | TWebhookSecretReminderEventPayload;
+17
View File
@@ -0,0 +1,17 @@
import path from "path";
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
env: {
NODE_ENV: "test"
},
include: ["./src/**/*.test.ts"]
},
resolve: {
alias: {
"@app": path.resolve(__dirname, "./src")
}
}
});
+6 -6
View File
@@ -20,6 +20,7 @@ require (
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0 github.com/muesli/roff v0.1.0
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
github.com/pion/dtls/v3 v3.0.4
github.com/pion/logging v0.2.3 github.com/pion/logging v0.2.3
github.com/pion/turn/v4 v4.0.0 github.com/pion/turn/v4 v4.0.0
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
@@ -29,9 +30,9 @@ require (
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1 github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.35.0 golang.org/x/crypto v0.36.0
golang.org/x/sys v0.30.0 golang.org/x/sys v0.31.0
golang.org/x/term v0.29.0 golang.org/x/term v0.30.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@@ -90,7 +91,6 @@ require (
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/transport/v3 v3.0.7 // indirect
@@ -115,8 +115,8 @@ require (
golang.org/x/mod v0.23.0 // indirect golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.30.0 // indirect golang.org/x/tools v0.30.0 // indirect
google.golang.org/api v0.188.0 // indirect google.golang.org/api v0.188.0 // indirect

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