mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 13:27:46 +00:00
Merge branch 'main' into feat/allowShareToAnyoneEdition
This commit is contained in:
@@ -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
|
||||||
@@ -120,4 +120,3 @@ export default {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,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": [],
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
+56
-16
@@ -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">;
|
||||||
@@ -912,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);
|
||||||
|
|
||||||
@@ -1000,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,
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
/* 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";
|
||||||
@@ -12,6 +12,7 @@ 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";
|
||||||
@@ -22,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,
|
||||||
@@ -97,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) {
|
||||||
@@ -134,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)
|
||||||
@@ -162,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) {
|
||||||
@@ -170,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -667,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."
|
||||||
},
|
},
|
||||||
@@ -690,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:
|
||||||
@@ -718,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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu
|
|||||||
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GenericResourceNameSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Name must be at least 1 character" })
|
||||||
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
|
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -241,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()
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
|
|||||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
@@ -251,7 +251,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
params: z.object({ organizationId: z.string().trim() }),
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
|
name: GenericResourceNameSchema.optional(),
|
||||||
slug: slugSchema({ max: 64 }).optional(),
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
authEnforced: z.boolean().optional(),
|
authEnforced: z.boolean().optional(),
|
||||||
scimEnabled: z.boolean().optional(),
|
scimEnabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -675,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;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim()
|
name: GenericResourceNameSchema
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -394,14 +368,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
secret: secretRawSchema.extend({
|
secret: secretRawSchema.extend({
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
tags: SecretTagsSchema.pick({
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
color: true
|
|
||||||
})
|
|
||||||
.extend({ name: z.string() })
|
|
||||||
.array()
|
|
||||||
.optional(),
|
|
||||||
secretMetadata: ResourceMetadataSchema.optional()
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -445,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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -498,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,
|
||||||
@@ -704,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")
|
||||||
])
|
])
|
||||||
@@ -800,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")
|
||||||
])
|
])
|
||||||
@@ -822,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({
|
||||||
@@ -884,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
|
||||||
@@ -986,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({
|
||||||
@@ -1260,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()
|
||||||
@@ -1429,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")
|
||||||
])
|
])
|
||||||
@@ -1747,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")
|
||||||
])
|
])
|
||||||
@@ -1862,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")
|
||||||
])
|
])
|
||||||
@@ -2124,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")
|
||||||
])
|
])
|
||||||
@@ -2246,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")
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim(),
|
verifier: z.string().trim(),
|
||||||
organizationName: z.string().trim().min(1),
|
organizationName: GenericResourceNameSchema,
|
||||||
providerAuthToken: z.string().trim().optional().nullish(),
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
attributionSource: z.string().trim().optional(),
|
attributionSource: z.string().trim().optional(),
|
||||||
password: z.string()
|
password: z.string()
|
||||||
|
|||||||
@@ -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">;
|
||||||
|
|||||||
@@ -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">;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -760,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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { ActorType } from "../auth/auth-type";
|
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";
|
||||||
@@ -108,6 +109,7 @@ 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,
|
||||||
@@ -146,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
|
||||||
@@ -154,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 ({
|
||||||
@@ -300,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 ({
|
||||||
@@ -533,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);
|
||||||
@@ -552,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);
|
||||||
@@ -646,13 +666,13 @@ 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,
|
||||||
@@ -674,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,9 +173,9 @@ 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?: {
|
actor?: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -192,9 +201,9 @@ 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?: {
|
actor?: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -340,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;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
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, TFindOpt } 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";
|
||||||
|
|
||||||
@@ -13,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 {
|
||||||
@@ -135,6 +187,17 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretVersionV2}.userActorId`
|
`${TableName.SecretVersionV2}.userActorId`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
.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) => {
|
.where((qb) => {
|
||||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||||
@@ -145,9 +208,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.SecretVersionV2),
|
selectAllTableCols(TableName.SecretVersionV2),
|
||||||
`${TableName.Users}.username as userActorName`,
|
db.ref("username").withSchema(TableName.Users).as("userActorName"),
|
||||||
`${TableName.Identity}.name as identityActorName`,
|
db.ref("name").withSchema(TableName.Identity).as("identityActorName"),
|
||||||
`${TableName.ProjectMembership}.id as membershipId`
|
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 (limit) void query.limit(limit);
|
||||||
@@ -162,14 +228,33 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const docs: Array<
|
const docs = await query;
|
||||||
TSecretVersionsV2 & {
|
|
||||||
userActorName: string | undefined | null;
|
const data = sqlNestRelationships({
|
||||||
identityActorName: string | undefined | null;
|
data: docs,
|
||||||
membershipId: string | undefined | null;
|
key: "id",
|
||||||
}
|
parentMapper: (el) => ({
|
||||||
> = await query;
|
_id: el.id,
|
||||||
return docs;
|
...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) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
|
||||||
}
|
}
|
||||||
@@ -181,6 +266,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
findLatestVersionMany,
|
findLatestVersionMany,
|
||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
findLatestVersionByFolderId,
|
findLatestVersionByFolderId,
|
||||||
findVersionsBySecretIdWithActors
|
findVersionsBySecretIdWithActors,
|
||||||
|
findBySecretId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -1198,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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -403,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) {
|
||||||
|
|||||||
@@ -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,7 @@ import {
|
|||||||
TDeleteManySecretRawDTO,
|
TDeleteManySecretRawDTO,
|
||||||
TDeleteSecretDTO,
|
TDeleteSecretDTO,
|
||||||
TDeleteSecretRawDTO,
|
TDeleteSecretRawDTO,
|
||||||
|
TGetAccessibleSecretsDTO,
|
||||||
TGetASecretByIdRawDTO,
|
TGetASecretByIdRawDTO,
|
||||||
TGetASecretDTO,
|
TGetASecretDTO,
|
||||||
TGetASecretRawDTO,
|
TGetASecretRawDTO,
|
||||||
@@ -205,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -323,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -445,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 ({
|
||||||
@@ -468,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -541,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 ({
|
||||||
@@ -589,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: [] };
|
||||||
@@ -614,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,
|
||||||
@@ -671,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({
|
||||||
@@ -721,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,
|
||||||
@@ -740,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
|
||||||
@@ -750,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 ({
|
||||||
@@ -772,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -860,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -902,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,
|
||||||
@@ -933,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({
|
||||||
@@ -968,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1019,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);
|
||||||
@@ -1181,6 +1252,7 @@ export const secretServiceFactory = ({
|
|||||||
secretName,
|
secretName,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
|
viewSecretValue: false,
|
||||||
type: "shared"
|
type: "shared"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1195,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,
|
||||||
@@ -1208,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,
|
||||||
@@ -1228,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,
|
||||||
@@ -1235,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);
|
||||||
@@ -1250,6 +1370,8 @@ export const secretServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
|
viewSecretValue,
|
||||||
|
throwOnMissingReadValuePermission,
|
||||||
environment,
|
environment,
|
||||||
path,
|
path,
|
||||||
recursive,
|
recursive,
|
||||||
@@ -1258,6 +1380,7 @@ export const secretServiceFactory = ({
|
|||||||
tagSlugs,
|
tagSlugs,
|
||||||
...paramsV2
|
...paramsV2
|
||||||
});
|
});
|
||||||
|
|
||||||
return { secrets, imports };
|
return { secrets, imports };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1286,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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -1304,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;
|
||||||
@@ -1347,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,
|
||||||
@@ -1363,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,
|
||||||
@@ -1400,6 +1532,7 @@ export const secretServiceFactory = ({
|
|||||||
path,
|
path,
|
||||||
actor,
|
actor,
|
||||||
environment,
|
environment,
|
||||||
|
viewSecretValue,
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
@@ -1419,6 +1552,7 @@ export const secretServiceFactory = ({
|
|||||||
includeImports,
|
includeImports,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
path,
|
path,
|
||||||
|
viewSecretValue,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -1449,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) {
|
||||||
@@ -1467,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 ({
|
||||||
@@ -1618,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 ({
|
||||||
@@ -2014,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)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -2303,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` });
|
||||||
@@ -2316,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 ({
|
||||||
@@ -2353,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2459,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 })
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2625,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,
|
||||||
@@ -2650,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({
|
||||||
@@ -2701,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) {
|
||||||
@@ -2714,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;
|
||||||
@@ -3102,6 +3295,7 @@ export const secretServiceFactory = ({
|
|||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsRawByFolderMappings,
|
getSecretsRawByFolderMappings,
|
||||||
getSecretAccessList,
|
getSecretAccessList,
|
||||||
getSecretByIdRaw
|
getSecretByIdRaw,
|
||||||
|
getAccessibleSecrets
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -180,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[];
|
||||||
@@ -209,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;
|
||||||
@@ -417,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">;
|
||||||
@@ -454,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 })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
+1
-11
@@ -484,8 +484,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
|
||||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -592,8 +590,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -644,13 +640,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
|
||||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -662,8 +654,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -868,4 +858,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
|||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
+55
-13
@@ -1,39 +1,34 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "fmt"
|
|
||||||
|
|
||||||
// "github.com/Infisical/infisical-merge/packages/api"
|
|
||||||
// "github.com/Infisical/infisical-merge/packages/models"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Infisical/infisical-merge/packages/gateway"
|
"github.com/Infisical/infisical-merge/packages/gateway"
|
||||||
"github.com/Infisical/infisical-merge/packages/util"
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
|
||||||
// "github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
// "github.com/go-resty/resty/v2"
|
|
||||||
"github.com/posthog/posthog-go"
|
"github.com/posthog/posthog-go"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var gatewayCmd = &cobra.Command{
|
var gatewayCmd = &cobra.Command{
|
||||||
Example: `infisical gateway`,
|
|
||||||
Short: "Used to infisical gateway",
|
|
||||||
Use: "gateway",
|
Use: "gateway",
|
||||||
|
Short: "Run the Infisical gateway or manage its systemd service",
|
||||||
|
Long: "Run the Infisical gateway in the foreground or manage its systemd service installation. Use 'gateway install' to set up the systemd service.",
|
||||||
|
Example: `infisical gateway --token=<token>
|
||||||
|
sudo infisical gateway install --token=<token> --domain=<domain>`,
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
token, err := util.GetInfisicalToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse token flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == nil {
|
if token == nil {
|
||||||
@@ -109,6 +104,50 @@ var gatewayCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gatewayInstallCmd = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "Install and enable systemd service for the gateway (requires sudo)",
|
||||||
|
Long: "Install and enable systemd service for the gateway. Must be run with sudo on Linux.",
|
||||||
|
Example: "sudo infisical gateway install --token=<token> --domain=<domain>",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
util.HandleError(fmt.Errorf("systemd service installation is only supported on Linux"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
util.HandleError(fmt.Errorf("systemd service installation requires root/sudo privileges"))
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == nil {
|
||||||
|
util.HandleError(fmt.Errorf("Token not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := cmd.Flags().GetString("domain")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse domain flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
|
||||||
|
util.HandleError(err, "Failed to install systemd service")
|
||||||
|
}
|
||||||
|
|
||||||
|
enableCmd := exec.Command("systemctl", "enable", "infisical-gateway")
|
||||||
|
if err := enableCmd.Run(); err != nil {
|
||||||
|
util.HandleError(err, "Failed to enable systemd service")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("Successfully installed and enabled infisical-gateway service")
|
||||||
|
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var gatewayRelayCmd = &cobra.Command{
|
var gatewayRelayCmd = &cobra.Command{
|
||||||
Example: `infisical gateway relay`,
|
Example: `infisical gateway relay`,
|
||||||
Short: "Used to run infisical gateway relay",
|
Short: "Used to run infisical gateway relay",
|
||||||
@@ -138,9 +177,12 @@ var gatewayRelayCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||||
|
gatewayInstallCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||||
|
gatewayInstallCmd.Flags().String("domain", "", "Domain of your self-hosted Infisical instance")
|
||||||
|
|
||||||
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
||||||
|
|
||||||
|
gatewayCmd.AddCommand(gatewayInstallCmd)
|
||||||
gatewayCmd.AddCommand(gatewayRelayCmd)
|
gatewayCmd.AddCommand(gatewayRelayCmd)
|
||||||
rootCmd.AddCommand(gatewayCmd)
|
rootCmd.AddCommand(gatewayCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,9 @@ func (g *Gateway) ConnectWithRelay() error {
|
|||||||
|
|
||||||
// Dial TURN Server
|
// Dial TURN Server
|
||||||
if relayPort == "5349" {
|
if relayPort == "5349" {
|
||||||
caCertPool := x509.NewCertPool()
|
|
||||||
caCertPool.AppendCertsFromPEM([]byte(g.config.CertificateChain))
|
|
||||||
|
|
||||||
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
||||||
conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{
|
conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{
|
||||||
ServerName: relayAddress,
|
ServerName: relayAddress,
|
||||||
RootCAs: caCertPool,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
||||||
@@ -193,7 +189,7 @@ func (g *Gateway) Listen(ctx context.Context) error {
|
|||||||
|
|
||||||
log.Printf("Listener started on %s", quicListener.Addr())
|
log.Printf("Listener started on %s", quicListener.Addr())
|
||||||
|
|
||||||
g.registerRelayIsActive(ctx, quicListener.Addr().String(), errCh)
|
g.registerRelayIsActive(ctx, errCh)
|
||||||
|
|
||||||
log.Info().Msg("Gateway started successfully")
|
log.Info().Msg("Gateway started successfully")
|
||||||
|
|
||||||
@@ -330,7 +326,7 @@ func (g *Gateway) createPermissionForStaticIps(staticIps string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh chan error) error {
|
func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error {
|
||||||
ticker := time.NewTicker(15 * time.Second)
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
maxFailures := 3
|
maxFailures := 3
|
||||||
failures := 0
|
failures := 0
|
||||||
@@ -345,13 +341,7 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
|
|||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
log.Debug().Msg("Performing relay connection health check")
|
log.Debug().Msg("Performing relay connection health check")
|
||||||
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
|
err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp)
|
||||||
defer cancel()
|
|
||||||
// Try to establish a QUIC connection
|
|
||||||
conn, err := quic.DialAddr(ctxTimeout, addr, &tls.Config{
|
|
||||||
InsecureSkipVerify: false, // Skip certificate verification
|
|
||||||
NextProtos: []string{"infisical-gateway"},
|
|
||||||
}, nil)
|
|
||||||
if err != nil && !strings.Contains(err.Error(), "tls:") {
|
if err != nil && !strings.Contains(err.Error(), "tls:") {
|
||||||
failures++
|
failures++
|
||||||
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
|
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
|
||||||
@@ -361,9 +351,6 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if conn != nil {
|
|
||||||
defer conn.CloseWithError(0, "All good")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const systemdServiceTemplate = `[Unit]
|
||||||
|
Description=Infisical Gateway Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=/etc/infisical/gateway.conf
|
||||||
|
ExecStart=infisical gateway
|
||||||
|
Restart=on-failure
|
||||||
|
InaccessibleDirectories=/home
|
||||||
|
PrivateTmp=yes
|
||||||
|
LimitCORE=infinity
|
||||||
|
LimitNOFILE=1000000
|
||||||
|
LimitNPROC=60000
|
||||||
|
LimitRTPRIO=infinity
|
||||||
|
LimitRTTIME=7000000
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
`
|
||||||
|
|
||||||
|
func InstallGatewaySystemdService(token string, domain string) error {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
log.Info().Msg("Skipping systemd service installation - not on Linux")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
log.Info().Msg("Skipping systemd service installation - not running as root/sudo")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := "/etc/infisical"
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token)
|
||||||
|
if domain != "" {
|
||||||
|
configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain)
|
||||||
|
} else {
|
||||||
|
configContent += "INFISICAL_API_URL=\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(configDir, "gateway.conf")
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
||||||
|
return fmt.Errorf("failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
servicePath := "/etc/systemd/system/infisical-gateway.service"
|
||||||
|
if _, err := os.Stat(servicePath); err == nil {
|
||||||
|
log.Info().Msg("Systemd service file already exists")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write systemd service file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadCmd := exec.Command("systemctl", "daemon-reload")
|
||||||
|
if err := reloadCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to reload systemd: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("Successfully installed systemd service")
|
||||||
|
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
|
||||||
|
log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: "infisical gateway"
|
||||||
|
description: "Run the Infisical gateway or manage its systemd service"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Run gateway">
|
||||||
|
```bash
|
||||||
|
infisical gateway --token=<token>
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Install service">
|
||||||
|
```bash
|
||||||
|
sudo infisical gateway install --token=<token> --domain=<domain>
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Run the Infisical gateway in the foreground or manage its systemd service installation. The gateway allows secure communication between your self-hosted Infisical instance and client applications.
|
||||||
|
|
||||||
|
## Subcommands & flags
|
||||||
|
|
||||||
|
<Accordion title="infisical gateway" defaultOpen="true">
|
||||||
|
Run the Infisical gateway in the foreground. The gateway will connect to the relay service and maintain a persistent connection.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
infisical gateway --token=<token> --domain=<domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The machine identity access token to authenticate with Infisical.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical gateway --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the gateway command.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--domain">
|
||||||
|
Domain of your self-hosted Infisical instance.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
sudo infisical gateway install --domain=https://app.your-domain.com
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical gateway install">
|
||||||
|
Install and enable the gateway as a systemd service. This command must be run with sudo on Linux.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo infisical gateway install --token=<token> --domain=<domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Must be run on Linux
|
||||||
|
- Must be run with root/sudo privileges
|
||||||
|
- Requires systemd
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
<Accordion title="--token">
|
||||||
|
The machine identity access token to authenticate with Infisical.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
sudo infisical gateway install --token=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the install command.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--domain">
|
||||||
|
Domain of your self-hosted Infisical instance.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
sudo infisical gateway install --domain=https://app.your-domain.com
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
### Service Details
|
||||||
|
The systemd service is installed with secure defaults:
|
||||||
|
- Service file: `/etc/systemd/system/infisical-gateway.service`
|
||||||
|
- Config file: `/etc/infisical/gateway.conf`
|
||||||
|
- Runs with restricted privileges:
|
||||||
|
- InaccessibleDirectories=/home
|
||||||
|
- PrivateTmp=yes
|
||||||
|
- Resource limits configured for stability
|
||||||
|
- Automatically restarts on failure
|
||||||
|
- Enabled to start on boot
|
||||||
|
|
||||||
|
After installation, manage the service with standard systemd commands:
|
||||||
|
```bash
|
||||||
|
sudo systemctl start infisical-gateway # Start the service
|
||||||
|
sudo systemctl stop infisical-gateway # Stop the service
|
||||||
|
sudo systemctl status infisical-gateway # Check service status
|
||||||
|
sudo systemctl disable infisical-gateway # Disable auto-start on boot
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
@@ -4,6 +4,8 @@ sidebarTitle: "Overview"
|
|||||||
description: "How to access private network resources from Infisical"
|
description: "How to access private network resources from Infisical"
|
||||||
---
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
|
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
|
||||||
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
|
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
|
||||||
Common use cases include generating dynamic credentials or rotating credentials for private databases.
|
Common use cases include generating dynamic credentials or rotating credentials for private databases.
|
||||||
@@ -45,19 +47,53 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
|
|||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Deploy the Gateway">
|
<Step title="Deploy the Gateway">
|
||||||
Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method:
|
Use the Infisical CLI to deploy the Gateway. You can run it directly or install it as a systemd service for production:
|
||||||
```bash
|
|
||||||
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
|
<Tabs>
|
||||||
```
|
<Tab title="Production (systemd)">
|
||||||
Alternatively, if you already have the token, use it directly with the `--token` flag:
|
For production deployments on Linux, install the Gateway as a systemd service:
|
||||||
```bash
|
```bash
|
||||||
infisical gateway --token <your-machine-identity-token>
|
sudo infisical gateway install --token <your-machine-identity-token> --domain <your-infisical-domain>
|
||||||
```
|
sudo systemctl start infisical-gateway
|
||||||
Or set it as an environment variable:
|
```
|
||||||
```bash
|
This will install and start the Gateway as a secure systemd service that:
|
||||||
export INFISICAL_TOKEN=<your-machine-identity-token>
|
- Runs with restricted privileges:
|
||||||
infisical gateway
|
- Runs as root user (required for secure token management)
|
||||||
```
|
- Restricted access to home directories
|
||||||
|
- Private temporary directory
|
||||||
|
- Automatically restarts on failure
|
||||||
|
- Starts on system boot
|
||||||
|
- Manages token and domain configuration securely in `/etc/infisical/gateway.conf`
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
The install command requires:
|
||||||
|
- Linux operating system
|
||||||
|
- Root/sudo privileges
|
||||||
|
- Systemd
|
||||||
|
</Warning>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="Development (direct)">
|
||||||
|
For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command:
|
||||||
|
```bash
|
||||||
|
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, if you already have the token, use it directly with the `--token` flag:
|
||||||
|
```bash
|
||||||
|
infisical gateway --token <your-machine-identity-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or set it as an environment variable:
|
||||||
|
```bash
|
||||||
|
export INFISICAL_TOKEN=<your-machine-identity-token>
|
||||||
|
infisical gateway
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
For detailed information about the gateway command and its options, see the [gateway command documentation](/cli/commands/gateway).
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
|
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
|
||||||
</Note>
|
</Note>
|
||||||
@@ -78,4 +114,3 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
|
|||||||
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
|
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: 'Secret Scanning'
|
||||||
|
description: "Scan and prevent secret leaks in your code repositories"
|
||||||
|
---
|
||||||
|
|
||||||
|
The Infisical Secret Scanner allows you to keep an overview and stay alert of exposed secrets across your entire GitHub organization and repositories.
|
||||||
|
|
||||||
|
To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes.
|
||||||
|
|
||||||
|
## Code Scanning
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Secret scans are built on event-driven architecture. This means that every time a push is made to one of your selected repositories, Infisical will scan the modified files for any exposed secrets.
|
||||||
|
|
||||||
|
If one or more exposed secrets are detected, it will be displayed in your Infisical dashboard. An exposed secret is known as a **"Risk"**. Each risk has the following data associated with it:
|
||||||
|
- **Date**: When the risk was first detected.
|
||||||
|
- **Secret Type**: Which type of secret was detected.
|
||||||
|
- **Info**: Information about the secret, such as the repository, file name, and the committer who made the change.
|
||||||
|
|
||||||
|
Once an exposed secret is detected, all organization admins will be sent an e-mail notification containing details about the exposed secret.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Each risk also contains a "View Exposed Secret" button, which will take you directly to the GitHub commit and to the line where the secret was exposed.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Responding to Exposed Secrets
|
||||||
|
|
||||||
|
After an exposed secret is detected, it will be marked as `Needs Attention`. When there are risks marked as needs attention, it's important to address them as soon as possible.
|
||||||
|
|
||||||
|
You can mark the risk as `Resolved` by changing the status to one of the following states:
|
||||||
|
- **This Is a False Positive**: The secret was not exposed, but was detected by the scanner.
|
||||||
|
- **I Have Rotated The Secret**: The secret was exposed, but it has now been removed.
|
||||||
|
- **No Rotation Needed**: You are choosing to ignore this risk. You may choose to do this if the risk is non-sensitive or otherwise not a security risk.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Ignoring Known Secrets
|
||||||
|
If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below.
|
||||||
|
|
||||||
|
### infisical-scan:ignore
|
||||||
|
|
||||||
|
To ignore a secret contained in line of code, simply add `infisical-scan:ignore ` at the end of the line as comment in the given programming.
|
||||||
|
|
||||||
|
```js example.js
|
||||||
|
function helloWorld() {
|
||||||
|
console.log("8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"); // infisical-scan:ignore
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### .infisicalignore
|
||||||
|
An alternative method to exclude specific findings involves creating a .infisicalignore file at your repository's root.
|
||||||
|
You can then add the fingerprints of the findings you wish to exclude. The [Infisical scan](/cli/scanning-overview) report provides a unique Fingerprint for each secret found.
|
||||||
|
By incorporating these Fingerprints into the .infisicalignore file, Infisical will skip the corresponding secret findings in subsequent scans.
|
||||||
|
|
||||||
|
```.ignore .infisicalignore
|
||||||
|
bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147
|
||||||
|
bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5
|
||||||
|
1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
@@ -7,6 +7,12 @@ Prerequisites:
|
|||||||
|
|
||||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
When integrating with Databricks, Infisical is intended to be the source of truth for the secrets in the configured Databricks scope.
|
||||||
|
|
||||||
|
Any secrets not present in Infisical will be removed from the specified scope. To prevent removal of secrets not managed by Infisical, Infisical recommends creating a designated secret scope for your integration.
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Authorize Infisical for Databricks">
|
<Step title="Authorize Infisical for Databricks">
|
||||||
Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**.
|
Obtain a Personal Access Token in **User Settings** > **Developer** > **Access Tokens**.
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ description: "Learn how to configure a Databricks Sync for Infisical."
|
|||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope.
|
You must create a secret scope in your Databricks workspace prior to configuration. Ensure your service principal has [Write permissions](https://docs.databricks.com/en/security/auth/access-control/index.html#secret-acls) for the specified secret scope.
|
||||||
|
|
||||||
|
Infisical recommends creating a designated Databricks secret scope for your sync to prevent removal of secrets not managed by Infisical.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||||
|
|||||||
+4
-3
@@ -220,7 +220,8 @@
|
|||||||
"documentation/platform/admin-panel/org-admin-console"
|
"documentation/platform/admin-panel/org-admin-console"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"documentation/platform/secret-sharing"
|
"documentation/platform/secret-sharing",
|
||||||
|
"documentation/platform/secret-scanning"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -339,6 +340,7 @@
|
|||||||
"cli/commands/secrets",
|
"cli/commands/secrets",
|
||||||
"cli/commands/dynamic-secrets",
|
"cli/commands/dynamic-secrets",
|
||||||
"cli/commands/ssh",
|
"cli/commands/ssh",
|
||||||
|
"cli/commands/gateway",
|
||||||
"cli/commands/export",
|
"cli/commands/export",
|
||||||
"cli/commands/token",
|
"cli/commands/token",
|
||||||
"cli/commands/service-token",
|
"cli/commands/service-token",
|
||||||
@@ -644,8 +646,7 @@
|
|||||||
"api-reference/endpoints/oidc-auth/attach",
|
"api-reference/endpoints/oidc-auth/attach",
|
||||||
"api-reference/endpoints/oidc-auth/retrieve",
|
"api-reference/endpoints/oidc-auth/retrieve",
|
||||||
"api-reference/endpoints/oidc-auth/update",
|
"api-reference/endpoints/oidc-auth/update",
|
||||||
"api-reference/endpoints/oidc-auth/revoke",
|
"api-reference/endpoints/oidc-auth/revoke"
|
||||||
"integrations/frameworks/terraform-cloud"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+27
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1741445498,
|
||||||
|
"narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-24.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
description = "Flake for github:Infisical/infisical repository.";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }: {
|
||||||
|
devShells.aarch64-darwin.default = let
|
||||||
|
pkgs = nixpkgs.legacyPackages.aarch64-darwin;
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
git
|
||||||
|
lazygit
|
||||||
|
|
||||||
|
python312Full
|
||||||
|
nodejs_20
|
||||||
|
nodePackages.prettier
|
||||||
|
infisical
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Generated
+245
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.7.2",
|
"@casl/ability": "^6.7.2",
|
||||||
"@casl/react": "^4.0.0",
|
"@casl/react": "^4.0.0",
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -47,8 +48,10 @@
|
|||||||
"@tanstack/react-router": "^1.95.1",
|
"@tanstack/react-router": "^1.95.1",
|
||||||
"@tanstack/virtual-file-routes": "^1.87.6",
|
"@tanstack/virtual-file-routes": "^1.87.6",
|
||||||
"@tanstack/zod-adapter": "^1.91.0",
|
"@tanstack/zod-adapter": "^1.91.0",
|
||||||
|
"@types/dagre": "^0.7.52",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
"@xyflow/react": "^12.4.4",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -507,6 +510,24 @@
|
|||||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dagrejs/dagre": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dagrejs/graphlib": "2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dagrejs/graphlib": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@date-fns/tz": {
|
"node_modules/@date-fns/tz": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
|
||||||
@@ -3955,6 +3976,61 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.52",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.52.tgz",
|
||||||
|
"integrity": "sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -4382,6 +4458,64 @@
|
|||||||
"vite": "^4 || ^5 || ^6"
|
"vite": "^4 || ^5 || ^6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xyflow/react": {
|
||||||
|
"version": "12.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.4.tgz",
|
||||||
|
"integrity": "sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xyflow/system": "0.0.52",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/react/node_modules/zustand": {
|
||||||
|
"version": "4.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
|
||||||
|
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/system": {
|
||||||
|
"version": "0.0.52",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.52.tgz",
|
||||||
|
"integrity": "sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-drag": "^3.0.7",
|
||||||
|
"@types/d3-selection": "^3.0.10",
|
||||||
|
"@types/d3-transition": "^3.0.8",
|
||||||
|
"@types/d3-zoom": "^3.0.8",
|
||||||
|
"d3-drag": "^3.0.0",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||||
@@ -5456,6 +5590,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classcat": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/classnames": {
|
"node_modules/classnames": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
@@ -5808,6 +5948,111 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.7.2",
|
"@casl/ability": "^6.7.2",
|
||||||
"@casl/react": "^4.0.0",
|
"@casl/react": "^4.0.0",
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -51,8 +52,10 @@
|
|||||||
"@tanstack/react-router": "^1.95.1",
|
"@tanstack/react-router": "^1.95.1",
|
||||||
"@tanstack/virtual-file-routes": "^1.87.6",
|
"@tanstack/virtual-file-routes": "^1.87.6",
|
||||||
"@tanstack/zod-adapter": "^1.91.0",
|
"@tanstack/zod-adapter": "^1.91.0",
|
||||||
|
"@types/dagre": "^0.7.52",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
"@xyflow/react": "^12.4.4",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { generateUserBackupKey } from "@app/lib/crypto";
|
|
||||||
|
|
||||||
import { createNotification } from "../notifications";
|
|
||||||
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
|
|
||||||
import { Button } from "../v2";
|
|
||||||
|
|
||||||
interface DownloadBackupPDFStepProps {
|
|
||||||
incrementStep: () => void;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the step of the signup flow where the user downloads the backup pdf
|
|
||||||
* @param {object} obj
|
|
||||||
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
|
|
||||||
* @param {string} obj.email - user's email
|
|
||||||
* @param {string} obj.password - user's password
|
|
||||||
* @param {string} obj.name - user's name
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function DonwloadBackupPDFStep({
|
|
||||||
incrementStep,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
name
|
|
||||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useToggle();
|
|
||||||
|
|
||||||
const handleBackupKeyGenerate = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading.on();
|
|
||||||
const generatedKey = await generateUserBackupKey(email, password);
|
|
||||||
await generateBackupPDFAsync({
|
|
||||||
generatedKey,
|
|
||||||
personalEmail: email,
|
|
||||||
personalName: name
|
|
||||||
});
|
|
||||||
incrementStep();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to generate backup key"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading.off();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
|
|
||||||
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faWarning}
|
|
||||||
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
|
|
||||||
/>
|
|
||||||
{t("signup.step4-message")}
|
|
||||||
</p>
|
|
||||||
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
|
|
||||||
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
|
|
||||||
<span className="mb-2">
|
|
||||||
{t("signup.step4-description1")} {t("signup.step4-description3")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
|
|
||||||
<div className="text-l w-full py-1 text-lg">
|
|
||||||
<Button
|
|
||||||
onClick={handleBackupKeyGenerate}
|
|
||||||
size="sm"
|
|
||||||
isFullWidth
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading}
|
|
||||||
className="h-12"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
>
|
|
||||||
Download PDF
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ import { encodeBase64 } from "tweetnacl-util";
|
|||||||
import { initProjectHelper } from "@app/helpers/project";
|
import { initProjectHelper } from "@app/helpers/project";
|
||||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
|
import { onRequestError } from "@app/hooks/api/reactQuery";
|
||||||
|
|
||||||
import InputField from "../basic/InputField";
|
import InputField from "../basic/InputField";
|
||||||
import checkPassword from "../utilities/checks/password/checkPassword";
|
import checkPassword from "../utilities/checks/password/checkPassword";
|
||||||
@@ -206,6 +207,7 @@ export default function UserInfoStep({
|
|||||||
|
|
||||||
incrementStep();
|
incrementStep();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
onRequestError(error);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().nonempty({ message: "Name is required" })
|
name: GenericResourceNameSchema.nonempty({ message: "Name is required" })
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen}>
|
<Modal modal={false} isOpen={isOpen}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Create Organization"
|
title="Create Organization"
|
||||||
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
|
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faUpRightAndDownLeftFromCenter,
|
||||||
|
faWindowRestore
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
ConnectionLineType,
|
||||||
|
Controls,
|
||||||
|
Node,
|
||||||
|
NodeMouseHandler,
|
||||||
|
Panel,
|
||||||
|
ReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
|
useReactFlow
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||||
|
|
||||||
|
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
||||||
|
import { BasePermissionEdge } from "./edges";
|
||||||
|
import { useAccessTree } from "./hooks";
|
||||||
|
import { FolderNode, RoleNode } from "./nodes";
|
||||||
|
import { ViewMode } from "./types";
|
||||||
|
|
||||||
|
export type AccessTreeProps = {
|
||||||
|
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EdgeTypes = { base: BasePermissionEdge };
|
||||||
|
|
||||||
|
const NodeTypes = { role: RoleNode, folder: FolderNode };
|
||||||
|
|
||||||
|
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||||
|
const accessTreeData = useAccessTree(permissions);
|
||||||
|
const { edges, nodes, isLoading, viewMode, setViewMode } = accessTreeData;
|
||||||
|
|
||||||
|
const { fitView, getViewport, setCenter } = useReactFlow();
|
||||||
|
|
||||||
|
const onNodeClick: NodeMouseHandler<Node> = useCallback(
|
||||||
|
(_, node) => {
|
||||||
|
setCenter(
|
||||||
|
node.position.x + (node.width ? node.width / 2 : 0),
|
||||||
|
node.position.y + (node.height ? node.height / 2 + 50 : 50),
|
||||||
|
{ duration: 1000, zoom: 1 }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setCenter]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({
|
||||||
|
padding: 0.2,
|
||||||
|
duration: 1000,
|
||||||
|
maxZoom: 1
|
||||||
|
});
|
||||||
|
}, 1);
|
||||||
|
}, [fitView, nodes, edges, getViewport()]);
|
||||||
|
|
||||||
|
const handleToggleModalView = () =>
|
||||||
|
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||||
|
|
||||||
|
const handleToggleUndockedView = () =>
|
||||||
|
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked));
|
||||||
|
|
||||||
|
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`;
|
||||||
|
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"w-full",
|
||||||
|
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
|
||||||
|
viewMode === ViewMode.Undocked &&
|
||||||
|
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"mb-4 h-full w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 transition-transform duration-500",
|
||||||
|
viewMode === ViewMode.Docked ? "relative p-4" : "relative p-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{viewMode === ViewMode.Docked && (
|
||||||
|
<div className="mb-4 flex items-start justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">Access Tree</h3>
|
||||||
|
<p className="text-sm leading-3 text-mineshaft-400">
|
||||||
|
Visual access policies for the configured role.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
className="h-10 rounded-r-none bg-mineshaft-700"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
|
||||||
|
onClick={handleToggleUndockedView}
|
||||||
|
>
|
||||||
|
Undock
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
className="h-10 rounded-l-none bg-mineshaft-600"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faUpRightAndDownLeftFromCenter} />}
|
||||||
|
onClick={handleToggleModalView}
|
||||||
|
>
|
||||||
|
Expand
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex items-center space-x-4",
|
||||||
|
viewMode === ViewMode.Docked ? "h-96" : "h-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<ReactFlow
|
||||||
|
className="rounded-md border border-mineshaft"
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
edgeTypes={EdgeTypes}
|
||||||
|
nodeTypes={NodeTypes}
|
||||||
|
fitView
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
colorMode="dark"
|
||||||
|
nodesDraggable={false}
|
||||||
|
edgesReconnectable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
connectionLineType={ConnectionLineType.SmoothStep}
|
||||||
|
proOptions={{
|
||||||
|
hideAttribution: false // we need pro license if we want to hide
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<Panel className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
{viewMode !== ViewMode.Docked && (
|
||||||
|
<Panel position="top-right" className="flex gap-1.5">
|
||||||
|
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
||||||
|
<IconButton
|
||||||
|
className="mr-1 rounded"
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleToggleUndockedView}
|
||||||
|
ariaLabel={undockButtonLabel}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
viewMode === ViewMode.Undocked
|
||||||
|
? faArrowUpRightFromSquare
|
||||||
|
: faWindowRestore
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip align="end" position="bottom" content={windowButtonLabel}>
|
||||||
|
<IconButton
|
||||||
|
className="rounded"
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={handleToggleModalView}
|
||||||
|
ariaLabel={windowButtonLabel}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
viewMode === ViewMode.Modal
|
||||||
|
? faArrowUpRightFromSquare
|
||||||
|
: faUpRightAndDownLeftFromCenter
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
<PermissionSimulation {...accessTreeData} />
|
||||||
|
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
|
||||||
|
<Controls position="bottom-left" />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccessTree = (props: AccessTreeProps) => {
|
||||||
|
return (
|
||||||
|
<AccessTreeErrorBoundary {...props}>
|
||||||
|
<AccessTreeProvider>
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<AccessTreeContent {...props} />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
</AccessTreeProvider>
|
||||||
|
</AccessTreeErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import { ViewMode } from "../types";
|
||||||
|
|
||||||
|
export interface AccessTreeContextProps {
|
||||||
|
secretName: string;
|
||||||
|
setSecretName: Dispatch<SetStateAction<string>>;
|
||||||
|
viewMode: ViewMode;
|
||||||
|
setViewMode: Dispatch<SetStateAction<ViewMode>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessTreeContext = createContext<AccessTreeContextProps | undefined>(undefined);
|
||||||
|
|
||||||
|
interface AccessTreeProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
|
||||||
|
const [secretName, setSecretName] = useState("");
|
||||||
|
const [viewMode, setViewMode] = useState(ViewMode.Docked);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
secretName,
|
||||||
|
setSecretName,
|
||||||
|
viewMode,
|
||||||
|
setViewMode
|
||||||
|
}),
|
||||||
|
[secretName, setSecretName, viewMode, setViewMode]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <AccessTreeContext.Provider value={value}>{children}</AccessTreeContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAccessTreeContext = (): AccessTreeContextProps => {
|
||||||
|
const context = useContext(AccessTreeContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAccessTreeContext must be used within a AccessTreeProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { ErrorInfo, ReactNode } from "react";
|
||||||
|
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||||
|
import { faCheck, faCopy, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { IconButton } from "@app/components/v2";
|
||||||
|
import { SessionStorageKeys } from "@app/const";
|
||||||
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorDisplay = ({
|
||||||
|
error,
|
||||||
|
permissions
|
||||||
|
}: {
|
||||||
|
error: Error | null;
|
||||||
|
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
|
}) => {
|
||||||
|
const display = JSON.stringify({ errorMessage: error?.message, permissions }, null, 2);
|
||||||
|
|
||||||
|
const [isCopied, , setIsCopied] = useTimedReset<boolean>({
|
||||||
|
initialState: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(display);
|
||||||
|
setIsCopied(true);
|
||||||
|
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2 text-mineshaft-100">
|
||||||
|
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red" />
|
||||||
|
<p>
|
||||||
|
Error displaying access tree. Please contact{" "}
|
||||||
|
<a
|
||||||
|
className="inline cursor-pointer text-mineshaft-200 underline decoration-primary-500 underline-offset-4 duration-200 hover:text-mineshaft-100"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
with the following information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex flex-1 flex-col overflow-hidden">
|
||||||
|
<pre className="thin-scrollbar w-full flex-1 overflow-y-auto whitespace-pre-wrap rounded bg-mineshaft-700 p-2 text-xs text-mineshaft-100">
|
||||||
|
{display}
|
||||||
|
</pre>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="absolute right-4 top-2"
|
||||||
|
ariaLabel="Copy secret value"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isCopied ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
console.error("Error caught by ErrorBoundary:", error, errorInfo, this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { hasError, error } = this.state;
|
||||||
|
const { children, permissions } = this.props;
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return <ErrorDisplay error={error} permissions={permissions} />;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessTreeErrorBoundary = ({ children, permissions }: ErrorBoundaryProps) => {
|
||||||
|
return <ErrorBoundary permissions={permissions}>{children}</ErrorBoundary>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { Panel } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { Button, FormLabel, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
import { ViewMode } from "../types";
|
||||||
|
|
||||||
|
type TProps = {
|
||||||
|
secretName: string;
|
||||||
|
setSecretName: Dispatch<SetStateAction<string>>;
|
||||||
|
viewMode: ViewMode;
|
||||||
|
setViewMode: Dispatch<SetStateAction<ViewMode>>;
|
||||||
|
setEnvironment: Dispatch<SetStateAction<string>>;
|
||||||
|
environment: string;
|
||||||
|
subject: ProjectPermissionSub;
|
||||||
|
setSubject: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PermissionSimulation = ({
|
||||||
|
setEnvironment,
|
||||||
|
environment,
|
||||||
|
subject,
|
||||||
|
setSubject,
|
||||||
|
environments,
|
||||||
|
setViewMode,
|
||||||
|
viewMode,
|
||||||
|
secretName,
|
||||||
|
setSecretName
|
||||||
|
}: TProps) => {
|
||||||
|
const [expand, setExpand] = useState(false);
|
||||||
|
|
||||||
|
const handlePermissionSimulation = () => {
|
||||||
|
setExpand(true);
|
||||||
|
setViewMode(ViewMode.Modal);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (viewMode !== ViewMode.Modal)
|
||||||
|
return (
|
||||||
|
<Panel position="top-left">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
className="mr-1 rounded"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={handlePermissionSimulation}
|
||||||
|
>
|
||||||
|
Permission Simulation
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
onClick={handlePermissionSimulation}
|
||||||
|
position="top-left"
|
||||||
|
className={`group flex flex-col gap-2 pb-4 pr-4 ${expand ? "" : "cursor-pointer"}`}
|
||||||
|
>
|
||||||
|
<div className="flex w-[20rem] flex-col gap-1.5 rounded border border-mineshaft-600 bg-mineshaft-800 p-2 font-inter text-gray-200">
|
||||||
|
<div>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<span className="text-sm">Permission Simulation</span>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel={expand ? "Collapse" : "Expand"}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setExpand((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={expand ? faChevronUp : faChevronDown} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
{expand && (
|
||||||
|
<p className="mb-2 mt-1 text-xs text-mineshaft-400">
|
||||||
|
Evaluate conditional policies to see what permissions will be granted given a secret
|
||||||
|
name or tags
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{expand && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<FormLabel label="Subject" />
|
||||||
|
<Select
|
||||||
|
value={subject}
|
||||||
|
onValueChange={(value) => setSubject(value as ProjectPermissionSub)}
|
||||||
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
ProjectPermissionSub.Secrets,
|
||||||
|
ProjectPermissionSub.SecretFolders,
|
||||||
|
ProjectPermissionSub.DynamicSecrets,
|
||||||
|
ProjectPermissionSub.SecretImports
|
||||||
|
].map((sub) => {
|
||||||
|
return (
|
||||||
|
<SelectItem className="capitalize" value={sub} key={sub}>
|
||||||
|
{sub.replace("-", " ")}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormLabel label="Environment" />
|
||||||
|
<Select
|
||||||
|
value={environment}
|
||||||
|
onValueChange={setEnvironment}
|
||||||
|
className="w-full border border-mineshaft-500 capitalize"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-[19rem]"
|
||||||
|
>
|
||||||
|
{environments.map(({ name, slug }) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={slug} key={slug}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{subject === ProjectPermissionSub.Secrets && (
|
||||||
|
<div>
|
||||||
|
<FormLabel label="Secret Name" />
|
||||||
|
<Input
|
||||||
|
placeholder="*"
|
||||||
|
value={secretName}
|
||||||
|
onChange={(e) => setSecretName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./AccessTreeContext";
|
||||||
|
export * from "./AccessTreeErrorBoundary";
|
||||||
|
export * from "./PermissionSimulation";
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { BaseEdge, BaseEdgeProps, EdgeProps, getSmoothStepPath } from "@xyflow/react";
|
||||||
|
|
||||||
|
export const BasePermissionEdge = ({
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
style
|
||||||
|
}: Omit<BaseEdgeProps, "path"> & EdgeProps) => {
|
||||||
|
const [edgePath] = getSmoothStepPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseEdge
|
||||||
|
id={id}
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
style={{
|
||||||
|
strokeDasharray: "5",
|
||||||
|
strokeWidth: 1,
|
||||||
|
stroke: "#707174",
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
path={edgePath}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./BasePermissionEdge";
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||||
|
import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||||
|
import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries";
|
||||||
|
|
||||||
|
import { useAccessTreeContext } from "../components";
|
||||||
|
import { PermissionAccess } from "../types";
|
||||||
|
import {
|
||||||
|
createBaseEdge,
|
||||||
|
createFolderNode,
|
||||||
|
createRoleNode,
|
||||||
|
getSubjectActionRuleMap,
|
||||||
|
positionElements
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, MongoQuery>) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
||||||
|
const [nodes, setNodes] = useNodesState<Node>([]);
|
||||||
|
const [edges, setEdges] = useEdgesState<Edge>([]);
|
||||||
|
const [subject, setSubject] = useState(ProjectPermissionSub.Secrets);
|
||||||
|
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
|
||||||
|
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
|
||||||
|
currentWorkspace.id
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return;
|
||||||
|
|
||||||
|
const { folders, name } = environmentsFolders[environment];
|
||||||
|
|
||||||
|
const roleNode = createRoleNode({
|
||||||
|
subject,
|
||||||
|
environment: name
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
||||||
|
|
||||||
|
const folderNodes = folders.map((folder) =>
|
||||||
|
createFolderNode({
|
||||||
|
folder,
|
||||||
|
permissions,
|
||||||
|
environment,
|
||||||
|
subject,
|
||||||
|
secretName,
|
||||||
|
actionRuleMap
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const folderEdges = folderNodes.map(({ data: folder }) => {
|
||||||
|
const actions = Object.values(folder.actions);
|
||||||
|
|
||||||
|
let access: PermissionAccess;
|
||||||
|
if (Object.values(actions).some((action) => action === PermissionAccess.Full)) {
|
||||||
|
access = PermissionAccess.Full;
|
||||||
|
} else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) {
|
||||||
|
access = PermissionAccess.Partial;
|
||||||
|
} else {
|
||||||
|
access = PermissionAccess.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createBaseEdge({
|
||||||
|
source: folder.parentId ?? roleNode.id,
|
||||||
|
target: folder.id,
|
||||||
|
access
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = positionElements([roleNode, ...folderNodes], [...folderEdges]);
|
||||||
|
setNodes(init.nodes);
|
||||||
|
setEdges(init.edges);
|
||||||
|
}, [permissions, environmentsFolders, environment, subject, secretName, setNodes, setEdges]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
subject,
|
||||||
|
environment,
|
||||||
|
setEnvironment,
|
||||||
|
setSubject,
|
||||||
|
isLoading: isPending,
|
||||||
|
environments: currentWorkspace.environments,
|
||||||
|
secretName,
|
||||||
|
setSecretName,
|
||||||
|
viewMode,
|
||||||
|
setViewMode
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./AccessTree";
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
faCheckCircle,
|
||||||
|
faCircleMinus,
|
||||||
|
faCircleXmark,
|
||||||
|
faFolder
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { Tooltip } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { PermissionAccess } from "../../types";
|
||||||
|
import { createFolderNode, formatActionName } from "../../utils";
|
||||||
|
import { FolderNodeTooltipContent } from "./components";
|
||||||
|
|
||||||
|
const AccessMap = {
|
||||||
|
[PermissionAccess.Full]: { className: "text-green", icon: faCheckCircle },
|
||||||
|
[PermissionAccess.Partial]: { className: "text-yellow", icon: faCircleMinus },
|
||||||
|
[PermissionAccess.None]: { className: "text-red", icon: faCircleXmark }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FolderNode = ({
|
||||||
|
data
|
||||||
|
}: NodeProps & { data: ReturnType<typeof createFolderNode>["data"] }) => {
|
||||||
|
const { name, actions, actionRuleMap, parentId, subject } = data;
|
||||||
|
|
||||||
|
const hasMinimalAccess = Object.values(actions).some(
|
||||||
|
(action) => action === PermissionAccess.Full || action === PermissionAccess.Partial
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
className="pointer-events-none !cursor-pointer opacity-0"
|
||||||
|
position={Position.Top}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`flex ${hasMinimalAccess ? "" : "opacity-40"} h-full w-full flex-col items-center justify-center rounded-md border border-mineshaft bg-mineshaft-800 px-2 py-3 font-inter shadow-lg transition-opacity duration-500`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-mineshaft-100">
|
||||||
|
<FontAwesomeIcon className="mb-0.5 font-medium text-yellow" icon={faFolder} />
|
||||||
|
<span>{parentId ? `/${name}` : "/"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1.5 flex w-full flex-wrap items-center justify-center gap-x-2 gap-y-1 rounded bg-mineshaft-600 px-2 py-1 text-xs">
|
||||||
|
{Object.entries(actions).map(([action, access]) => {
|
||||||
|
const { className, icon } = AccessMap[access];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={action}
|
||||||
|
className="hidden" // just using the tooltip to trigger node toolbar
|
||||||
|
content={
|
||||||
|
<FolderNodeTooltipContent
|
||||||
|
action={action}
|
||||||
|
access={access}
|
||||||
|
subject={subject}
|
||||||
|
actionRuleMap={actionRuleMap}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<FontAwesomeIcon icon={icon} className={className} size="xs" />
|
||||||
|
<span className="capitalize">{formatActionName(action)}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
className="pointer-events-none !cursor-pointer opacity-0"
|
||||||
|
position={Position.Bottom}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { faCheckCircle, faCircleMinus, faCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { NodeToolbar, Position } from "@xyflow/react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatedConditionsOperatorNames,
|
||||||
|
PermissionConditionOperators
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||||
|
|
||||||
|
import { PermissionAccess } from "../../../types";
|
||||||
|
import { createFolderNode, formatActionName } from "../../../utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
action: string;
|
||||||
|
access: PermissionAccess;
|
||||||
|
} & Pick<ReturnType<typeof createFolderNode>["data"], "actionRuleMap" | "subject">;
|
||||||
|
|
||||||
|
export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subject }: Props) => {
|
||||||
|
let component: ReactElement;
|
||||||
|
|
||||||
|
switch (access) {
|
||||||
|
case PermissionAccess.Full:
|
||||||
|
component = (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-1.5 capitalize text-green">
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} size="xs" />
|
||||||
|
<span>Full {formatActionName(action)} Permissions</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-mineshaft-200">
|
||||||
|
Policy grants unconditional{" "}
|
||||||
|
<span className="font-medium text-mineshaft-100">
|
||||||
|
{formatActionName(action).toLowerCase()}
|
||||||
|
</span>{" "}
|
||||||
|
permission for {subject.replaceAll("-", " ")} in this folder.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case PermissionAccess.Partial:
|
||||||
|
component = (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-1.5 capitalize text-yellow">
|
||||||
|
<FontAwesomeIcon icon={faCircleMinus} className="text-yellow" size="xs" />
|
||||||
|
<span>Conditional {formatActionName(action)} Permissions</span>
|
||||||
|
</div>
|
||||||
|
<p className="mb-1 text-mineshaft-200">
|
||||||
|
Policy conditionally allows{" "}
|
||||||
|
<span className="font-medium text-mineshaft-100">
|
||||||
|
{formatActionName(action).toLowerCase()}
|
||||||
|
</span>{" "}
|
||||||
|
permission for {subject.replaceAll("-", " ")} in this folder.
|
||||||
|
</p>
|
||||||
|
<ul className="flex list-disc flex-col gap-2 pl-4">
|
||||||
|
{actionRuleMap.map((ruleMap, index) => {
|
||||||
|
const rule = ruleMap[action];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!rule ||
|
||||||
|
!rule.conditions ||
|
||||||
|
(!rule.conditions.secretName && !rule.conditions.secretTags)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={`${action}_${index + 1}`}>
|
||||||
|
<span className={`italic ${rule.inverted ? "text-red" : "text-green"} `}>
|
||||||
|
{rule.inverted ? "Forbids" : "Allows"}
|
||||||
|
</span>
|
||||||
|
<span> when:</span>
|
||||||
|
{Object.entries(rule.conditions).map(([key, condition]) => (
|
||||||
|
<ul key={`${action}_${index + 1}_${key}`} className="list-[square] pl-4">
|
||||||
|
{Object.entries(condition as object).map(([operator, value]) => (
|
||||||
|
<li key={`${action}_${index + 1}_${key}_${operator}`}>
|
||||||
|
<span className="font-medium capitalize text-mineshaft-100">
|
||||||
|
{camelCaseToSpaces(key)}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-mineshaft-200">
|
||||||
|
{
|
||||||
|
formatedConditionsOperatorNames[
|
||||||
|
operator as PermissionConditionOperators
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</span>{" "}
|
||||||
|
<span className={rule.inverted ? "text-red" : "text-green"}>
|
||||||
|
{typeof value === "string" ? value : value.join(", ")}
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case PermissionAccess.None:
|
||||||
|
component = (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-1.5 capitalize text-red">
|
||||||
|
<FontAwesomeIcon icon={faCircleXmark} size="xs" />
|
||||||
|
<span>No {formatActionName(action)} Permissions</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-mineshaft-200">
|
||||||
|
Policy always forbids{" "}
|
||||||
|
<span className="font-medium text-mineshaft-100">
|
||||||
|
{formatActionName(action).toLowerCase()}
|
||||||
|
</span>{" "}
|
||||||
|
permission for {subject.replaceAll("-", " ")} in this folder.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled access type: ${access}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeToolbar
|
||||||
|
className="rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-sm font-light text-bunker-100"
|
||||||
|
isVisible
|
||||||
|
position={Position.Bottom}
|
||||||
|
>
|
||||||
|
{component}
|
||||||
|
</NodeToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./FolderNodeTooltipContent";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./FolderNode";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user